System Design

Strangler Fig Pattern: Safely Migrating Monoliths

Learn how the Strangler Fig Pattern enables safe, incremental migration from monolithic architectures to microservices. A senior engineer's guide with examples.

Khader Vali June 19, 2026 19 min read

The Strangler Fig Pattern: Safely Migrating Monoliths to Microservices

In the fast-paced world of software development, monolithic applications often become the legacy that holds innovation back. While once efficient, they can evolve into complex, tightly coupled systems that are difficult to scale, maintain, and deploy. The allure of microservices—with their promise of independent scalability, technological flexibility, and enhanced resilience—is strong. Yet, the journey from a monolithic behemoth to a fleet of nimble microservices is fraught with peril. A “big bang” rewrite is almost always a recipe for disaster, risking project failure, budget overruns, and prolonged downtime.

This is where the Strangler Fig Pattern emerges as a strategic, incremental, and significantly safer approach. As a senior software engineer and technical writer, I’ve seen firsthand how this pattern empowers teams to transform their architecture without bringing their entire business to a halt. It’s not just a technical pattern; it’s a methodology for managing risk and fostering continuous delivery during a complex migration.

Join me as we delve deep into the Strangler Fig Pattern, exploring its origins, its application, architectural considerations, and real-world scenarios to guide you through your own migration journey. We’ll examine how to decompose a monolith gracefully, piece by piece, ensuring that your business operations remain uninterrupted and your path to a modern, scalable architecture is secure.

Understanding the Strangler Fig Pattern

The Strangler Fig Pattern, coined by Martin Fowler, draws its name and inspiration from a fascinating phenomenon in tropical rainforest ecosystems. Understanding this natural analogy is key to grasping the pattern’s elegance and effectiveness in software architecture.

The Analogy Explained

Imagine a strangler fig seed germinating high in the canopy of a host tree. Initially, it’s just a small plant, dependent on its host. As it grows, it sends roots down the trunk of the host tree, eventually reaching the ground. Over time, these roots thicken and intertwine, forming a robust, lattice-like structure that completely envelops the host tree’s trunk. The fig tree’s canopy expands, outcompeting the host for sunlight. Eventually, the host tree, deprived of light and nutrients, withers and dies, leaving behind the hollow, but strong, framework of the strangler fig where the host once stood.

In the context of software, the “host tree” is your existing monolithic application. The “strangler fig” represents new microservices and components that you build around the monolith. Instead of tearing down the old system and rebuilding from scratch, you incrementally develop new functionality as separate services. These new services gradually take over specific responsibilities previously handled by the monolith. A facade or API Gateway acts as the “roots and canopy,” intercepting requests and routing them either to the new microservice or, if the functionality hasn’t been migrated yet, to the corresponding part of the monolith.

Over time, as more and more functionality is extracted and rebuilt as independent microservices, the monolith shrinks. Eventually, the entire functionality of the old monolith is replaced by the new services, and the legacy monolith can be safely decommissioned, leaving behind a modern, microservice-based architecture.

Core Principles

The Strangler Fig Pattern is built upon several core principles that make it a powerful strategy for architectural evolution:

  • Incremental Replacement: This is the cornerstone. Instead of a high-risk “big bang” rewrite, you replace small, manageable pieces of the monolith one at a time. Each successful migration builds confidence and reduces overall risk.
  • Decoupling and Isolation: Each new microservice is designed to be independent, with its own codebase, deployment pipeline, and potentially its own data store. This reduces coupling and allows teams to work more autonomously.
  • Risk Reduction: By migrating incrementally, you minimize the potential for large-scale failures. If a new microservice has issues, only that specific functionality is affected, not the entire application.
  • Continuous Operation: The business-critical application remains operational throughout the entire migration process. Users continue to interact with the system, often without even realizing that a transformation is underway.
  • Learning and Adaptation: The iterative nature of the pattern allows teams to learn about microservices, discover optimal bounded contexts, and refine their architecture as they go.
  • Technology Modernization: New services can be built using modern technologies, languages, and frameworks, without being constrained by the legacy stack of the monolith.

Why Migrate from a Monolith?

Before diving deeper into the how, it’s crucial to understand the compelling reasons many organizations choose to move away from monolithic architectures, despite their initial simplicity.

The Monolith’s Bottlenecks

While monoliths offer a straightforward development model in their early stages, their advantages quickly diminish as applications grow in size and complexity:

  • Scalability Issues: Monoliths scale as a single unit. If one small component, like a recommendation engine, becomes a bottleneck, you have to scale the entire application, even if other parts don’t need additional resources. This is inefficient and expensive.
  • Deployment Complexities: A change in any part of the monolith requires redeploying the entire application. This leads to long deployment cycles, increased risk of downtime, and difficulty in implementing continuous delivery. “Big bang” deployments are nerve-wracking.
  • Technology Lock-in: Once a monolith is built with a specific set of technologies (language, framework, database), it’s incredibly difficult to introduce new ones or upgrade existing major versions. Teams are stuck with potentially outdated tech.
  • Developer Productivity: As the codebase grows, it becomes harder for developers to understand the entire system. Onboarding new team members can be a nightmare. Merge conflicts become frequent, and a change in one area might unknowingly break functionality elsewhere due to tight coupling.
  • Resilience: A single point of failure can bring down the entire application. If a memory leak in a reporting module causes the application server to crash, your e-commerce checkout might also go down.
  • Resource Contention: Different modules within a monolith might have conflicting resource requirements. For example, a CPU-intensive analytics module might compete for resources with a low-latency API, leading to suboptimal performance for both.

The Promise of Microservices (Briefly)

Microservices aim to address these issues by breaking down the application into small, independent services. Each service:

  • Can be developed, deployed, and scaled independently.
  • Can use different technologies best suited for its specific task (polyglot persistence and programming).
  • Is owned by a small, autonomous team.
  • Is more resilient; the failure of one service doesn’t necessarily impact others.

However, the journey to microservices isn’t without its own challenges, and that’s precisely why the Strangler Fig Pattern is so valuable—it provides a controlled path to realize these benefits.

Strangler Fig Pattern: Safely Migrating Monoliths
Generated Image

The Strangler Fig Pattern in Action: A Step-by-Step Guide

Implementing the Strangler Fig Pattern is an iterative process. It requires careful planning, disciplined execution, and continuous monitoring. Let’s break down the key steps involved.

Step 1: Identify a Bounded Context / Candidate Module

This is arguably the most crucial first step. You can’t just randomly pick a piece of the monolith. You need to identify a logical, self-contained unit of functionality that can operate relatively independently. This is where Domain-Driven Design (DDD) concepts like “Bounded Contexts” become invaluable. A bounded context defines a clear boundary within which a particular domain model is consistent.

How to identify a good candidate:

  • Low-Risk, High-Value: Start with a module that is relatively isolated, has few dependencies on other parts of the monolith, and provides clear business value when extracted. This builds confidence and demonstrates early success.
  • Clearly Defined Responsibilities: Can you articulate what this module does without using “and” too much? E.g., “User Authentication” is a good candidate. “User Management and Product Catalog and Order Processing” is not.
  • High Change Frequency: If a particular module is frequently updated or causes frequent bugs due to its complexity, it’s a strong candidate for extraction.
  • Performance Bottleneck: If a specific part of the monolith is consistently causing performance issues, extracting it into a dedicated microservice can allow it to scale independently.
  • Example Candidates: User authentication, product catalog, inventory management, notification service, reporting module, payment processing.

Let’s consider an e-commerce monolith. A good starting point might be the “User Authentication” module.

Step 2: Implement a Facade / Reverse Proxy

The “strangler” itself begins here. You introduce a new component, typically an API Gateway or a reverse proxy, that sits in front of your existing monolith. All incoming requests will now go through this proxy. Initially, the proxy simply forwards all requests directly to the monolith.

Architectural Description (Diagram in Words):


Client Request ----> API Gateway / Reverse Proxy ----> Monolith Application
                                ^
                                |
                                | (All requests initially routed here)

This proxy is your control point. It allows you to intercept requests, apply policies (like authentication, rate limiting), and most importantly, redirect traffic to new services as they are built. Common technologies for this include Nginx, Apache HTTP Server, HAProxy, AWS API Gateway, Azure API Management, Kong, or Spring Cloud Gateway.

Code Example (Nginx Proxy Configuration):


# nginx.conf extract

http {
    upstream monolith_backend {
        server 127.0.0.1:8080; # Address of your monolithic application
    }

    server {
        listen 80;
        server_name myapp.com;

        location / {
            proxy_pass http://monolith_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

In this initial setup, Nginx simply acts as a transparent proxy, forwarding all requests for myapp.com to the monolithic application running on port 8080.

Step 3: Develop the New Microservice

Now that you have your proxy in place, you can start building the new microservice for the identified bounded context. This service should be developed independently, with its own codebase, deployment pipeline, and ideally, its own dedicated data store.

Key Considerations:

  • Data Isolation: The ideal scenario is for the new microservice to have its own database. If the monolith and the new service need to share data during the migration, consider data replication mechanisms (e.g., Change Data Capture – CDC) rather than direct shared database access, which can create tight coupling.
  • API Design: Design a clean, well-defined API for your new service (REST, gRPC, etc.).
  • Technology Choice: This is an opportunity to use modern technologies if desired, free from the constraints of the monolith.

Let’s continue with our “User Authentication” example. We’ll build a simple Python Flask microservice for user login.

Code Example (Python Flask Authentication Microservice – auth_service.py):


from flask import Flask, request, jsonify

app = Flask(__name__)

# A very basic in-memory "user database" for demonstration
users_db = {
    "alice": {"password": "password123", "roles": ["user"]},
    "bob": {"password": "securepass", "roles": ["admin", "user"]}
}

@app.route('/auth/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')

    if not username or not password:
        return jsonify({"message": "Username and password required"}), 400

    user_info = users_db.get(username)
    if user_info and user_info['password'] == password:
        # In a real app, generate a JWT or session token here
        return jsonify({"message": "Login successful", "token": f"jwt_for_{username}"}), 200
    else:
        return jsonify({"message": "Invalid credentials"}), 401

@app.route('/auth/validate', methods=['GET'])
def validate_token():
    # Placeholder for token validation logic
    auth_header = request.headers.get('Authorization')
    if auth_header and auth_header.startswith('Bearer '):
        token = auth_header.split(' ')[1]
        # In a real app, decode and validate JWT
        if "jwt_for_" in token: # Simple check
            username = token.replace("jwt_for_", "")
            return jsonify({"message": "Token valid", "user": username, "roles": users_db.get(username, {}).get("roles", [])}), 200
    return jsonify({"message": "Invalid or missing token"}), 401

if __name__ == '__main__':
    app.run(port=5001, debug=True) # Runs on a different port than monolith

This microservice provides endpoints for /auth/login and /auth/validate, which were previously handled by the monolith. It runs on port 5001, separate from the monolith (which is on 8080).

Step 4: Reroute Traffic Incrementally

This is where the “strangling” truly begins. Once your new microservice is ready and thoroughly tested, you update your API Gateway/proxy to start routing specific requests to it. This is typically done based on URL paths, HTTP methods, or headers.

Architectural Description (Diagram in Words):


                                     /auth/* (to new service)
                                    /
Client Request ----> API Gateway / Reverse Proxy ----> New Auth Microservice
                                    \
                                     \
                                      ----> Monolith Application (for all other requests)

You can start with a “dark launch” (sending a copy of production traffic to the new service without affecting user responses) or route a small percentage of user traffic (canary deployment) before fully switching over. This incremental rerouting is key to safety.

Code Example (Updated Nginx Proxy Configuration):


# nginx.conf extract - Updated

http {
    upstream monolith_backend {
        server 127.0.0.1:8080; # Address of your monolithic application
    }

    upstream auth_service_backend {
        server 127.0.0.1:5001; # Address of your new Auth Microservice
    }

    server {
        listen 80;
        server_name myapp.com;

        # Route /auth requests to the new authentication service
        location /auth {
            proxy_pass http://auth_service_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # All other requests still go to the monolith
        location / {
            proxy_pass http://monolith_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

Now, any request to myapp.com/auth/* will be routed to the new Flask authentication service, while all other requests (e.g., myapp.com/products, myapp.com/orders) will still go to the monolith. This is the heart of the Strangler Fig Pattern: controlled, incremental migration.

Step 5: Decommission the Old Monolith Module

Once you are confident that the new microservice is fully functional, stable, and handling all relevant traffic, you can remove the corresponding functionality from the monolith. This involves:

  • Deleting the old code module from the monolith’s codebase.
  • Removing any related database tables or schema elements (after ensuring data consistency and migration).
  • Updating any internal monolith calls to point to the new microservice’s API (if the monolith still needs to interact with this functionality).

This step truly makes the monolith “shrink” and reduces its complexity. It’s a critical part of realizing the benefits of the pattern.

Step 6: Repeat and Iterate

The Strangler Fig Pattern is not a one-off event; it’s a continuous process. After successfully migrating one module, you return to Step 1 and identify the next candidate. Each iteration refines your understanding of your domain, improves your microservice development practices, and gradually transforms your architecture.

  • Identify the next bounded context.
  • Build the new service.
  • Route traffic.
  • Decommission the old code.
  • Monitor, learn, and improve.

Architectural Considerations and Design Patterns

Successfully applying the Strangler Fig Pattern involves more than just routing traffic. It requires careful consideration of various architectural aspects and often leverages other design patterns to manage the complexities of distributed systems.

API Gateway / Facade

As discussed, the API Gateway is central to the Strangler Fig Pattern. It serves as the single entry point for all client requests, abstracting the underlying architecture. Beyond simple routing, a robust API Gateway typically provides:

  • Request Routing: Directing requests to the appropriate microservice or monolith.
  • Authentication and Authorization: Centralizing security concerns.
  • Rate Limiting: Protecting services from overload.
  • Logging and Monitoring: Providing a central point for request traceability.
  • SSL Termination: Handling secure connections.
  • Circuit Breaking: Preventing cascading failures in a distributed system.

Using a dedicated API Gateway solution (like Kong, Apache APISIX, AWS API Gateway, Azure API Management, or Spring Cloud Gateway) rather than just Nginx for more advanced features is often recommended for larger migrations.

Data Migration Strategies

Data is often the trickiest part of a monolithic migration. Tightly coupled data schemas are a hallmark of monoliths. Several strategies can be employed:

  • Shared Database (Temporary): In the initial phases, the new microservice might temporarily share the monolith’s database. This is a common pragmatic choice to get started, but it introduces coupling and should be treated as a temporary state. The new service should only access its own logical tables or views.
  • Data Replication (CDC): Use Change Data Capture (CDC) tools (e.g., Debezium, Apache Kafka Connect) to stream changes from the monolith’s database to the new microservice’s dedicated database. This keeps data synchronized while allowing the new service to have its own schema.
  • Database per Service: The ultimate goal. Each microservice owns its data, encapsulated within its boundaries. This promotes true independence but requires careful planning for data synchronization and consistency across services.
  • Eventual Consistency: When data is partitioned across multiple services, strong transactional consistency across all services is often replaced by eventual consistency, managed through asynchronous events.
  • Data Duplication/Materialized Views: For read-heavy scenarios, relevant data might be duplicated or materialized in the new service’s database, kept in sync via events.

The transition from a shared database to a database-per-service model is often a sub-migration within the larger strangler pattern, using the same incremental principles.

Communication Between Services

As you extract services, they will need to communicate. There are two primary paradigms:

  • Synchronous Communication (REST/gRPC): For requests that require an immediate response. RESTful APIs over HTTP are common for exposing functionality. gRPC offers performance benefits and strong typing for inter-service communication.
  • Asynchronous Communication (Message Queues/Event Buses): For scenarios where immediate responses aren’t required, or for event-driven architectures. Technologies like Apache Kafka, RabbitMQ, or AWS SQS/SNS allow services to communicate by publishing and subscribing to events. This decouples services in time and failure. This is crucial for maintaining data consistency across services in an eventually consistent model.

Monitoring and Observability

During a migration, and especially in a microservice environment, robust monitoring and observability are non-negotiable. You need to understand how both the monolith and the new services are performing and interacting.

  • Centralized Logging: Aggregate logs from all services (monolith, proxy, new microservices) into a central system (e.g., ELK stack, Splunk, Datadog).
  • Distributed Tracing: Tools like OpenTelemetry, Jaeger, or Zipkin allow you to trace a single request as it flows through multiple services, helping to identify bottlenecks and failures in a distributed system.
  • Metrics and Alerts: Collect performance metrics (CPU, memory, request latency, error rates) for all components and set up alerts for anomalies.
  • Health Checks: Implement health check endpoints for all services to allow the API Gateway and orchestration platforms to determine service availability.

Without adequate observability, identifying issues during the incremental rerouting phase becomes incredibly difficult, negating the safety benefits of the Strangler Fig Pattern.

Deployment Strategies

Modern deployment strategies complement the Strangler Fig Pattern, making incremental rerouting safer and more efficient:

  • Containerization (Docker, Kubernetes): Packaging services in containers provides consistency across environments and simplifies deployment. Kubernetes allows for advanced deployment patterns like canary releases, rolling updates, and blue/green deployments, which are perfect for rerouting traffic incrementally.
  • CI/CD Pipelines: Automated Continuous Integration and Continuous Deployment pipelines are essential for rapidly building, testing, and deploying new microservices without manual intervention.
Strangler Fig Pattern: Safely Migrating Monoliths
Generated Image

Real-World Scenarios and Examples

Let’s walk through a few common real-world scenarios to illustrate how the Strangler Fig Pattern can be applied.

Scenario 1: Migrating a Legacy E-commerce Order Processing Module

Initial State: A large e-commerce monolith handles everything from product display to order processing, payment, and shipping. The “Order Processing” module is particularly complex, involves many business rules, and is a frequent source of bugs and performance bottlenecks, especially during peak sales periods.

The Problem: Scaling the entire monolith just to handle increased order volume is inefficient. Updates to order logic are slow and risky.

Strangler Fig Strategy:

  1. Identify Candidate: The “Order Processing” bounded context is a prime candidate. It has clear inputs (customer, cart) and outputs (order confirmation, payment request, shipping notification).
  2. Implement Proxy: An API Gateway (e.g., AWS API Gateway) is placed in front of the monolith. All requests hit this gateway first.
  3. Develop New Microservice: A new “Order Service” microservice is built. It will handle creating, updating, and retrieving orders. For data, an initial approach might involve:
    • New orders are created directly in the Order Service’s dedicated database.
    • Historical order data from the monolith’s database is migrated or synchronized to the new Order Service’s database using CDC (e.g., Debezium streaming from PostgreSQL to Kafka, then to the new service’s database).

    The monolith’s existing Order module is modified to delegate creation requests to the new Order Service’s API if needed for internal calls during the transition.

  4. Reroute Traffic:
    • Initially, the API Gateway continues to send all /orders/* requests to the monolith.
    • Once the new Order Service is stable, the API Gateway is configured to route all POST /orders requests (for creating new orders) to the new Order Service.
    • After successful monitoring, GET /orders/{id} and other read operations are gradually rerouted.
    • Any internal calls from other parts of the monolith (e.g., a “User Profile” module needing to list a user’s orders) are updated to call the new Order Service’s API instead of the internal monolith function.
  5. Decommission: Once all order-related functionalities (creation, retrieval, updates, status changes) are fully handled by the new Order Service and verified, the corresponding code and database tables within the monolith are removed.

Challenges & Solutions:

  • Data Consistency: Ensuring that both old and new order data are consistent during the transition. CDC is key here, possibly with a reconciliation process.
  • Transactional Integrity: What if an order is created but payment fails? Implement Saga patterns or eventual consistency with compensating transactions via an event bus (e.g., Kafka) to manage distributed transactions across the Order Service, Payment Service (if also extracted), and Inventory Service.
  • Rollback Strategy: Have a clear plan to revert traffic to the monolith’s old order service if issues arise with the new service.

Scenario 2: Extracting a User Management and Authentication System

Initial State: A monolithic enterprise application has a tightly integrated user management system that handles authentication, user profiles, roles, and permissions. Every module within the monolith directly interacts with this system’s database tables or internal APIs.

The Problem: The authentication logic is complex, difficult to update, and a potential security vulnerability if not handled correctly. Adding new identity providers (OAuth, SSO) is a pain. Other applications in the enterprise also need user management functionality, leading to duplication

Written by

Khader Vali

Senior Software Engineer specializing in cloud architecture, real-time systems, and enterprise-scale applications.

Share this article

Related Articles

Rust & WebAssembly: Building Performant Web Apps

Jul 03, 2026 · 9 min read

Building Scalable WebSockets for Real-time Apps

Jun 21, 2026 · 13 min read

Building Event-Driven Serverless Apps on AWS

Jun 25, 2026 · 15 min read