Skip to main content

Architecture6 min read

Modernizing legacy systems without a big-bang rewrite

An incremental approach to legacy modernization: finding seams, characterization tests, the strangler fig pattern and migrating data safely.

Verolution EngineeringPublished

Long-lived systems eventually become hard to change. The code encodes decisions made by people who have since moved on, parts of the platform are out of support, and every release feels risky. The tempting answer is to start over: a clean rewrite on a modern stack, with the old system switched off on launch day.

Incremental modernization is less dramatic and more dependable. The system is replaced one capability at a time while it keeps running and serving the business.

Why big-bang rewrites are risky

  • The target moves. The business cannot pause while you rebuild, so the old system keeps changing. Either you freeze it, which the business resents, or the rewrite chases a moving specification.
  • Behavior is undocumented. Years of fixes, edge cases and business rules live only in the code, and users rely on quirks nobody intended. Hyrum's Law describes this: with enough users, every observable behavior of a system will be depended on by somebody. A rewrite built from specifications tends to rediscover those behaviors as production incidents.
  • Value arrives late. Nothing is usable until the rewrite is complete, so patience and funding are tested long before any payoff.
  • The cutover is all or nothing. Moving every user and all the data in one window concentrates risk into a single event, and rollback is often impractical once new data has been written.

A rewrite can still be the right call for a small, well-understood system. For most business-critical systems, an incremental path is the safer choice.

Grow the new system with the strangler fig pattern

Martin Fowler named the strangler fig pattern (opens in a new tab) after plants that grow around a host tree and gradually replace it. The new system grows around the edges of the old one, taking over capabilities one at a time until the legacy system can be switched off.

Each step is small enough to test, release and reverse if necessary, and the business benefits from the first migrated capability, not the last. The cost is running two systems for a while, with routing, data synchronization and extra operational work. Budget for it.

Find the seams

In Working Effectively with Legacy Code, Michael Feathers describes a seam as a place where you can change a program's behavior without editing it in that place. At the architecture level, seams are wherever work can be intercepted or redirected: HTTP endpoints and UI routes, message queues, file drops, scheduled jobs and integration adapters.

Map them, noting the capabilities and data behind each entry point. The best early seams have clear boundaries and limited data coupling. Inside a codebase, branch by abstraction does the same job: put an interface in front of the component, move callers onto it, then swap the implementation behind it.

Pin down current behavior with characterization tests

Feathers also describes characterization tests: tests that record what a system actually does today, not what a specification says it should do. Feed in representative inputs, capture the outputs and assert them. They freeze existing behavior, bugs included, so any difference in a new implementation is either a new bug or an undocumented behavior that someone must decide to keep or drop.

For larger surfaces, replay recorded production requests, with sensitive data masked, against both implementations and diff the results. Stronger still is a parallel run: the new implementation processes live traffic alongside the old one, the legacy result is still the one returned, and every difference is investigated. This only works if the new path cannot cause side effects such as sending messages or moving money.

Put a facade in front of the legacy system

A facade, such as a reverse proxy or API gateway for web traffic, sits between clients and the legacy system. At first it forwards everything unchanged; as capabilities move, routing rules send their requests to the new system.

Route by path, operation, customer segment or percentage of traffic, controlled by configuration or feature flags, so moving traffic (and moving it back) is a configuration change rather than a deployment. Keep the facade thin: business logic that accumulates there becomes one more system to migrate.

Migrate one capability at a time

Choose the first slice for what it teaches: meaningful enough to prove the approach, and loosely coupled enough that a problem is survivable. Read-only capabilities such as lookups and reports are often good candidates, because they create no new data that must be kept in sync. The first slice also builds the shared machinery (routing, pipeline, observability, data synchronization) that later slices reuse.

Keep legacy concepts out with an anti-corruption layer

Legacy systems carry their own vocabulary: overloaded status codes, repurposed fields, names that no longer mean what they say. If new components talk to the legacy system directly, those concepts leak into the new design, and you end up rebuilding the old model on a new stack.

Eric Evans' Domain-Driven Design describes the anti-corruption layer for this situation: an isolating layer that translates between the two models, so the new system works in its own terms while the legacy system needs little or no change. Often built from facades, adapters and translators, it is also where knowledge such as what a legacy status code really means gets captured as tested code.

Migrate data deliberately

Data is usually the hardest part. Hold to one rule: at any moment, every data entity has exactly one system of record. The common options each carry risk:

  • Temporarily shared database. New components use the legacy database directly: quick to start, but it couples new code to the old schema, so treat it as a short-lived step.
  • Dual writes. The application writes to both stores. Without a transaction spanning both, a failure between the writes leaves them inconsistent, and concurrent updates can land in different orders. If you use them, make writes idempotent and back them with reconciliation.
  • Change data capture. Changes stream from the legacy database's transaction log to the new store, using a tool such as Debezium. The legacy system stays the system of record and its code is untouched, at the cost of replication lag, schema mapping and the need for log access.
  • Bulk migration with a cutover window. Simplest for small or rarely changing datasets, but it requires a freeze or downtime.

Whatever you choose, reconcile continuously: record counts, checksums by partition, field-level comparisons on samples, and business invariants such as matching account balances. Alert on discrepancies, decide in advance how they get repaired, and plan how data written by the new system flows back if a capability has to be rolled back.

Decommission deliberately

A common failure mode is a migration that never finishes: the new system does most of the work, the old one still runs a few functions, and you operate both indefinitely. Make switching off part of the plan:

  • Instrument legacy entry points to see what still calls them, and observe a full business cycle, including month-end and year-end processing, before declaring a path unused.
  • Look for hidden consumers: reports, spreadsheets connected straight to the database, partner integrations and scheduled exports.
  • Make components read-only before shutting them down, and archive data according to your retention and legal obligations.
  • Remove the credentials, network routes, infrastructure and licenses, and update the runbooks.

Measure progress by risk retired

Track measures that show risk being retired, not activity such as lines of code rewritten:

  • Share of traffic or transactions served by the new system, per capability.
  • Capabilities migrated and decommissioned, not just built.
  • Legacy run cost: infrastructure, licenses and support effort.
  • Delivery health in migrated areas, such as lead time for changes and change failure rate.

Where to start

  1. Map the legacy system's entry points and data flows, and identify candidate seams.
  2. Write characterization tests around the first capability you intend to move.
  3. Put a routing facade in front of the legacy system, forwarding everything, before migrating anything.
  4. Choose a loosely coupled first slice, ideally read-only, and define done to include switching off its legacy path.
  5. Decide the data strategy for that slice, including reconciliation checks and a rollback path.

Our IT consulting team can help you weigh a rewrite against an incremental path.

More engineering notes

Start a conversation

Let’s engineer what’s next.

Tell us what you’re building, fixing or exploring. We’ll help you find a sensible first step — whether that’s a focused review, a prototype or a full engagement.