Guide contents

Lesson 56 of 129

Migrating to Microservices with the Strangler Fig Pattern

Rewriting a monolith as a set of services in one step is risky: it takes long, delivers no value until finished, and the requirements move while it runs. The alternative is incremental migration, in which functionality is moved out of the monolith piece by piece while the system stays in production. The main patterns are the strangler fig, the anti-corruption layer, and branch by abstraction.

Strangler fig pattern

The strangler fig pattern places a routing layer (a proxy, an API gateway, or a load balancer) in front of the monolith. Initially it forwards all traffic to the monolith. As each capability is rebuilt as a service, the router is changed to send that capability's requests to the new service. The monolith shrinks until it can be removed.

flowchart LR
    C["Clients"] --> R["Routing layer (gateway)"]
    R -->|"/orders/*"| N["New Orders service"]
    R -->|"/catalog/*, /billing/*, others"| M["Legacy monolith"]
    N --> NDB[("Orders DB")]
    M --> MDB[("Monolith DB")]

Steps for one capability:

  1. Identify a capability with a clear boundary (see decomposition by business capability).
  2. Introduce the router if not present. All traffic still goes to the monolith.
  3. Build the new service implementing the same behavior.
  4. Verify the new service. Options: shadow traffic (copy requests to the new service and compare responses without returning them), or run both and compare. Shadowing is safe for reads. For writes, the shadow copy must not repeat side effects such as charging a card or sending an email, so it needs a stubbed or isolated backend.
  5. Shift traffic gradually, by percentage or by user segment, using the router. Keep the ability to route back.
  6. Remove the old code from the monolith when the new service handles all traffic and has been stable.

Properties:

  • Each step is small and reversible.
  • The system is deployable at every stage.
  • Value is delivered incrementally.
  • The router is a component that must be highly available and observable.

Anti-corruption layer

The legacy model often differs from the model wanted in the new service: different names, different structures, different rules. If the new service uses the legacy model directly, the legacy design leaks into it.

An anti-corruption layer (ACL) is a translation component between the two. It converts legacy requests, data, and events into the new service's domain model and back. The new service depends only on its own model and the ACL.

Where it sits:

  • In front of the legacy system, when the new service calls the monolith (for example, to read customer data that has not yet been migrated).
  • In front of the new service, when legacy code calls it and expects the old interface.
  • Between the legacy database and the new service when data is synchronized (for example, a translation step in an event consumer).

The ACL is temporary. It is removed when the legacy dependency disappears.

Branch by abstraction

Some functionality is called from inside the monolith, so a router at the network edge cannot intercept it. Branch by abstraction handles this inside the codebase:

  1. Create an abstraction (an interface) over the existing implementation, and change all callers to use it.
  2. Implement the interface a second time as a client of the new service.
  3. Switch between implementations with a configuration or feature flag (see feature flags), first for a subset of traffic.
  4. When the new implementation is verified, delete the old implementation and, if desired, the interface.
flowchart TD
    Caller["Monolith code"] --> I["PaymentGateway interface"]
    I -->|"flag off"| Old["Legacy in-process implementation"]
    I -->|"flag on"| New["Client of new Payments service"]

The monolith continues to build and release throughout, so the change can be merged in small steps to the main branch instead of living on a long-running branch.

Data migration

Data is the hardest part, since the goal is that each service owns its data (see database per service). Typical sequence:

  1. Identify the tables owned by the capability. Find every other reader and writer, including joins, reports, and stored procedures.
  2. Encapsulate access in the monolith: route all access to those tables through one module, and change other modules to use its interface instead of joining tables.
  3. Create the new datastore and start synchronizing:
    • Copy existing data in a backfill.
    • Keep it current by change data capture or dual writes done carefully. Dual writes without a transactional mechanism can diverge.
  4. Read from the new store for the migrated capability, while writes still go to the old one (or the reverse), comparing results.
  5. Switch writes so the new store is the source of truth. Keep the old store updated for rollback until stable.
  6. Remove the old tables and the synchronization.

Foreign keys between migrated and remaining tables must be dropped and replaced by identifiers plus API calls or replicated data. Queries that joined them become cross-service queries.

Order of extraction

Candidate Reasoning
Capabilities with clear boundaries and few dependencies Lower risk; establishes the pipeline and platform
Capabilities that change often Independent release pays off soonest
Capabilities with distinct scaling or resource needs Gains from separate scaling
Capabilities that must be isolated (security, compliance) Reduced audit scope or blast radius
Capabilities with low coupling to the monolith's database Data migration is simpler

A common first extraction is a peripheral capability such as notifications or report generation, to validate tooling before tackling the core domain. Core, highly coupled parts (for example, order processing that touches many tables) are extracted later, after the boundaries are understood and the platform is proven.

Risks

  • Two systems to run during the transition; the migration can stall halfway, leaving both.
  • Data divergence between old and new stores during synchronization.
  • Router failure affects all traffic.
  • Behavior differences between old and new implementations, found late without comparison testing.
  • Wrong boundaries, discovered after the extraction. Keeping extractions small limits the cost.

Interview questions

  • Why is the strangler fig pattern preferred over a rewrite? It moves functionality in small, reversible steps, keeps the system in production throughout, and delivers value incrementally instead of at the end of a long project.
  • What does an anti-corruption layer do? It translates between the legacy model and the new service's model so that the legacy design does not leak into the new service. It is removed when the legacy dependency ends.
  • When is branch by abstraction used instead of a routing layer? When the code to be replaced is called from inside the monolith, so a network-level router cannot intercept the calls. An interface with two implementations and a flag allows switching in-process.
  • How is data moved to a new service safely? Encapsulate table access, backfill the new store, keep it in sync (for example with change data capture), compare reads, switch writes, then retire the old tables. Rollback stays possible until the end.