Skip to main content
🏗️Architecture Patterns

🌿 Strangler Pattern — Incrementally Migrate from Monolith to Microservices Without the Big-Bang Risk

The Strangler Fig Pattern is one of the most reliable strategies for modernizing legacy systems. Named after the strangler fig tree — a tropical plant that...

📖 12 min read

🌿 Strangler Pattern — Incrementally Migrate from Monolith to Microservices Without the Big-Bang Risk

The Strangler Fig Pattern is one of the most reliable strategies for modernizing legacy systems. Named after the strangler fig tree — a tropical plant that grows around an existing tree, gradually replacing it until the host tree disappears — this pattern lets you incrementally migrate a monolith into a modern architecture without ever performing a risky big-bang rewrite. In this guide, we cover the full lifecycle: routing strategies, feature toggles, real migration examples, and everything you need to execute this pattern with confidence.


🌳 What Is the Strangler Fig Pattern?

Martin Fowler coined the term Strangler Fig Application in 2004, inspired by the strangler fig trees he observed in Australia. Just as the fig slowly wraps around its host tree and eventually stands on its own, the strangler pattern lets you build a new system around the edges of the old one, progressively replacing functionality until the legacy system can be decommissioned.

The core idea is simple: instead of replacing the entire monolith at once, you intercept requests at the boundary and route them to either the old or new system. Over time, more and more routes point to the new system. Eventually, nothing points to the old one, and you can safely shut it down.

This approach is fundamentally different from a rewrite. You are never in a state where the system does not work. At every step, the application is fully functional — some parts run on the old system, some on the new, and users cannot tell the difference.


💥 Why Big-Bang Rewrites Fail

Before diving deeper into the strangler pattern, it is worth understanding why the alternative — a complete rewrite — so often fails:

  • Feature parity takes forever: The old system has years of accumulated business logic, edge cases, and bug fixes baked in. Replicating all of it takes far longer than anyone estimates.
  • Moving target: While the new system is being built, the old system keeps evolving. You are essentially chasing a moving goalpost.
  • All-or-nothing risk: A big-bang cutover means if anything goes wrong, everything goes wrong. Rollback is often impractical or impossible.
  • Team morale and stakeholder fatigue: Long rewrite projects with no visible progress erode trust and motivation.
  • Second-system effect: Teams tend to over-engineer the replacement, adding complexity that was never needed.

The strangler pattern avoids every one of these problems by delivering value incrementally and maintaining a working system at all times.


🔄 The Three Phases: Transform, Coexist, Eliminate

The strangler pattern follows three distinct phases for each piece of functionality you migrate:

Phase Description Key Activities
Transform Build the replacement component in the new architecture Design new service, implement business logic, write tests, set up data migration
Coexist Run old and new side-by-side, gradually shifting traffic Configure routing layer, enable feature toggles, monitor both systems, compare outputs
Eliminate Remove the old component once the new one is fully proven Remove old code, clean up routing rules, decommission old infrastructure

You repeat this cycle for each bounded context or feature area. The order in which you migrate features matters — start with the ones that are most independent and have the clearest boundaries.


🚦 The Routing Layer — Your Migration Backbone

The routing layer is the single most critical piece of infrastructure in a strangler migration. It sits between clients and your backend systems, deciding which requests go to the legacy monolith and which go to the new services. Common choices include an API gateway, a reverse proxy like Nginx, or a service mesh.

Here is a practical Nginx configuration that routes specific paths to a new microservice while keeping everything else on the monolith:

upstream legacy_monolith {
    server monolith.internal:8080;
}

upstream new_order_service {
    server orders-v2.internal:3000;
}

upstream new_inventory_service {
    server inventory-v2.internal:3001;
}

server {
    listen 80;
    server_name api.example.com;

    # Migrated: Orders now handled by new service
    location /api/v1/orders {
        proxy_pass http://new_order_service;
        proxy_set_header X-Migration-Phase "strangler-coexist";
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
    }

    # Migrated: Inventory now handled by new service
    location /api/v1/inventory {
        proxy_pass http://new_inventory_service;
        proxy_set_header X-Migration-Phase "strangler-coexist";
        proxy_set_header Host $host;
    }

    # Everything else still goes to the monolith
    location / {
        proxy_pass http://legacy_monolith;
        proxy_set_header Host $host;
    }
}

Notice the X-Migration-Phase header. This is a useful convention for observability — your monitoring systems can track how much traffic is flowing to new versus legacy systems.

For more on API gateway patterns and routing strategies, see API Gateway Patterns on SWEHelper.


🎛️ Feature Toggles for Gradual Rollout

Routing at the infrastructure level handles path-based migration, but feature toggles give you finer-grained control. They let you enable the new implementation for specific users, regions, or percentages of traffic — essential for safe rollouts.

Here is a feature toggle implementation that supports percentage-based rollout and user-level overrides:

class StranglerFeatureToggle:
    def __init__(self, config_store):
        self.config = config_store

    def should_use_new_service(self, feature_name, user_context):
        toggle = self.config.get_toggle(feature_name)

        if not toggle or not toggle.get("enabled"):
            return False

        # Check user-level overrides first
        user_id = user_context.get("user_id")
        if user_id in toggle.get("force_new", []):
            return True
        if user_id in toggle.get("force_legacy", []):
            return False

        # Check region-based rollout
        region = user_context.get("region")
        enabled_regions = toggle.get("enabled_regions", [])
        if enabled_regions and region not in enabled_regions:
            return False

        # Percentage-based rollout using consistent hashing
        rollout_pct = toggle.get("rollout_percentage", 0)
        user_hash = hash(f"{feature_name}:{user_id}") % 100
        return user_hash < rollout_pct


# Usage in your application layer
toggle = StranglerFeatureToggle(config_store)

def get_order(order_id, user_context):
    if toggle.should_use_new_service("orders_v2", user_context):
        return new_order_service.get(order_id)
    else:
        return legacy_monolith.get_order(order_id)

The consistent hashing approach is important: it ensures that a given user always gets the same experience (new or legacy) across requests, avoiding confusing inconsistencies. As you increase the rollout_percentage, more users are smoothly transitioned to the new service.

For a deeper dive into toggle strategies and their operational implications, check out Feature Toggle Best Practices and the Feature Flag Rollout Calculator on SWEHelper.


📋 Migration Checklist

Use this checklist before, during, and after migrating each feature:

  • Before migration: Identify the bounded context, map all dependencies (database tables, shared libraries, message queues), define the API contract for the new service, set up dual-write or data sync strategy.
  • Routing setup: Configure the proxy or API gateway, verify routing rules in a staging environment, add observability headers.
  • Feature toggle setup: Create toggle configuration, test with internal users first, set initial rollout percentage to 0%.
  • Coexistence testing: Run shadow traffic (mirror requests to both old and new, compare responses), validate data consistency, monitor error rates and latency on the new service.
  • Gradual rollout: Increase traffic in stages (1% → 5% → 25% → 50% → 100%), monitor each stage for at least 24-48 hours, have a rollback plan at every stage.
  • Elimination: Remove legacy code paths, clean up toggle configurations, decommission old database tables (after a grace period), update documentation.

🏗️ Real Migration Example: E-Commerce Order System

Let us walk through a concrete example. Imagine a monolithic e-commerce platform where you want to extract the Order Management functionality into a standalone microservice.

Step 1: Analyze and Isolate

Map the order module's dependencies within the monolith. In our example, Orders depend on Users, Products, and Payments. It writes to the orders, order_items, and order_events tables. It publishes events to an internal event bus.

Step 2: Define the Seam

Create a clear API boundary. All order-related endpoints (/api/v1/orders/*) become the seam. Internal method calls from other modules to order logic need to be converted to API calls or event-driven interactions.

Step 3: Build the New Service

Implement the new Order Service with its own database, replicating the schema and business logic. Set up a data synchronization mechanism — during coexistence, both the monolith and the new service need consistent data. A common approach is Change Data Capture (CDC) using tools like Debezium.

Step 4: Deploy the Routing Layer

Configure Nginx (as shown earlier) to route /api/v1/orders to the new service. Initially, use feature toggles so that only internal testers hit the new service.

Step 5: Shadow Testing

Mirror production traffic to the new service without serving its responses to users. Compare the new service's output against the monolith's output. Fix discrepancies. This phase typically reveals edge cases the team missed.

Step 6: Gradual Rollout

Increase the feature toggle rollout percentage: 1% of users, then 5%, then 25%, then 50%, then 100%. At each stage, monitor latency (p50, p95, p99), error rates, and data consistency. Keep this process going for at least one full business cycle at 100% before proceeding to elimination.

Step 7: Eliminate the Legacy Code

Once the new Order Service has been handling 100% of traffic for a sufficient period (typically 2-4 weeks), remove the order-related code from the monolith. Clean up database tables, remove feature toggles, and simplify the routing configuration.

For more on decomposing monoliths, see Microservices Decomposition Strategies on SWEHelper.


✅ Pros and Cons

Pros Cons
Zero-downtime migration — system always works Increased operational complexity during coexistence
Incremental delivery of value to stakeholders Data synchronization between old and new can be challenging
Easy rollback at any stage Requires a disciplined approach to routing and toggles
Reduced risk compared to big-bang rewrite Longer overall migration timeline
Team can learn and adapt as they go Temptation to leave legacy code running indefinitely
Validates new architecture under real production load Needs investment in routing infrastructure and observability

⚠️ Common Pitfalls

  • Never reaching the "Eliminate" phase: This is the most common failure mode. Teams migrate traffic to the new service but never clean up the old code. Set hard deadlines for elimination.
  • Shared database trap: If the new service reads directly from the monolith's database, you have not truly decoupled them. Use APIs, events, or CDC — not shared database access.
  • Ignoring data consistency: During coexistence, both systems may write to overlapping data. Plan your data ownership strategy carefully.
  • Migrating too much at once: The strangler pattern works because changes are small. If you try to migrate five modules simultaneously, you lose the incremental benefit.
  • Insufficient monitoring: You need to compare the behavior of old and new systems in real time. Without proper observability, you are flying blind.
  • Neglecting the routing layer: The proxy or gateway is a single point of failure. Make it highly available and ensure the team understands its configuration.

🔧 Integration with CI/CD

The strangler pattern works best when paired with a solid CI/CD pipeline. Here is how they integrate:

  • Separate pipelines: The new service should have its own CI/CD pipeline, independent of the monolith. This lets teams deploy the new service at their own cadence.
  • Toggle configuration as code: Store feature toggle settings in version control. Changes to rollout percentages should go through code review, just like code changes.
  • Automated routing validation: Include integration tests in your pipeline that verify the routing layer sends requests to the correct backend based on the current toggle state.
  • Canary deployments: Combine the strangler pattern with canary deployment strategies for even safer rollouts.
  • Automated rollback triggers: Set up alerts that automatically decrease the rollout percentage or revert routing if error rates exceed a threshold.

For CI/CD pipeline design guidance, see the CI/CD Pipeline Builder tool on SWEHelper.


🤔 When to Use the Strangler Pattern

The strangler pattern is ideal when:

  • You have a large legacy system that cannot be replaced overnight.
  • The system is actively serving users and downtime is unacceptable.
  • You can identify clear boundaries between functional areas.
  • There is an HTTP or messaging boundary where you can intercept and reroute traffic.
  • Your team needs to learn the new architecture incrementally rather than all at once.

It is less suitable when the monolith is small enough to rewrite in a few weeks, when there are no clear seams to exploit, or when the legacy system has deeply intertwined components with no discernible boundaries.


❓ Frequently Asked Questions

How long does a typical strangler migration take?

It depends on the size and complexity of the monolith. Migrating a single bounded context typically takes 4-12 weeks including the coexistence and elimination phases. A full migration of a large monolith can span 6 months to 2+ years, but you are delivering value at every step. The key advantage is that each completed migration is a standalone improvement — even if you pause the overall effort, the migrated parts remain in production.

How do I handle database migrations during the strangler process?

The recommended approach is to give the new service its own database from day one. During coexistence, use Change Data Capture (CDC) tools like Debezium to sync data from the monolith's database to the new service's database. Avoid the shared database anti-pattern — it creates tight coupling that defeats the purpose of the migration. For more on data migration strategies, see Database Migration Patterns on SWEHelper.

Can I use the strangler pattern with non-HTTP systems like message queues?

Absolutely. The routing layer does not have to be an HTTP proxy. For event-driven systems, you can use a message router or topic-based routing to direct messages to the new consumer while keeping the legacy consumer active. The principle is the same: intercept at the boundary, route selectively, and migrate incrementally.

What happens if the new service fails during coexistence?

This is exactly why the pattern is so powerful. If the new service fails, you simply route traffic back to the monolith by updating the routing configuration or toggling the feature flag. The monolith is still running and fully capable of handling the load. This rollback can happen in seconds if your routing layer is properly configured.

How do I convince stakeholders to invest in a strangler migration instead of a rewrite?

Focus on three arguments: risk reduction (no big-bang cutover), continuous delivery of value (each migrated feature is a shipped improvement), and historical evidence (point to well-documented rewrite failures like Netscape 6.0). Frame it as a series of small, low-risk projects rather than one massive bet-the-company initiative.


The strangler pattern is not glamorous — it requires patience, discipline, and strong operational practices. But it is the approach that actually works for modernizing systems that matter. Start small, migrate incrementally, and never stop eliminating the old. Your future self will thank you.

Related Articles