Stackbook Logo
← Back to Architectural Thinking

The Evolution From Monolith to Microservices

How systems grow from a single deployable into services: the forces that justify a split, safe migration patterns, and the costs to expect.

4 min read

Almost every successful system starts as a monolith, and that is a good thing. A single deployable is easy to build, debug, and reason about. Microservices are not a destination; they are a response to specific pressures that appear as a product and organization grow. Understanding those pressures, and how to migrate without a risky rewrite, is what separates a healthy evolution from an expensive one.

Why Monoliths Are the Right Start

In the early days, the domain is unclear and boundaries are guesses. In a monolith, getting a boundary wrong means moving a function. In a distributed system, it means rewriting APIs, migrating data, and coordinating deployments. Early on you also lack the traffic, team size, and operational tooling that make services pay off. Starting with a well-structured monolith, ideally modular, lets you learn the domain cheaply.

Signs It Is Time to Split

Split because of pain you can name, not because of fashion. Common legitimate triggers:

  • Deploy contention: many teams share one release train, and a small change waits behind everyone else's.
  • Uneven scaling: one component needs forty instances while the rest need two, and scaling the whole app wastes money.
  • Fault isolation: a memory leak in a minor feature takes down checkout.
  • Technology fit: one capability, such as search or media processing, genuinely needs a different runtime or datastore.
  • Team autonomy: teams cannot make progress without constant cross-team coordination.

If your pain is "the code is messy", services will not fix it. They will distribute the mess across a network.

Prepare Inside the Monolith First

The most valuable work happens before any service exists. Turn the monolith into a modular monolith:

  1. Identify bounded contexts using domain-driven design, such as billing, catalog, and identity.
  2. Enforce module boundaries with dependency rules and architecture tests.
  3. Remove cross-module database joins and foreign keys, giving each module ownership of its tables.
  4. Replace direct calls across modules with explicit interfaces or in-process events.

If you cannot separate modules inside one process, you will not separate them across processes.

The Strangler Fig Migration

Rather than rewriting, grow the new system around the old one and retire pieces gradually, a pattern named by Martin Fowler after strangler figs that overgrow their host trees.

          +-------------------+
Client -> |  Routing layer    |
          |  (gateway/proxy)  |
          +----+---------+----+
               |         |
        old paths     migrated paths
               |         |
        +------v--+   +--v----------+
        | Monolith|   | New service |
        +---------+   +-------------+

The sequence for each extraction:

  1. Pick a capability with clear boundaries and low coupling, ideally one that benefits most from independence.
  2. Put a routing layer in front, so traffic can be shifted per route.
  3. Build the new service and dual-run it: shadow traffic or compare responses before cutting over.
  4. Migrate data, using change data capture or dual writes with reconciliation, and establish the service as the single source of truth.
  5. Shift traffic gradually, keep the old path as a rollback, then delete it.

Extract the edges first, such as notifications or reporting, before the deeply entangled core. Early wins build confidence and reveal operational gaps while the stakes are low.

The Costs You Are Signing Up For

Microservices trade code complexity for operational and distributed-systems complexity:

  • Network calls fail, time out, and add latency. You need retries with backoff, timeouts, circuit breakers, and idempotency.
  • Transactions no longer span services. You will use sagas and accept eventual consistency.
  • Debugging needs distributed tracing and centralized logs.
  • Every service needs a pipeline, monitoring, on-call ownership, and security hardening. A platform team and good paved roads become close to mandatory.
  • Data duplication and contract versioning become permanent concerns.

A common anti-pattern is the distributed monolith: services that must be deployed together because they share a database or chatty synchronous calls. You pay all the costs and gain none of the benefits.

Know When to Stop

You do not have to finish. Many mature systems run a healthy core monolith with a handful of services extracted where the pressure was real. Stop splitting when the remaining pain is gone, and be willing to merge services that were split too eagerly.

Key Takeaways

  • Start with a monolith; extract services only in response to named, measurable pain.
  • Build a modular monolith with owned data first, since boundaries must exist before they can be distributed.
  • Migrate incrementally with the strangler pattern, a routing layer, and a rollback path.
  • Budget for the operational cost: observability, resilience patterns, and platform support.
  • Avoid the distributed monolith, and accept that a hybrid end state is perfectly fine.