The Strangler Fig Pattern: The Safest Way to Leave Your Monolith Behind

by Arif Ikhsanudin, Backend Developer

Why this pattern exists and what it's named after

Martin Fowler named the Strangler Fig pattern after the strangler fig tree — a plant that grows around a host tree, gradually taking over its structure while the host tree dies inside the fig's canopy. The analogy for software: you build new service functionality that wraps the monolith at the traffic layer. Over time, as new services handle more routes, the monolith handles less, until it can be shut down entirely. The monolith is never replaced in a single migration event — it is gradually strangled.

The name is evocative but the mechanism is straightforward: put a proxy (your API gateway or a dedicated routing layer) in front of the monolith. Route some requests to new services, route the rest to the monolith. Expand the portion going to new services over time. When a route is fully migrated and the monolith path is unused, delete the monolith code for that route.

This is the safest decomposition approach because it provides incremental production validation, fine-grained rollback capability, and no big-bang cutover event.

The routing layer as the control plane

The most important architectural decision in a Strangler Fig migration is where routing lives. The routing layer is the switchboard that determines which requests go to the monolith versus new services. It needs to be:

  • Independently deployable (you can change routing without deploying either the monolith or the new services)
  • Observable (you can see how much traffic goes to each target)
  • Capable of weighted routing (send 10% to new service, 90% to monolith)

Your API gateway (Kong, NGINX, AWS API Gateway) is the natural fit if it supports weighted routing or traffic splitting. If it doesn't, a lightweight proxy service (a simple HTTP router in Go or Node.js) deployed in front of the monolith serves the purpose.

# NGINX: strangler fig routing configuration
upstream monolith {
    server monolith.internal:8080;
}

upstream order-service {
    server order-service.internal:8080;
}

server {
    # Orders endpoint: currently migrated, goes to new service
    location /api/orders {
        proxy_pass http://order-service;
    }

    # All other requests: still handled by monolith
    location / {
        proxy_pass http://monolith;
    }
}

Changing which routes go to the new service is a routing configuration change, not an application deployment. This is the key operational property: migration decisions are decoupled from both the monolith and the new service.

The migration sequence per endpoint

For each endpoint you're migrating, the sequence should be:

1. Build the new service endpoint — implement the same behavior in the new service. Test it with unit tests and integration tests. Don't route production traffic yet.

2. Shadow mode — route production traffic to both the monolith (authoritative) and the new service (shadow). Compare results. Fix discrepancies. Run this until discrepancy rate is negligible.

3. Canary — route 1–5% of production traffic to the new service authoritatively. Monitor error rates and latency against the monolith baseline. Fix any issues that only appear under production conditions.

4. Gradual rollout — increase traffic weight: 10%, 25%, 50%, 100%. Validate at each step. The increment size and dwell time depend on your risk tolerance and the endpoint's criticality.

5. Decommission the monolith path — once 100% of traffic routes to the new service and you've operated it successfully for a sufficient stabilization period (typically two to four weeks), remove the monolith code for that endpoint. Update the routing configuration to remove the monolith path.

# Simple shadow comparison (can be added to monolith temporarily)
def create_order_with_shadow(request):
    # Authoritative: monolith result
    monolith_result = monolith_create_order(request)
    
    # Shadow: compare without affecting user
    try:
        service_result = order_service_client.create_order(request)
        if not results_equivalent(monolith_result, service_result):
            logger.warning(
                "Shadow divergence for order %s: monolith=%s, service=%s",
                request.order_id, monolith_result, service_result
            )
    except Exception as e:
        logger.warning("Shadow call failed for order %s: %s", request.order_id, e)
    
    return monolith_result

Handling shared state: the hardest part

The Strangler Fig pattern handles routing cleanly. Data is harder. The new service needs its own data, but during the migration period, the monolith's database is authoritative.

Two patterns for managing shared state during migration:

Anti-corruption layer: the new service calls the monolith's API (not the database directly) for data it doesn't yet own. This keeps the monolith as the source of truth during migration. The new service gradually takes over data ownership as migration progresses.

Dual write with event sourcing: when the monolith writes data, it also publishes an event. The new service consumes the event and maintains its own data store. During migration, the service can serve reads from its own store while writes still go through the monolith. When ready, flip write ownership to the service.

Neither is simple. Both are safer than a one-time data migration at cutover time.

Knowing when you're done

The Strangler Fig migration is complete for a given bounded context when:

  • 100% of traffic routes to the new service
  • The monolith code for that context has been deleted (not just disabled)
  • The new service owns its own data store with no dependency on the monolith's database
  • The team has operated the new service independently through at least one incident

"Deleted" is load-bearing. Disabled code comes back. Unused monolith paths accumulate. Code that is actually deleted cannot drift back into use. When the migration sequence ends in deletion, you know it's genuinely complete — not just phase-shifted to a later date.

The strangler fig eventually becomes the only tree standing. That outcome is achievable only through the incremental discipline of migrating one endpoint, one route, one bounded context at a time.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Spring Data Repository Design — When findBy Methods Are Enough and When They're Not

Spring Data's derived query methods eliminate boilerplate for simple queries. They become unreadable for complex ones and break entirely for dynamic filtering. Here is where each approach belongs and how to recognize when you've outgrown derived queries.

Read more

Why Clients Hire Contractors and What They Are Actually Looking For

Clients do not hire contractors because they ran out of full-time employees. They hire contractors to solve a specific problem fast — and understanding that changes how you pitch yourself.

Read more

The Hidden Cost of Treating Remote Developers as Less Valuable

Remote work can be a huge advantage for companies and developers alike. But undervaluing remote developers carries hidden costs that often outweigh any short-term savings.

Read more

How to Keep a “Lessons Learned” Notebook

Ever finish a project and realize you forgot all the small mistakes and smart hacks you discovered? A “lessons learned” notebook can turn fleeting experiences into a goldmine of knowledge for the next project.

Read more