System Design

Strangler Fig Pattern: Safe Monolith to Microservices Migration

Learn how to safely migrate your monolithic application to microservices using the Strangler Fig Pattern. This guide covers architecture, implementation, and best practices.

Khader Vali October 5, 2026 17 min read

The Strangler Fig Pattern: Safely Migrating Monoliths to Microservices

As a senior software engineer, I’ve seen countless organizations grapple with the decision to move from monolithic architectures to microservices. The allure of microservices—scalability, independent deployments, technology diversity, and team autonomy—is strong. However, the path from a monolithic beast to a nimble fleet of microservices is fraught with peril. A “big bang” rewrite is often a one-way ticket to project failure, massive cost overruns, and lost business value. This is where the Strangler Fig Pattern comes in, offering a pragmatic, incremental, and significantly safer approach to this critical transformation.

At Khadervali.com, we believe in sharing practical, battle-tested strategies. In this extensive guide, we’ll dive deep into the Strangler Fig Pattern, exploring its principles, architecture, implementation details, real-world considerations, and why it’s often the most intelligent choice for evolving your legacy systems.

The Monolith’s Dilemma: Why Migrate?

Monolithic applications, while often starting as efficient and easy to develop, eventually hit a wall. Here are some common pain points:

  • Scaling Challenges: You might only need to scale one component, but with a monolith, you often have to scale the entire application, leading to inefficient resource utilization.
  • Slow Development Cycles: A large codebase means longer build times, complex merge conflicts, and a higher risk of introducing regressions. Deployments become infrequent and risky.
  • Technology Lock-in: Once you pick a technology stack for your monolith, it’s incredibly hard to change it. This stifles innovation and makes it difficult to attract talent interested in modern tools.
  • Tight Coupling: Changes in one part of the system can inadvertently affect seemingly unrelated parts, leading to brittle code and fear of modification.
  • Team Bottlenecks: Large teams working on a single codebase often step on each other’s toes, leading to coordination overhead and reduced productivity.

Microservices promise a solution to these problems, but the journey to get there can be more disruptive than the problems themselves if not handled carefully.

Introducing the Strangler Fig Pattern

The Strangler Fig Pattern, coined by Martin Fowler, is inspired by a natural phenomenon. Strangler fig trees grow around a host tree, slowly encapsulating it, eventually “strangling” it and becoming the primary structure. In software terms, this means gradually replacing the functionality of an existing system (the monolith) with new services (the fig) until the old system can eventually be retired.

The Core Idea: Incremental Replacement

Instead of rebuilding everything from scratch, you identify specific functionalities within your monolith that can be extracted or rewritten as independent microservices. A facade or proxy layer then routes incoming requests—some to the old monolith, and others to the new microservices. Over time, more and more functionality is migrated, and the monolith shrinks until it’s no longer needed.

Why Choose the Strangler Fig Pattern?

The benefits of this approach are substantial:

  • Reduced Risk: You’re not attempting a massive, all-or-nothing rewrite. Each step is small, testable, and reversible.
  • Continuous Delivery: You can continue to deploy changes and new features to your existing monolith while simultaneously building and deploying new microservices. Business operations remain uninterrupted.
  • Preserves Existing Functionality: Critical business logic that works well in the monolith can remain there until it’s absolutely necessary or beneficial to migrate it.
  • Learning Opportunity: Teams gain experience with microservice development, deployment, and operations in a controlled environment, learning from early, smaller migrations before tackling more complex ones.
  • Improved System Stability: By isolating services, failures in one new microservice are less likely to bring down the entire system, and new functionality can be rolled out with reduced risk to the core business.
  • Immediate Value: As soon as a microservice is deployed, it can start delivering benefits—better performance, scalability, or easier development for that specific domain.

Key Components and Architectural Overview

To implement the Strangler Fig Pattern effectively, you need a few key architectural components working in concert. Let’s visualize this with a textual diagram and then break down each part.

Architectural Diagram (in words):


+--------------------------------------------------+
|               Internet / Client Applications     |
+--------------------------------------------------+
          | (Incoming Requests)
          V
+--------------------------------------------------+
|             API Gateway / Reverse Proxy          | <-- The Strangler Facade
|             (e.g., Nginx, AWS API Gateway)       |
|             Routes requests based on rules       |
+--------------------------------------------------+
          |   /                           \
          |  /                             \
          V /                               \ V
+---------------------+             +---------------------+
|   Monolith          |             |   New Microservice A  | <-- Strangler Vines
|   (Old System)      |             |   (e.g., User Service)|
|   - Legacy Logic    |             |   - Independent DB    |
|   - Shrinking Code  |             |   - Dedicated APIs    |
+---------------------+             +---------------------+
          |                                     |
          V                                     V
+---------------------+             +---------------------+
| Monolith's Database |             | Microservice A's DB |
+---------------------+             +---------------------+

(Additional Microservices B, C, etc. would branch off the API Gateway as well)

Data Synchronization Channels (e.g., Kafka, Debezium for CDC)
Between Monolith's DB <--> New Microservices' DBs (during migration)

1. The Facade/Proxy (The Strangler Facade)

This is the most critical component. It’s the entry point for all client requests and intelligently routes them to either the existing monolith or the newly built microservices. Think of it as a traffic controller.

  • How it works: Based on configured rules (e.g., URL paths, headers, query parameters), the facade directs requests. For instance, /api/users/* might go to the new User Service, while /api/orders/* still goes to the monolith.
  • Implementation Options:
    • API Gateway: Dedicated services like AWS API Gateway, Azure API Management, Kong, or Spring Cloud Gateway. These offer advanced features like authentication, rate limiting, and analytics.
    • Reverse Proxy: Tools like Nginx or Apache HTTP Server can be configured to forward requests based on paths. This is a simpler, often quicker starting point.
    • Custom Proxy: You could build a small service in front of your monolith, especially if your routing logic is very complex or requires deep integration with existing authentication.

Let’s consider a simple Nginx configuration as an example of a facade:


# Nginx as a Strangler Facade
http {
    upstream monolith_backend {
        server localhost:8080; # Your monolith's address
    }

    upstream user_service_backend {
        server localhost:8081; # Your new user microservice address
    }

    server {
        listen 80;

        location /api/users/ {
            proxy_pass http://user_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;
        }

        location /api/orders/ {
            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;
        }

        # Catch-all for anything not explicitly routed
        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;
        }
    }
}

This configuration routes requests starting with /api/users/ to the new user service and everything else (including /api/orders/) to the monolith. This is the fundamental mechanism of the Strangler Fig Pattern.

2. New Microservices (The Strangler Vines)

These are the independent services you build or extract. Each microservice should ideally be:

  • Loosely Coupled: Minimal dependencies on other services.
  • Independently Deployable: Can be deployed without affecting other services.
  • Domain-Oriented: Focuses on a specific business capability (e.g., User Management, Product Catalog, Order Processing).
  • Owns its Data: Has its own dedicated database, preventing the “shared database” anti-pattern common in monoliths.

When creating new services, you have two primary approaches:

  • Extracting Functionality: Taking existing code and data from the monolith and moving it into a new service. This is often more challenging due to intertwined code and shared databases.
  • Building New Functionality: If you’re adding entirely new features, you can build them directly as microservices from day one. This is generally the easiest starting point for strangulation.

3. The Monolith (The Host Tree)

The existing application. As microservices are extracted, the monolith’s responsibilities shrink. Its codebase becomes smaller, and eventually, it might only handle legacy functionalities that aren’t worth migrating or are rarely used. The goal is to eventually retire it, allowing the new microservices to stand alone.

  • Gradual Reduction: Code related to migrated features should be removed or deprecated in the monolith once the new service is stable and verified.
  • Eventual Deprecation: The ultimate goal is to eliminate the monolith, but this can take months or even years, depending on the complexity.

4. Data Migration and Synchronization Strategy

This is arguably the most complex part of any monolith-to-microservice migration. The “database per service” principle is crucial for microservice autonomy, but migrating data from a shared monolithic database to multiple independent databases while keeping the system operational is tricky.

Common strategies include:

  • Database Replication: Replicating a portion of the monolith’s database to a new microservice’s database.
  • Double Write: During the migration period, both the monolith and the new microservice write data to both the old monolithic database and the new microservice’s database. Reads can then be gradually shifted to the new service.
  • Event-Driven Synchronization (Change Data Capture – CDC): The monolith publishes events whenever data changes. New microservices subscribe to these events and update their own databases accordingly. Tools like Debezium are excellent for CDC.
  • One-time Data Migration: For read-heavy or less frequently updated data, a single bulk transfer might suffice, followed by an immediate switch-over.

The “double write” pattern is common for ensuring data consistency during the transition:


// Example in a Monolith (e.g., Java Spring Boot)
// Before migration, only writes to monolith's DB
// public User saveUser(User user) {
//     return userRepository.save(user);
// }

// During migration: Double Write to monolith and new microservice
// Assumes an HttpClient or gRPC client for the new User Microservice
public User saveUser(User user) {
    // 1. Write to Monolith's database
    User savedUser = userRepository.save(user);

    try {
        // 2. Write to the new User Microservice (via its API)
        // This call should ideally be asynchronous or idempotent to handle failures gracefully
        userServiceClient.createUser(user);
        logger.info("Successfully double-wrote user to new microservice: " + user.getUsername());
    } catch (Exception e) {
        logger.error("Failed to double-write user to new microservice: " + user.getUsername(), e);
        // Handle failure: alert, retry mechanism, or log for manual reconciliation
        // Crucially, the monolith's operation must succeed even if microservice write fails
    }

    return savedUser;
}

This ensures the monolith continues to function correctly, while the new service’s data store is populated. Once confidence is high, the monolith’s write to its own database for that specific entity can be removed, and its reads redirected to the new service.

Strangler Fig Pattern: Safe Monolith to Microservices Migration
Generated Image

Step-by-Step Implementation Guide for the Strangler Fig Pattern

Migrating a monolith isn’t a single project; it’s a program of continuous evolution. Here’s a structured approach:

Phase 1: Analysis and Identification – Where to Strangler First?

This initial phase is crucial for success. You can’t just randomly pick a part of your monolith.

  1. Identify Bounded Contexts (Domain-Driven Design): Look for natural boundaries within your application’s domain. For an e-commerce platform, these might be “User Management,” “Product Catalog,” “Order Processing,” “Payment Gateway,” “Shipping,” etc. These are excellent candidates for independent microservices.
  2. Analyze Module Coupling: Use tools to visualize dependencies within your codebase. Identify modules with low incoming and outgoing dependencies. These are easier to extract. Highly coupled modules indicate a more complex migration.
  3. Look for “Pain Points”:
    • High Traffic/Performance Bottlenecks: A specific module that receives a lot of requests or is a performance hotspot is a prime candidate. Migrating it can yield immediate performance benefits.
    • Frequent Changes: Features that are constantly evolving are good candidates, as new development can happen faster in a microservice.
    • Independent Teams: If a specific part of the system is owned by a distinct team, it’s easier to give them autonomy over a new microservice.
    • New Functionality: If you’re building a brand-new feature, always build it as a microservice from day one, routing requests through the facade.
  4. Start Small and Simple: Don’t pick the most complex, business-critical component first. Choose a module that has clear boundaries, minimal dependencies, and isn’t central to critical transaction flows. A “User Profile” service or a “Notification” service are often good starting points.

Phase 2: Establish the Facade/Proxy

Before you build any new microservices, set up your routing layer. This is non-negotiable.

  1. Choose Your Technology: Nginx, API Gateway (AWS, Azure, Google Cloud), Kong, Spring Cloud Gateway, or a custom solution. Start simple; you can always evolve later.
  2. Deploy and Configure: Deploy the facade in front of your monolith. Initially, it should route *all* traffic to the monolith. This validates your setup and ensures no disruption.
  3. Implement Basic Monitoring: Ensure you have logging, metrics, and tracing set up for your facade. You need to know how it’s performing and if it’s routing traffic correctly.

Let’s consider our Nginx example again. Initially, it routes everything to the monolith:


# Nginx as initial Strangler Facade (all to monolith)
http {
    upstream monolith_backend {
        server localhost:8080; # Your monolith's address
    }

    server {
        listen 80;

        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;
        }
    }
}

This serves as the stable base for future routing changes.

Phase 3: Extract or Build the First Microservice

Now, let’s take that first bite out of the monolith.

  1. Define the Microservice’s Scope: Based on your analysis, clearly define what the microservice will do and what data it will own.
  2. Implement the Microservice: Build the new service using your chosen technology stack. Focus on API design (REST, gRPC) and ensure it’s independently deployable.
  3. Example: A Simple User Service (Python Flask)

# user_service.py (A simple Flask microservice)
from flask import Flask, jsonify, request

app = Flask(__name__)

# In a real app, this would be a database
users_db = {
    "1": {"id": "1", "name": "Alice Smith", "email": "alice@example.com"},
    "2": {"id": "2", "name": "Bob Johnson", "email": "bob@example.com"}
}
next_id = 3

@app.route('/api/users', methods=['GET'])
def get_users():
    return jsonify(list(users_db.values()))

@app.route('/api/users/', methods=['GET'])
def get_user(user_id):
    user = users_db.get(user_id)
    if user:
        return jsonify(user)
    return jsonify({"message": "User not found"}), 404

@app.route('/api/users', methods=['POST'])
def create_user():
    global next_id
    new_user_data = request.json
    if not new_user_data or 'name' not in new_user_data or 'email' not in new_user_data:
        return jsonify({"message": "Invalid user data"}), 400

    new_user_id = str(next_id)
    new_user = {"id": new_user_id, "name": new_user_data['name'], "email": new_user_data['email']}
    users_db[new_user_id] = new_user
    next_id += 1
    return jsonify(new_user), 201

if __name__ == '__main__':
    app.run(port=8081, debug=True) # Runs on port 8081
  1. Update the Facade: Modify your API Gateway or Nginx configuration to route requests for the new service to its endpoint.

# Nginx with User Service route
http {
    upstream monolith_backend {
        server localhost:8080;
    }

    upstream user_service_backend {
        server localhost:8081; # Our new Flask user service
    }

    server {
        listen 80;

        # Route /api/users to the new user service
        location /api/users/ {
            proxy_pass http://user_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;
        }

        # 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;
        }
    }
}
  1. Modify the Monolith (if necessary): If the monolith previously handled user management, you might need to:
    • Remove its internal user management logic.
    • Change internal calls within the monolith to call the new User Service’s API instead of its own internal methods.
    • For example, a Java monolith might have a UserService interface. You’d create a new implementation that calls the REST API of the new microservice.

// Monolith's original User Service interface and implementation
// public interface UserService { User findById(Long id); User save(User user); }
// public class InternalUserService implements UserService { /* ... database calls ... */ }

// Monolith's new User Service implementation (after user service extraction)
// This will replace the old InternalUserService in the monolith's dependency injection
public class RemoteUserServiceClient implements UserService {
    private final RestTemplate restTemplate; // Or WebClient, FeignClient, etc.
    private final String userServiceBaseUrl = "http://localhost:8081/api/users"; // Address of new microservice

    public RemoteUserServiceClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    public User findById(Long id) {
        return restTemplate.getForObject(userServiceBaseUrl + "/" + id, User.class);
    }

    @Override
    public User save(User user) {
        // If the monolith still needs to create users for some reason,
        // it calls the new microservice.
        // During the double-write phase, this would be more complex (write local then call remote).
        return restTemplate.postForObject(userServiceBaseUrl, user, User.class);
    }
    // ... other methods ...
}

Phase 4: Data Migration and Synchronization

This is where the rubber meets the road for data independence.

  1. Initial Data Load: Copy existing user data from the monolith’s database to the new microservice’s database. This might be a one-time script.
  2. Implement Synchronization:
    • Double Write: As shown in the Java example, modify the monolith to write to both its old user table and call the new microservice’s API to create/update users. This ensures both systems are consistent during the transition.
    • CDC (Change Data Capture): Use tools like Debezium or similar mechanisms to capture changes from the monolith’s database and publish them as events. The new microservice subscribes to these events and updates its own database.
    • Event-driven for New Services: If the monolith needs to be informed of changes in the new microservice, the microservice can publish events (e.g., “UserCreatedEvent”) that the monolith subscribes to.
  3. Verification: Continuously monitor data consistency. Write reconciliation scripts to compare data between the two systems.

Let’s illustrate a conceptual event-driven flow for data synchronization:


// Monolith (Producer of events for User updates)
// Assuming we have an event bus (e.g., Kafka, RabbitMQ)
public class MonolithUserService {
    // ... existing monolith logic ...

    public User updateUserInMonolith(User user) {
        User updatedUser = userRepository.save(user); // Save to monolith's DB
        eventPublisher.publish(new UserUpdatedEvent(updatedUser.getId(), updatedUser.getName(), updatedUser.getEmail()));
        return updatedUser;
    }
}

// New User Microservice (Consumer of events from Monolith)
public class UserServiceEventListener {
    private final UserServiceRepository userServiceRepository; // Microservice's own repository

    public UserServiceEventListener(UserServiceRepository userServiceRepository) {
        this.userServiceRepository = userServiceRepository;
    }

    @EventListener
    public void handleUserUpdatedEvent(UserUpdatedEvent event) {
        // Find user in microservice's DB, update it, or create if it doesn't exist
        User microserviceUser = userServiceRepository.findById(event.getUserId())
                                                    .orElse(new User(event.getUserId()));
        microserviceUser.setName(event.getUserName());
        microserviceUser.setEmail(event.getUserEmail());
        userServiceRepository.save(microserviceUser);
        logger.info("Microservice updated user from monolith event: " + event.getUserId());
    }
}

This demonstrates how changes originating in the monolith can be propagated to the new microservice, ensuring data consistency until the monolith’s ownership of that data is completely relinquished.

Strangler Fig Pattern: Safe Monolith to Microservices Migration
Generated Image

Phase 5: Iteration, Refinement, and Deprecation

The process doesn’t stop with one microservice.

  1. Repeat: Once the first microservice is stable, repeat phases 1-4 for the next identified bounded context. Each iteration makes the monolith smaller and your microservice ecosystem richer.
  2. Monitoring and Observability: Double down on your observability stack. Distributed systems are inherently more complex. You need robust logging, metrics, tracing (e.g., Jaeger, OpenTelemetry), and alerting to quickly identify and resolve issues across services.
  3. Performance Testing: Regularly test the performance of both the new services and the parts of the monolith that remain, especially the interactions through the facade.
  4. Refactor Monolith Code: As features move out, remove the dead code from the monolith. This keeps the monolith manageable and prevents confusion.
  5. Eventually Deprecate: Over time, the monolith will shrink to a minimal core or eventually disappear entirely. Plan for its decommissioning from the start. Celebrate when you can finally turn it off!

Real-World Scenarios and Critical Considerations

The Strangler Fig Pattern isn’t just about code and architecture; it impacts teams, processes, and culture.

Team Structure (Conway’s Law)

Your organizational structure will reflect your system’s architecture. As you move to microservices, consider aligning your teams with bounded contexts. Each team can own a set of related microservices, fostering autonomy and accelerating development. This might mean reorganizing existing teams or creating new ones.

Testing Strategy

With distributed systems, testing becomes more complex. You’ll need:

  • Unit Tests: For individual components within a service.
  • Integration Tests: To ensure services can communicate correctly with each other and their databases.
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

Idempotent APIs: Reliability in Distributed Systems

Aug 02, 2026 · 17 min read

Rust & WebAssembly: Building Performant Web Apps

Oct 03, 2026 · 14 min read