Replacing a large legacy system in one release is attractive on a diagram and dangerous in practice. The old system usually contains years of behaviour, undocumented edge cases, operational knowledge, and integrations that are difficult to reproduce all at once.

The strangler pattern takes a different approach: place a boundary in front of the existing system, move one capability at a time to a new implementation, and gradually reduce the responsibilities of the old system until it can be retired.

The important idea is not the routing mechanism. It is the ability to make replacement incremental.

Create a controllable boundary

A strangler migration needs a place where traffic or work can be directed to either implementation.

Conceptually, the structure is simple:

client
  |
  v
routing boundary
  |-- /accounts ------> legacy system
  |-- /invoices ------> new implementation
  `-- /reports --------> legacy system

The boundary might be an API gateway, application facade, message consumer, module interface, or another stable entry point. It does not need to be a network proxy.

What matters is that callers do not need to know which implementation currently owns a capability.

Without such a boundary, migration logic tends to leak into every caller. That creates many coordinated changes and makes rollback harder.

Migrate by capability, not by technical layer

A common mistake is to rebuild horizontal layers separately: first the database layer, then business logic, then the interface. This often leaves the old and new systems tightly coupled for a long time.

Prefer a vertical slice that owns a useful behaviour from entry point to persistence where practical.

For example, instead of migrating all customer tables first, migrate a bounded capability such as updating a customer’s notification preferences. The new path can then be tested, observed, and operated independently while unrelated customer behaviour stays in the legacy system.

A good migration slice has three properties:

  • its boundary is understandable;
  • its dependencies can be identified;
  • failure can be contained without migrating the rest of the system.

Small slices reduce the number of assumptions that must be correct at the same time.

Discover behaviour before replacing it

Legacy code is often difficult because its real contract is broader than its documented contract.

Before moving a capability, identify observable behaviour that callers depend on:

  • accepted inputs and validation rules;
  • output shapes and error semantics;
  • side effects such as emails or events;
  • ordering requirements;
  • retry and idempotency behaviour;
  • performance expectations;
  • operational procedures used during failures.

Tests around the existing behaviour can help preserve important semantics while the implementation changes. Logs and production traces can reveal paths that documentation misses.

Do not assume every historical behaviour must survive. Some behaviour is accidental or harmful. The goal is to distinguish intentional compatibility requirements from defects that should be deliberately removed.

Decide who owns data during each stage

Routing requests is usually easier than migrating data.

Before moving a capability, define which system is authoritative for every piece of state it touches. Ambiguous ownership is a common source of inconsistent data.

Suppose the legacy system owns customer profiles while the new service needs a customer’s delivery region. Several approaches are possible:

1. New code reads the legacy source directly.
2. Legacy code exposes the required data through a stable interface.
3. Data is replicated into a read model for the new capability.
4. Ownership of the relevant data moves with the capability.

Each choice has trade-offs. Direct database sharing is fast to introduce but preserves schema coupling. Replication creates synchronization concerns. Moving ownership can require a carefully staged data migration.

Choose explicitly rather than allowing both systems to write the same state without a clear consistency model.

Avoid permanent dual writes

Writing the same business change independently to two systems looks like an easy transition strategy:

request
  |-- write legacy
  `-- write new

But one write can succeed while the other fails. Retries can then duplicate effects or leave the systems disagreeing.

If temporary synchronization is necessary, define how failures are detected and repaired. Prefer a single authoritative write followed by a reliable propagation mechanism when the architecture allows it.

Most importantly, give transitional synchronization an exit condition. Temporary migration machinery has a habit of becoming permanent architecture.

Make routing changes reversible

A migration step is safer when traffic can return to the old implementation without an emergency deployment.

That does not mean every change needs an elaborate feature-flag system. It means the routing decision should be explicit and operationally understandable.

For each migrated capability, know:

How is traffic moved to the new path?
How is it moved back?
What state changes make rollback unsafe?
Who decides that rollback is necessary?

Rollback becomes more difficult after the new system starts producing state that the legacy system cannot understand. Treat that point as a deliberate migration milestone rather than discovering it during an incident.

Compare behaviour before switching ownership

For high-risk paths, the new implementation can sometimes process a copy of production input without becoming authoritative. Its output is compared with the legacy result and then discarded.

This technique is useful for deterministic calculations, classification rules, transformations, and other operations where results can be compared safely.

For example:

request
  |
  +--> legacy calculation --> response to caller
  |
  `--> new calculation -----> comparison only

Differences should be classified rather than treated automatically as defects. A mismatch may reveal a bug in the new implementation, an undocumented legacy rule, or an intentional behaviour change.

Shadow execution is less suitable for operations with irreversible side effects unless those effects can be suppressed reliably.

Measure the migration itself

A strangler migration needs signals at the capability boundary. Otherwise, teams know that traffic moved but not whether the new path is behaving correctly.

Useful measurements include:

  • request volume by implementation;
  • error rates and error categories;
  • latency distributions;
  • fallback or rollback counts;
  • data synchronization lag;
  • result mismatches during comparison;
  • business outcomes affected by the migrated capability.

Compare the new path with the legacy baseline where that comparison is meaningful. A lower exception count is not necessarily an improvement if valid requests are being silently rejected earlier.

Observability should help answer a practical question: can this capability safely remain on the new implementation?

Remove old paths as ownership moves

The strangler pattern fails when it only adds new code.

After a capability has moved and the rollback window has closed, remove obsolete routing rules, adapters, synchronization jobs, unused data access, and legacy code that no longer serves traffic.

Track migration progress in terms of retired responsibility, not only completed replacement work.

A useful sequence is:

identify capability
    -> establish boundary
    -> preserve required behaviour
    -> move traffic gradually
    -> verify operation
    -> transfer ownership
    -> remove legacy path

If the final removal step never happens, the system becomes a layered collection of old and new implementations rather than a successful migration.

Keep the destination architecture modest

A legacy replacement is already a large source of change. It is tempting to combine it with a new programming language, communication style, deployment model, persistence technology, and organizational structure.

Every additional novelty increases the number of variables involved when something fails.

Adopt new architectural ideas when they solve a concrete problem in the replacement, not because migration creates an opportunity to redesign everything.

The new system does not need to be theoretically perfect. It needs clearer boundaries, easier change, and lower operational risk than the system it replaces.

Know when not to use the pattern

Incremental replacement has costs. For a small, well-understood application with few integrations, a direct replacement may be simpler. The strangler pattern also provides less value when old and new implementations cannot coexist for technical or regulatory reasons.

Use it when the system is important enough that a single cutover is risky and when meaningful capabilities can be separated over time.

Treat migration as a sequence of completed states

The strongest strangler migrations do not run indefinitely as one giant project. They produce a series of stable intermediate architectures.

After each step, the system should have explicit ownership, known routing, observable behaviour, and a smaller legacy surface than before.

That is the practical advantage of the pattern. Instead of betting on one perfect replacement, the team repeatedly makes a bounded change, proves that it works, removes what it replaced, and then moves to the next capability.