Legacy modernization fails when it is framed as replacing an application rather than changing how a business capability is delivered. A complete rewrite asks a new system to rediscover years of behavior before returning any value. A broad microservices program changes deployment topology while leaving the hardest questions, such as data ownership and behavioral compatibility, unanswered. Both approaches create a long interval in which risk rises faster than learning.

The Strangler Fig pattern takes a narrower path. Put a controllable interception point in front of the legacy system, move one coherent capability behind that point, compare its behavior with the old path, and then redirect traffic gradually. Repeat until the old implementation has no remaining responsibilities. The unit of progress is not a service count or a percentage of translated code. It is an end-to-end business capability whose requests, rules, data, operations, and ownership have moved together.

This article develops that method around a billing application. The destination could be a better-structured monolith, a managed platform, or a small number of services. Strangling determines how responsibility moves safely; it does not prescribe how many processes should exist afterward.

Define the Capability Before the Replacement

A capability is a business outcome with a recognizable interface and owner: calculate an invoice, change a shipping address, apply a credit, or produce a compliance export. It is not an arbitrary technical layer such as “all database access” or “the validation package.” Technical layers cut across many workflows, so moving one tends to require synchronized changes everywhere.

Start by drawing the current request path and naming the observable contract. For invoice delivery, the contract may include generating a PDF, selecting a locale, recording a delivery attempt, sending an email, and exposing status to support staff. Include scheduled jobs and operator actions, not only HTTP endpoints. A capability that appears to be one controller may also be invoked by a nightly batch, a message consumer, and a manual repair script.

For each candidate slice, record:

Question Evidence to collect
Who initiates it? Routes, jobs, events, command-line tools, and operator procedures
What must remain compatible? Inputs, outputs, side effects, timing assumptions, and error semantics
Which rules change together? Policy ownership and release history
Which data does it read and write? Tables, files, caches, reports, and downstream exports
How will traffic be selected? Tenant, account, request type, region, or deterministic percentage
How can it return to the old path? Routing switch, data prerequisites, and recovery time

A good first slice has meaningful value but limited coupling. It is exercised often enough to produce evidence, yet its failure is reversible. Read-only capabilities are tempting because rollback is easy, but they teach little about moving ownership of writes. A bounded write workflow with a narrow audience is often more informative.

The first deliverable is a capability map, not replacement code. If the team cannot state where the slice begins, which outcomes it owns, and what proves equivalence, implementation is premature.

Build an Interception Point You Can Control

The interception point decides whether a request reaches the legacy implementation or the replacement. It can be an edge proxy, an API gateway route, an application facade, a message router, or a scheduler that chooses between job handlers. Choose the closest stable boundary that sees enough identity and request context to make a deterministic decision.

flowchart LR C[Client or job] --> R[Capability router] R -->|legacy cohort| L[Legacy implementation] R -->|migration cohort| N[New implementation] L --> D[(Current data)] N --> O[(Owned data)] L -. normalized outcome .-> M[Comparison and telemetry] N -. normalized outcome .-> M R -. configuration .-> K[Migration control]

Routing must be stable. If the same account alternates between implementations on successive requests, caches, workflow state, and user expectations can split. Hashing a tenant or account identifier into a cohort is safer than random selection. Persist an explicit assignment when a workflow spans multiple requests. Never route solely by an unauthenticated header that callers can forge.

Treat the router as production infrastructure. It needs versioned configuration, audit history, bounded evaluation time, metrics for each decision, and a default when configuration is unavailable. The safe default depends on migration phase: early on it is usually legacy; after ownership has moved and the old path has stale data, falling back may be dangerous. “Rollback” is therefore a planned state transition, not an unconditional catch block.

Avoid teaching every caller about both systems. A shared facade can normalize identifiers, errors, and response shapes while migration proceeds. Keep it thin: if business rules accumulate in the interception layer, the temporary bridge becomes a third implementation that must later be removed.

Baseline the legacy path before redirecting anything. Record outcome counts, latency distributions, error classes, side-effect volumes, and representative payloads with sensitive fields removed. Without a baseline, differences discovered during rollout have no reference point.

Select Slices by Dependency, Not Code Size

Migration order is a dependency problem. A small endpoint that writes shared tables used by twenty workflows may be riskier than a larger capability with isolated storage. Score candidates using business value, coupling, reversibility, testability, operational ownership, and data complexity. The score is a decision aid, not arithmetic that overrides judgment.

Prefer a vertical slice that includes its entry point, domain decisions, persistence changes, and operational controls. Moving only the user interface leaves all risk in the legacy backend. Moving only computation while the old application remains the sole writer can be useful as a temporary stage, but it is not completed ownership.

Map dependencies in three groups:

  1. Required inputs: customer identity, tax policy, product price, or another capability’s published result.
  2. Owned state: records whose invariants and write policy move with this capability.
  3. Published outputs: events, APIs, documents, and reporting data consumed elsewhere.

Replace hidden table sharing with explicit contracts only where the slice needs it. Do not begin a platform-wide contract campaign. A stable query API, replicated read model, or published event can decouple one dependency without forcing every neighboring module to migrate at once.

Deployment shape follows operational need. Two capabilities may become modules in one deployable because they share scaling and release characteristics. Another may run separately because it handles untrusted documents or requires independent capacity. The Strangler Fig pattern remains the same in either case: redirect responsibility through a controlled boundary and retire the old path after evidence is sufficient.

Work Through an Invoice-Delivery Slice

Assume a legacy billing application generates invoice PDFs and emails them in one transaction-like request. Failures leave ambiguous states: support cannot tell whether rendering failed, email was rejected, or the request timed out after delivery. The modernization goal is reliable invoice delivery with visible status, not “extract the PDF service.”

First define the replacement contract. requestDelivery(invoiceId, recipient, locale) accepts a stable invoice version and an idempotency key. It returns a delivery identifier. Status progresses through accepted, rendered, sent, or failed, and retries cannot send the same invoice twice for the same key. The legacy UI can call this contract through an adapter, so replacing the UI is not a prerequisite.

The first cohort contains internal test accounts. For them, the router sends delivery requests to the new handler. The handler reads an immutable invoice snapshot from the legacy database through a narrow read adapter, writes its own delivery state, renders the document, and calls the mail provider. It emits a normalized outcome used by the existing support screen.

Before sending real mail, run the new path in a shadow mode. It renders the PDF and computes a canonical comparison record containing invoice number, currency, line totals, tax totals, locale, page count, and a hash of normalized text. Byte-for-byte PDF comparison would be brittle because metadata and compression can differ. Business-level fields reveal material differences without pretending two renderers must produce identical files.

Suppose shadow comparison finds that credits created before a historical cutoff use a rounding rule absent from current documentation. That is useful migration evidence. The team can encode the compatibility rule, explicitly reject the old edge case, or repair affected data. Quietly widening the rollout would turn discovery into customer impact.

After comparisons pass, enable real delivery for the internal cohort, then selected low-risk tenants, then broader cohorts. At each stage, the routing assignment, idempotency record, and invoice snapshot version are stored together. If email provider failures rise, operators can pause new assignments while allowing accepted work to finish. Requests not yet accepted can return to legacy; accepted requests remain on the new path because switching mid-workflow would risk duplicate delivery.

Completion means more than all HTTP traffic using the new handler. The nightly resend job, support repair action, audit export, alerts, and runbook must also use the new delivery record. Only then has the capability moved.

Transfer Data Ownership Deliberately

Data is usually the limiting dependency. Four transition patterns are common, and each makes a different consistency promise.

Legacy-owned reads let the replacement read current tables through a constrained adapter. This is useful early, but schema coupling remains. The adapter should expose business-shaped values and absorb legacy column quirks rather than leaking raw tables into new code.

Change capture or events build a replacement-owned read model. They reduce query coupling and can support scale, but consumers must tolerate lag, duplicates, and reordered delivery. Reconciliation is mandatory because a stream is not proof that every historical row arrived correctly.

Backfill plus single-writer cutover copies existing state, verifies it, pauses or fences writes briefly, applies the final delta, and changes the authoritative writer. This gives a crisp ownership moment when a bounded interruption or write queue is acceptable.

Dual writing updates both stores during transition. It appears flexible but creates partial-success cases: one write succeeds and the other fails, retries reorder updates, or an old process writes only one side. If dual writing is unavoidable, designate one authoritative system, use durable outbox records, make consumers idempotent, and continuously reconcile. Never infer consistency from a low error count.

Write down authority for every migration phase. “Both databases are primary” is not a state; it is an unresolved conflict policy. Also specify whether rollback requires reverse replication. Once the new implementation accepts writes that legacy cannot understand, routing back without copying or transforming those writes can corrupt user-visible state.

Reports and administrative tools deserve special attention. They often query tables directly and can keep a legacy schema alive after request traffic moves. Replace those dependencies with exported views or capability APIs, assign owners, and include them in retirement criteria.

Make Observation and Rollback Part of the Design

Every request needs a correlation identifier that survives the router, both implementations, downstream calls, and asynchronous work. Emit the selected route and migration cohort as low-cardinality attributes. Compare business outcomes such as invoices delivered, totals rendered, duplicate attempts prevented, and support-visible status, not only CPU and HTTP status.

Use three rollout modes:

  • Shadow: execute a side-effect-free candidate path and compare normalized outputs.
  • Canary: give the replacement authority for a small, stable cohort.
  • Ramp: expand by an explicit schedule while holding at each stage long enough to exercise relevant cycles.

Define stop conditions before rollout: an unexpected difference in monetary fields, an elevated terminal-failure rate, reconciliation lag beyond its bound, or loss of trace coverage. Threshold values must come from the system’s service objectives and baseline, not from invented universal percentages.

Rollback has layers. A routing rollback sends eligible new requests to legacy. A deployment rollback restores replacement code while preserving its data contract. A business rollback may compensate already completed effects. Test each layer. For asynchronous workflows, draining or fencing in-flight work is often more important than flipping the route quickly.

Keep migration configuration separate from ordinary feature flags. Record who changed a cohort, why, and against which readiness evidence. Expire temporary routes and adapters through explicit review dates; otherwise the migration layer becomes permanent architecture by neglect.

Recognize Failure Modes and Costs

The pattern reduces change risk but adds a period of duplication. Teams operate two implementations, bridge contracts, reconcile data, and investigate differences. That cost is justified only when slices finish. Too many simultaneous slices produce a distributed half-migration in which nobody knows which system owns what.

Common failure modes include:

  • Routing by endpoint instead of capability. One workflow crosses both systems and violates invariants.
  • Leaving writes shared indefinitely. Schema changes require coordination forever, so ownership never moves.
  • Calling a rewrite a strangler. The replacement is built behind closed doors, and traffic moves only at the end.
  • Measuring request success only. Silent differences in side effects, reports, or monetary values escape detection.
  • Assuming old is always a safe fallback. New-only state makes a late rollback destructive.
  • Preserving every historical accident. Compatibility work consumes the program without distinguishing contractual behavior from obsolete defects.
  • Migrating by organizational fashion. Teams create many network services even when a modular deployable would meet the target qualities with less operational burden.

There are situations where strangling is a poor fit. If the legacy platform cannot be intercepted, cannot run in the target environment, or presents an immediate security risk, containment and replacement may need a shorter path. A tiny application with well-understood behavior may be cheaper to replace directly. Conversely, a stable system with low change demand may not justify modernization at all. The decision should compare future change cost and risk, not reward architectural activity for its own sake.

Verify Completion and Retire the Old Path

Testing proceeds from contract to operations. Build characterization tests from observed legacy behavior, but review them so accidental bugs do not silently become requirements. Run the same contract suite against both implementations. Add property tests for invariants such as preserved invoice totals and at-most-one delivery per idempotency key. Use fault injection to exercise provider timeouts, duplicate messages, delayed change events, and partial storage failures.

Rehearse the migration sequence in an environment with production-shaped data relationships. Verify backfill counts and aggregates, then sample records by risk category rather than only at random. Run reconciliation in production from the first shadow cohort onward. A useful reconciler reports missing keys, divergent versions, impossible state transitions, and age of the oldest unresolved difference.

Operational readiness includes dashboards split by route, alerts that name the owning team, capacity tests for the new path, a tested pause mechanism, and runbooks for in-flight work. Support and compliance users should validate their workflows because request-level tests will not reveal a broken audit export.

Retirement requires positive evidence:

  1. Every known entry point routes to the replacement.
  2. The replacement is authoritative for owned writes and historical data is verified.
  3. Downstream consumers no longer read legacy-owned structures for this capability.
  4. The rollback window has closed by an explicit decision, with required records archived.
  5. Legacy jobs, credentials, alerts, routes, code, and infrastructure are removed.

Watch for traffic and data access after disabling the old path before deleting it. A dormant quarterly job may be invisible during a short rollout. Once the observation period covers relevant business cycles, delete the bridge rather than retaining it “just in case.”

Strangler modernization succeeds through completed transfers of responsibility. A controllable route creates room to learn; capability slices keep that learning tied to business outcomes; explicit data authority prevents permanent ambiguity; and retirement turns temporary complexity into a simpler system. The result need not be a fleet of services. It needs to be a system in which each migrated capability has a clear contract, owner, operational story, and no hidden dependence on the application it replaced.