By clicking “Accept”, you agree to the storing of cookies on your device. View our Privacy Policy.
December 15, 2025
2
min read

Legacy systems aren't the problem!

The Wrong Approach Is

Bart Szczepański
CTO

This is the story of how we transformed a decade-old .NET Framework 4.5 logistics platform into a modern .NET 9 and Nuxt 4 application without stopping the business, a big-bang rewrite, and without losing the institutional knowledge that made the system valuable in the first place.

The myth of "bad" legacy code

There's a dangerous myth in our industry that legacy code is bad code. Well, it's not! Legacy code is code that survived.

Consider what that actually means. This code processed millions of transactions. It handled edge cases nobody documented. It kept the business running while JavaScript frameworks rose and fell like empires. That AngularJS app from 2014? It outlived Backbone, Ember, and at least two versions of Angular that were supposedly going to replace it.

When a logistics company approached us with a decade-old .NET Framework 4.5 dashboard, I saw opportunity. A system that had served them well for years, now ready for modernization. With our experience in legacy migrations, I knew exactly where to start.

Here's what we inherited:

  • .NET Framework 4.5 with ASP.NET MVC 5 and OWIN authentication
  • AngularJS 1.x (the original, before the great Angular rewrite)
  • Breeze.js for data management with OData queries
  • Entity Framework 6 with business logic scattered across controllers
  • jQuery 2.1 holding it all together

Modern? Not even remotely by 2025 standards. But this system had run their business reliably for over ten years - huge kudos to the engineer who built it. That kind of longevity should be the foundation of every project. Let's be honest though, no system is without flaws. Even when a feature appears to work perfectly, as the system grows and accumulates more data, edge cases emerge that can break performance, degrade the user experience, and chip away at reliability.

Why most modernizations fail

You've seen this movie before. The typical approach unfolds predictably:

  1. Declare the system unmaintainable
  2. Get approval for a rewrite
  3. Spend 8-15 months building something new
  4. Discover you missed critical edge cases
  5. Lose competitive advantage while the business waits

The problem isn't the legacy code. It's the big-bang rewrite mentality.
Successful modernization requires understanding why decisions were made, not just cataloging what exists. It means recognizing patterns from previous technology generations and knowing which "bad practices" were actually best practices for their era. Most importantly, it demands the discipline to change incrementally rather than heroically.

The right approach: Strangler Fig pattern

So what actually works when modernizing a decade-old system? After researching various approaches, we landed on Martin Fowler's Strangler Fig pattern. The core insight is deceptively simple yet elegant: instead of attempting a risky big-bang replacement, you gradually grow the new system around the old one. You don't tear down what works - you build alongside it until the old system naturally fades away.

Phase 1: New frontend, same database

Before writing a single line of new code, we mapped the existing system - every integration point, every data flow, every assumption baked into the architecture.

Diagram 1: Both legacy AngularJS and modern Nuxt 4 frontends share the same SQL Server database, enabling zero-downtime migration.

The key design decision: both systems shared the same database. Users could switch between interfaces at will. If the new system encountered issues, we could roll back instantly. The business never stopped operating. No dramatic switchover. Just a gradual shift as users gained confidence in the new system.

Phase 2: Clean Architecture backend

For the new backend, we adopted Clean Architecture with CQRS. Not because it's fashionable, but because it enforces the separation that legacy systems typically lack. When business logic is scattered across controllers, validation lives in three different places, and nobody's certain where the authoritative rules reside, clear boundaries become essential.

Diagram 2: Four distinct layers enforce separation that legacy systems often lack.

The architecture proves its worth. Commands handle writes with explicit intent. Queries handle reads, optimized independently. The practical benefits:

  • Add caching to queries without affecting commands
  • Test business logic in complete isolation
  • Swap infrastructure (moving from SQL Server to PostgreSQL, for instance) without touching domain logic

This separation paid dividends almost immediately when we needed to optimize read performance without risking write operation integrity.

Phase 3: New capabilities, not just new tech

Something critical gets lost in modernization discussions: the real value isn't switching frameworks. Anyone can do that. The real value is enabling capabilities the legacy system couldn't support.

Diagram 3: Real-time SuperDispatch webhook integration. A capability impossible with the legacy Breeze.js/OData architecture.

Over the years, the business evolved and the customer came to rely heavily on SuperDispatch, a third-party platform for managing vehicle transport orders. This created a real challenge: they needed webhook integration with SuperDispatch to get real-time order updates. In the legacy system, this would have required serious architectural gymnastics. Breeze.js was designed for client-to-server queries, not server-to-server event handling - webhooks simply weren't in its vocabulary. With our new architecture we added a webhook endpoint, wrote a handler, and had real-time order updates flowing within a day.
That's the capability unlock that justifies modernization. Not "we're using newer tech" but "we can now do things that were previously impossible."

From untyped chaos to compile-time confidence

The frontend transformation is where developers feel the difference most acutely. The legacy AngularJS code had no types. Services communicated through magic strings. Breeze.js queries returned untyped entities whose shape you simply had to know. Every refactoring session meant hoping you didn't break something in production. For the new frontend, we chose Nuxt 4 (yes, it's Nuxt not Next :) ) with Vue 3.5 and TypeScript:

The UI layer, we selected shadcn-vue and Tailwind CSS 4. Accessible, customizable primitives without vendor lock-in. No component library tax, no fighting against someone else's design decisions.
The benefits materialized immediately. Compile-time errors caught typos before they reached production. Refactoring became safe: rename a field and TypeScript identifies every usage. New developers could understand the codebase by reading types instead of reverse-engineering behavior from runtime errors.

The tech stack evolution

Here's the complete transformation:

Layer Legacy (2015) Modern (2025)
Backend Framework .NET Framework 4.5 .NET 9
API Style Breeze.js + OData REST + CQRS
ORM Entity Framework 6 EF Core 9
Auth OWIN + OAuth JWT + ASP.NET Core Identity
Frontend Framework AngularJS 1.x Nuxt 4 + Vue 3.5
State Management Breeze.js EntityManager Vue composables + Pinia
Type Safety None TypeScript 5.9
CSS Bootstrap 3 + custom Tailwind CSS 4
Build Manual bundling Vite

The details that matter

This is where modernization projects actually succeed or fail in the weeds.

There's something deeply satisfying about watching hidden complexities surface and addressing them correctly. Two examples illustrate this.

The Archived Order Mystery

SuperDispatch's API returns status: "new" for archived orders. Counterintuitive, to say the least. The actual archived state lives in a separate boolean flag: is_archived: true. The legacy system had workarounds for this buried in controller logic. Inelegant, certainly, but understanding why those workarounds existed was essential. Without that understanding, we would have introduced subtle regressions that wouldn't surface until users complained about "missing" orders.

NOTE: Over the years, I have learned that we should NEVER blame the Engineer for their decisions. The code is only one piece of the puzzle we see.

The Invisible Sort Order

The legacy dashboard displayed orders in a specific sequence. Nobody documented why. Nobody wrote a spec. But users had relied on that sequence for years. Their muscle memory was trained on it. The SuperDispatch API returns data in a different order. User expectations are requirements, even when undocumented. We added a ChangedAt timestamp to track order modifications locally, then backfilled existing records from the API. A small detail with significant user experience impact.
These aren't glamorous problems. You won't find them in architecture blogs or conference talks. But they determine whether a modernization succeeds or fails in production.

Business results

So what did all this work actually achieve? Here are the metrics that matter:

  • Feature velocity: 3x faster development (days instead of weeks)
  • Deployment confidence: Automated CI/CD replaced manual RDP sessions
  • Integration capability: Real-time SuperDispatch sync—previously impossible
  • Onboarding time: New engineers productive in days, not weeks
  • Uptime: Zero downtime throughout the entire migration

Something that might surprise you is that the legacy AngularJS system still runs. We use it for edge-case reports that didn't justify the migration effort. That's not failure. That's pragmatism. We'll migrate those screens when the business case warrants it, not because someone decided everything must run on the new stack.

Five principles for legacy modernization

If you take nothing else from this post, take these five principles. They've guided every successful modernization we've undertaken.

Five principles that guide successful legacy modernization
  1. Legacy code isn't bad code. It's surviving code.
    Approach it with curiosity, not contempt. Ask "why does this exist?" before asking "how do I replace it?" The answers will save you from repeating history.
  2. Strangle, don't rewrite.
    Grow the new system around the old. Keep both running. Let the new system prove itself in production before considering retirement of the old one.
  3. Shared database first, then diverge.
    Start with data consistency from day one. Extract to separate databases later, once the new system is stable and you understand actual data boundaries. Don't prematurely optimize your architecture.
  4. Enable new capabilities, not just new tech.
    The value isn't the framework switch. Anyone can upgrade dependencies. The real value is what the new architecture makes possible that the old one couldn't support. If you can't articulate that difference, you're not modernizing - you're rewriting!
  5. Business value, not technical purity.
    The goal is faster feature delivery and competitive advantage - not a prettier codebase, not architecture astronautics. Solve real problems for real users.

The Rebels approach

At Rebels, we specialize in these transformations. Whether it's IoT firmware, cloud infrastructure, or enterprise software modernization, the principles remain constant:

  • Understand what exists before changing it
  • Validate assumptions with working software
  • Pilot changes with real users
  • Scale what works, iterate on what doesn't

Your legacy system isn't a liability. It's a foundation battle-tested over years, encoding business knowledge that no documentation could capture. The question isn't whether to modernize - it's whether you have the right approach to build on that foundation without burning it down.

Bart Szczepański
CTO

As a Chief Technology Officer, I work closely with our customers to provide attentive care and guide them through the complex and exciting world of the Internet of Things (IoT). My goal is to make sure every client gets the support and expertise they need to succeed with their IoT solutions. I hold a master's degree in Electronics and Telecommunications from Wrocław University of Technology. While I'm passionate about technology and innovation, I'm just as proud of my role as a husband and father. In my free time I enjoy trail running.

Rapidly adapt our competences into your IoT solution

Contact us and share your challenges

Let's Talk
Let's Talk

Contact our
IoT Expert

Prefer e-mail?
Bartłomiej
Jacyno-Onuszkiewicz
CEO, Rebels Software
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.