Skip to main content
All guides

Platform modernization

Migrating by coexistence, not cutover

The strangler fig pattern is usually explained as a routing diagram. The part that decides whether it works is data ownership during the years the two systems overlap.

Updated September 29, 2025 · 8 min read

The short answer

  • The strangler fig pattern replaces a system capability by capability, running old and new side by side behind a routing layer until the old system has nothing left to serve.
  • The routing facade should only decide where a request goes. The moment it holds business rules or its own data, you have built a third system that nobody owns.
  • The genuinely hard problem is not routing but the write path: for any entity, exactly one system must own writes at any moment, and the other reads what it produces.
  • Coexistence is a cost you pay every sprint, so give each slice a planned end date and delete the legacy path as soon as the new one carries production traffic.
  • A slice is done when the legacy code for it is deleted, not when the new screen ships. Anything short of deletion means you are maintaining both systems indefinitely.

The strangler fig pattern is usually drawn as a picture: the old system on one side, the new system on the other, a proxy in the middle, and an arrow that gradually moves work across. The picture is accurate and nearly useless. Routing is the easy part. What determines whether an incremental migration succeeds is a less comfortable question: while both systems are live, which one owns the data, and what happens when they disagree. What follows is drawn from rebuilding a fifteen-year-old operational platform while the businesses running on it stayed open.

What coexistence actually commits you to

Choosing coexistence over a cutover is not the easy path; it is a different risk profile. A cutover concentrates the risk into one weekend; coexistence spreads it across every sprint for a year or more. You trade a single catastrophic failure mode for a long tail of small ones, and you accept that for the duration of the program your team maintains two systems and the seam between them.

That trade is worth making when downtime is unacceptable, when the legacy behavior is only partly documented, or when you need real usage to tell you whether the new design is right. It is not worth making for a small internal tool that four people use, where a planned Saturday migration is cheaper than six months of dual maintenance.

  • Two deployment pipelines, two on-call surfaces, and two places a bug can live
  • A permanent obligation to keep the legacy data model readable while the new one evolves
  • Feature work that has to be built once, or deliberately frozen in the old system
  • Support staff who must know which system a given user is currently hitting
  • A reconciliation process, because two systems touching the same data will drift
Real businesses use the platform every day, so a big-bang rewrite was never acceptable. The new product was built alongside the old one, preserving data compatibility and shifting workflows incrementally with a clear fallback path at each stage.
Rebuilding for the next decade, an anonymized B-Team engagement

What sits in front of both systems

Something has to decide, per request, whether the old or the new system answers. In practice this is a reverse proxy, an API gateway, or a routing table in the legacy application itself. The mechanism matters far less than the rule you enforce about it: the facade decides where a request goes, and nothing else.

Keep routing decisions coarse and legible. Route by URL path, by capability, or by tenant. Choose something a support engineer can reason about at 2am. Avoid routing on request bodies, user attributes, or percentage rollouts that vary per request, because those make the question "which system handled this transaction?" unanswerable after the fact. Log the routing decision with every request. You will need it during the first incident.

Choosing the first capability to move

The first slice is not chosen for business value. It is chosen to prove the seam works. You are testing routing, authentication across two systems, deployment, observability, and rollback. You do all of it under real traffic, with real consequences, but on something small enough to reverse in an afternoon.

Look for a capability with few write paths, clear boundaries, and a user population that will tell you quickly if something is wrong. Reporting and read-heavy views are common first choices because a stale read is recoverable and a lost write is not. Avoid starting with the most painful capability in the legacy system; that pain usually exists because it is entangled with everything else, which makes it the worst possible test of an unproven seam.

  • Small number of writes, or read-only for the first pass
  • A boundary you can describe in one sentence without using the word "and"
  • Real users who notice problems within hours, not at quarter end
  • Reversible with a routing change, not a data restore
  • Not the capability your largest customer is currently escalating about

The hard part: who owns the write path

Everything above is logistics. This is the part that determines whether the migration produces a working platform or a data integrity investigation. During coexistence, both systems can read the same entities. Only one of them can write a given entity at a given time. The instant two systems both write the same record you need distributed transactions, and you are not getting those across a fifteen-year-old application and a new one.

So you do not try. You establish single ownership per entity, per phase, and the non-owner reads. How the non-owner sees that data is a real architectural choice with real costs.

StrategyHow it worksWhere it fitsWhat it costs you
Shared databaseBoth systems read and write the same schema; the new system maps the legacy tables into its own domain model.Same datastore, single tenant boundary, and a legacy schema you can still read.Every schema change has to stay backward compatible for the whole program. The new domain model advances more slowly than it otherwise would.
Dual writeThe owning system writes to both stores in the same request path.Short overlaps where a slice is being validated and a fast rollback is required.Partial failures. The second write fails and now the two stores disagree with no transaction to unwind them. Needs idempotency keys and a reconciliation job, always.
Change data captureReads the database transaction log and streams changes into the other system asynchronously.Long overlaps, high write volume, and cases where you cannot modify legacy code safely.Eventual consistency the UI has to be honest about, plus operational ownership of a streaming pipeline nobody on the team has run before.
Read-throughThe new system holds no copy; it calls the legacy system on demand for data it does not own.Early slices, low read volume, and reference data that changes rarely.Legacy availability and latency become the new system's availability and latency. You have coupled the new platform to the thing you are replacing.
One-way batch syncA scheduled job copies data in one direction on a fixed interval.Reporting, analytics, and any consumer that tolerates being hours behind.Staleness that users will eventually act on. Safe only where the copy is explicitly read-only and labelled as such.
Coexistence data strategies and what each one costs

Whichever you choose, two things are not optional. First, make every sync path idempotent and keyed, so replaying a message or retrying a failed write cannot create duplicates. Second, run a reconciliation job that compares the two stores on a schedule and reports differences to a human. It is not there to fix drift automatically, but to tell you your ownership assumptions were wrong before a customer does.

Moving one capability, in order

  1. 1

    Write down the boundary. Which entities does this capability write, which does it only read, and which other capability owns each of those entities today.

  2. 2

    Put the facade in front of the legacy implementation first and route 100% of traffic back to legacy. Nothing has changed for users, and you have proven the routing layer, logging, and rollback before any new code is involved.

  3. 3

    Build the new implementation against the agreed data strategy, with the legacy behavior treated as the specification, including the parts that look like bugs until you ask why they exist.

  4. 4

    Run the new implementation in shadow mode: it receives real traffic and produces results, but the legacy response is what users see. Compare outputs and fix the differences you did not expect.

  5. 5

    Shift reads to the new system. Reads are reversible with a routing change, and this is where you learn about latency and permission gaps under real load.

  6. 6

    Move the write path. Transfer ownership of the entity to the new system, switch the legacy path to read, and announce a specific window during which a rollback is still cheap.

  7. 7

    Delete the legacy implementation and its routing rule. Until this happens, the slice is not finished and both systems still need maintaining.

How long coexistence should last

Per slice, weeks. For the program, as long as it takes, but with a declining number of slices left and a visible burn-down. The failure mode is coexistence that becomes the architecture. It happens quietly: the remaining capabilities are the ugliest, the new platform is good enough that the pressure comes off, and nobody wants to touch the legacy billing code. Two years later the organization pays for two systems, two skill sets, and a routing layer, and new engineers are taught the seam as if it were a design decision.

Prevent it structurally rather than with resolve. Give every slice a target date for legacy deletion at the point you start it, not at the point you finish. Report progress as legacy code and routes removed, not as new features shipped. Sequence the ugliest capability third or fourth, while the program still has attention and budget, instead of leaving it for the end where it will simply never be done.

How you know a slice is done

Shipping the new screen is not the finish line, and neither is moving the traffic. A slice is complete when the old path is gone.

  • The legacy implementation and its routing rule have been deleted, not disabled behind a flag
  • Any temporary sync, dual write, or translation added for this slice has been removed
  • The reconciliation job for these entities reports zero differences and has been retired
  • Support documentation and runbooks refer to one system, not two
  • No open ticket says "only in the old system" for this capability

That list is deliberately unforgiving, because every item left undone is a cost your team carries for years. Incremental migration works because each step is small and reversible, and because the program optimizes for a low blast radius over speed. It stops working the moment the intermediate state is allowed to become permanent.

Common questions

What is the strangler fig pattern?
It is an incremental migration approach where a new system is built alongside an existing one and takes over its capabilities a slice at a time, with a routing layer directing each request to whichever system currently owns that capability. The old system shrinks until it serves nothing and can be deleted. The name comes from a fig that grows around a host tree and eventually stands on its own.
Do we need a shared database during coexistence?
Not necessarily, but it is the most common arrangement when both systems run against the same datastore, and it is the least risky place to start because there is no sync path to get wrong. The cost is that the new system's schema changes must stay backward compatible with the legacy application for the entire program. That constraint slows the new domain model down, and you should plan for it rather than discover it.
Is dual writing a safe way to keep the two systems consistent?
It is workable for short overlaps and dangerous as a long-term arrangement. Without a distributed transaction, a partial failure leaves the two stores disagreeing and nothing to unwind it. If you use dual writes, make them idempotent, key every write so retries cannot duplicate records, and run a reconciliation job that surfaces drift to a person.
How do we stop the routing facade from turning into its own system?
Give it a named owner, a test suite, and an explicit rule that it contains routing logic only. Any data mapping, caching, or per-tenant conditional belongs inside the new system where it is owned and tested. The check is simple: if the facade cannot be deleted on the day the last slice moves, it has become a third system.
When is a big-bang cutover the better choice?
When the system is small, the user population is contained, the behavior is well understood, and a planned outage is genuinely acceptable. In that situation a weekend migration is cheaper than months of dual maintenance and sync infrastructure. Coexistence earns its cost when downtime is not an option, the legacy behavior is only partly documented, or you need real usage to validate the new design.
How do we report progress on a migration like this?
Measure legacy code paths and routes removed, not new features shipped. New features can be delivered for a year while the old system stays fully intact, which feels like progress and is not. A burn-down of remaining legacy capabilities, each with a target deletion date, is the only view that shows whether the program is actually ending.

Want this applied to your operation?

The first conversation is a no-cost fit discussion about the problem, its importance, and the people involved. We respond within one business day.

Start a conversation