Migrating a monolithic system that is live in production toward a microservices architecture is one of the riskiest maneuvers a software team can undertake. The classic mistake is the complete rewrite ("big bang rewrite"): freezing new-feature development, building the new system from scratch over months, and, on the day of truth, discovering that it fails to replicate all the nuances of the original. The industry has learned to do it another way: incrementally, keeping the monolith alive and in production while pieces are extracted from it one by one. The pattern that gives this strategy its name is the Strangler Fig, coined by Martin Fowler. In this lesson we will look at the incremental strategy, how to identify the monolith's seams, how to protect new models with an Anti-Corruption Layer, the Branch by Abstraction technique, and the risks you should anticipate.

Contents

  1. The problem with the "big bang" rewrite
  2. The Strangler Fig pattern
  3. Identifying the monolith's seams
  4. Branch by Abstraction
  5. Anti-Corruption Layer (ACL)
  6. Step-by-step incremental strategy
  7. Risks and how to mitigate them
  8. Common Mistakes and Tips
  9. Exercises
  10. Conclusion

  1. The problem with the "big bang" rewrite

The temptation to throw away the monolith and start from scratch is enormous: the old code "is disgusting," nobody fully understands how it works, and the new paradigm promises cleanliness. But the total rewrite almost always fails for the same reasons:

  • The monolith encodes years of implicit business rules. Many of those rules are undocumented: they live in seemingly absurd if statements that actually handle a real edge case.
  • The business does not stop. While you rewrite for 18 months, the monolith keeps receiving changes. You are chasing a moving target.
  • Cutover day is a leap into the void. All the data and traffic migration happens at once, with maximum risk and minimum reversibility.

The mature alternative is to strangle the monolith bit by bit. Each step is small, is in production, and is reversible.

  1. The Strangler Fig pattern

The name comes from the strangler fig: a plant that grows around a host tree, gradually envelops it, and over the years replaces it completely until the original tree disappears. Applied to software, the idea is to interpose a routing layer (usually a gateway or proxy) in front of the monolith. That layer decides, request by request, whether it is served by the old monolith or by an already-extracted new service.

graph TD
    Cliente[Client] --> GW[Gateway / Strangler Facade]
    GW -->|migrated functionality| MS1[New Payments service]
    GW -->|migrated functionality| MS2[New Catalog service]
    GW -->|not yet migrated| Mono[Legacy monolith]
    MS1 --> DBN[(Service DB)]
    Mono --> DBM[(Monolith DB)]

The diagram shows the key piece: the Strangler Facade (the facade or gateway). At first, almost all traffic goes to the monolith. As you extract capabilities, you redirect specific routes (for example, /payments/*) to the new service. The client never notices the change because it always talks to the same facade. When the last route leaves the monolith, it is left empty and can be shut down.

Advantages over the big bang:

  • Continuous delivery of value: each extraction is an increment that already delivers.
  • Bounded risk: if a new service fails, redirect that route back to the monolith.
  • No freeze: the monolith keeps evolving in the parts not yet migrated.

  1. Identifying the monolith's seams

A seam (a term from Michael Feathers) is a point in the system where you can alter behavior without editing the code at that location. To extract a microservice you need to find clean seams that delimit a business capability. The best candidates to leave first are modules that meet:

Criterion Why it makes extraction easier
Low coupling with the rest Few dependencies to break when separating it
High internal cohesion Forms a complete business capability on its own
Relatively independent data Its database is not tangled up with every table
High value or high change Justifies the effort: either it scales differently, or it changes a lot
Manageable risk If it fails, it does not bring down the whole business

The worst candidate to start with is the central module that touches everything (for example, "Customer" in many companies). Start with the petals, not the heart.

// BEFORE: the monolith calls its internal class directly
public class OrderProcessor {
    private final ShippingCalculator calculator = new ShippingCalculator(); // rigid dependency

    public Order process(Order o) {
        double cost = calculator.calculate(o.getDestination(), o.getWeight());
        o.setShippingCost(cost);
        return o;
    }
}

Here there is no seam: OrderProcessor directly instantiates ShippingCalculator with new. To be able to replace the shipping calculation with a remote service, we first have to create a seam. That leads us to the next technique.

  1. Branch by Abstraction

Branch by Abstraction is a technique for making a large change in small steps without using long-lived branches in version control. The idea: you introduce an abstraction (an interface) between the consumer and the current implementation; then you add a second implementation behind the same abstraction; finally you switch from one to the other and delete the old one.

// STEP 1: extract an abstraction (interface) over the capability
public interface ShippingService {
    double calculate(Address destination, double weight);
}

// STEP 2: the current (legacy) implementation satisfies the interface, without changing its logic
public class LocalShippingService implements ShippingService {
    private final ShippingCalculator calculator = new ShippingCalculator();
    public double calculate(Address destination, double weight) {
        return calculator.calculate(destination, weight);
    }
}

// STEP 3: the consumer depends on the abstraction, not on the concrete class
public class OrderProcessor {
    private final ShippingService shipping; // injected
    public OrderProcessor(ShippingService shipping) { this.shipping = shipping; }

    public Order process(Order o) {
        o.setShippingCost(shipping.calculate(o.getDestination(), o.getWeight()));
        return o;
    }
}

Let's go step by step through why this enables the migration:

  • Step 1 creates the "branch by abstraction": ShippingService is now the switch point. All the difference between the old and the new world will pass through this interface.
  • Step 2 wraps the existing logic without touching it. The system still behaves the same; we have only interposed a thin layer.
  • Step 3 inverts the dependency: OrderProcessor no longer knows how shipping is calculated, only that a ShippingService exists.

Now we can add a new implementation that calls the microservice over the network:

// STEP 4: new implementation that delegates to the remote microservice
public class RemoteShippingService implements ShippingService {
    private final HttpClient client;
    public RemoteShippingService(HttpClient client) { this.client = client; }

    public double calculate(Address destination, double weight) {
        // HTTP call to the new shipping service
        return client.post("/shipping/calculate",
                Map.of("destination", destination, "weight", weight), Double.class);
    }
}

With a feature flag you decide which implementation is injected. You can switch it for 1% of the traffic, observe, and gradually ramp up. If something goes wrong, you fall back to LocalShippingService instantly. When 100% of the traffic uses the remote version for a reasonable period, you delete the local implementation. The "branch" has been merged without drama.

  1. Anti-Corruption Layer (ACL)

The legacy monolith usually has an old data model, with confusing names, fields reused for several purposes, and concepts that no longer fit the current business. If the new microservice adopts that model as-is, it inherits the corruption. The Anti-Corruption Layer (a term from Eric Evans in DDD) is a translation layer that isolates the clean model of the new service from the dirty model of the legacy.

graph LR
    MSnuevo[New service\nclean model] --> ACL[Anti-Corruption Layer\ntranslation]
    ACL --> Legado[Monolith / legacy system\nold model]
// Clean model of the new service
public record Customer(String id, String fullName, LoyaltyLevel level) {}

// The ACL translates from the legacy format to the clean model
public class CustomerAcl {

    private final LegacyCustomerApi legacy;
    public CustomerAcl(LegacyCustomerApi legacy) { this.legacy = legacy; }

    public Customer get(String id) {
        // The legacy returns a map with cryptic names: CL_NOM, CL_AP1, CL_TIP
        Map<String,String> raw = legacy.fetch(id);
        String name = raw.get("CL_NOM") + " " + raw.get("CL_AP1");
        // "TIP=3" in the legacy means VIP customer: we translate to our enum
        LoyaltyLevel level = "3".equals(raw.get("CL_TIP"))
                ? LoyaltyLevel.VIP : LoyaltyLevel.STANDARD;
        return new Customer(id, name.trim(), level);
    }
}

What we achieve with this ACL:

  • The new service never sees CL_NOM or CL_TIP. It works with Customer, fullName, and a clear LoyaltyLevel enum.
  • All the legacy's "ugliness" is encapsulated in a single place. If the legacy changes, you only adjust the ACL.
  • It is a natural deletion point: when the legacy dies, you remove the ACL and the clean model survives intact.

The ACL has a cost (code and translation latency), but it protects the investment in the new model. It is almost mandatory when integrating with legacy systems you do not control.

  1. Step-by-step incremental strategy

A typical, safe sequence to strangle a monolith:

  1. Put a facade in front of the monolith (gateway/proxy). For now it forwards 100% to the monolith. Nothing changes functionally, but it creates the switch point.
  2. Add observability: logs, metrics, and distributed traces. You cannot migrate blindly; you need to measure latency and errors before and after.
  3. Choose the first capability according to the seam criteria (low coupling, separable data).
  4. Apply Branch by Abstraction inside the monolith for that capability.
  5. Build the microservice with its own database. If it touches legacy data, separate the data (replication or temporary dual writes).
  6. Put an ACL in place if the legacy model contaminates.
  7. Switch traffic gradually with feature flags; observe; roll back if needed.
  8. Delete the dead code in the monolith once it is stabilized.
  9. Repeat with the next capability until the monolith is empty.

Data separation is usually the hardest part. Common techniques: read-only views over the monolith's DB at first, then dual writes, and finally the service's own DB as the source of truth.

  1. Risks and how to mitigate them

Risk Mitigation
Transactions that crossed modules are no longer ACID Saga pattern, eventual consistency, and designing compensations
Latency: what was an in-memory call is now network Measure, batch calls, cache; do not extract if chatter is extremely high
Shared database that prevents separation Identify tables by domain, views, progressive dual writes
The "zombie" monolith that never finishes dying Define a progress metric (migrated routes) and a shutdown date
Explosion of operational complexity Invest beforehand in CI/CD, observability, and platform
Poorly chosen boundaries Validate with DDD (Bounded Contexts) before cutting

The most underestimated risk is data consistency: inside the monolith a local transaction solved everything; in a distributed setting you need sagas and must accept eventual consistency. Do not extract a service if its capability participates in transactions tightly coupled with others, unless you are prepared to handle the compensation.

  1. Common Mistakes and Tips

  • Starting with the central module. It is the most entangled. Start with peripheral ones to gain experience and confidence.
  • Migrating without observability. Without metrics and traces you will not know whether the new service is better or worse. Instrument before cutting.
  • Sharing the database "temporarily" forever. A database shared between the monolith and the new service is a distributed microservice with the worst of both worlds. Have a real separation plan.
  • Not using feature flags. Without gradual switching or rollback, every extraction becomes a mini big bang again.
  • Forgetting to delete the old code. If you do not remove the legacy implementation after switching, you accumulate debt and confusion.
  • Tip: treat each extraction as an experiment with a hypothesis ("this service will lower payment latency by 30%"). If it does not hold, consider reverting.

  1. Exercises

Exercise 1. You have an e-commerce monolith with the modules: Catalog, Cart, Payments, Customer, and Recommendations. Which would you extract first and which would you leave until last? Justify it using the seam criteria.

Exercise 2. The "Notifications" module directly instantiates new EmailSender() in 12 places. Describe how you would apply Branch by Abstraction to be able to replace it with a notifications microservice, step by step.

Exercise 3. The new "Billing" service must read customers from the legacy, whose table uses the column EST with values A, B, C meaning active, temporary suspension, and permanent suspension. Explain which piece you would use and why, and sketch the translation.

Solutions

Solution 1. First Recommendations: low coupling (it is an add-on), relatively independent data, and if it fails it does not break the purchase. Then Catalog or Payments. Customer last: it is the central module, almost everything depends on it, and separating its data is the riskiest part.

Solution 2. (1) Create the NotificationService interface. (2) Implement it with LocalNotifications that wraps the current EmailSender without changing its logic. (3) Replace the 12 instantiations with injection of NotificationService. (4) Create RemoteNotifications that calls the microservice over the network. (5) Switch gradually with a feature flag. (6) Delete LocalNotifications and EmailSender when 100% uses the remote version stably.

Solution 3. An Anti-Corruption Layer, so that the Billing service does not inherit the cryptic column EST. The ACL translates: ACustomerStatus.ACTIVE, BCustomerStatus.TEMPORARY_SUSPENSION, CCustomerStatus.PERMANENT_SUSPENSION, exposing a clear enum to the clean model. If tomorrow the legacy changes the codes, only the ACL is touched.

  1. Conclusion

Migrating from monolith to microservices is not a leap, but a trickle. The Strangler Fig pattern keeps the system alive and in production while capabilities are extracted from it one by one through a routing facade. We have seen how to identify clean seams, how to create them with Branch by Abstraction and feature flags, how to protect the new model with an Anti-Corruption Layer, and what the risks are—especially those of data and consistency—that must be anticipated. Any incremental change of this magnitude generates many decisions that are worth recording and justifying so that the team neither forgets nor repeats them. That is precisely what the next lesson is about: Architectural Governance and Architecture Decision Records (ADR), where we will learn to document and govern architecture decisions in a lightweight and sustainable way.

Application Architecture Course

Module 1: Fundamentals of Application Architecture

Module 2: Design Principles and Tactics

Module 3: Architectural Styles and Patterns

Module 4: Distributed Architectures and Microservices

Module 5: Event-Driven Architectures and Messaging

Module 6: Domain-Driven Design (DDD)

Module 7: Data and Persistence

Module 8: Cloud Architecture and Deployment

Module 9: Quality, Security and Observability

Module 10: Evolution, Governance and Case Studies

© Copyright 2026. All rights reserved