Engineering

From monolith to modular services without stopping delivery

Hayden JacksonHayden Jackson
9 min read
Software developers working at desks with laptops and monitors showing code in an office

Short answer: You move from a monolith to modular services without stopping delivery by changing the system in small, reversible steps while it keeps running. First, tidy the monolith into clear internal modules. Then put a routing layer in front of it, move one well-bounded capability at a time, release each move behind a feature flag, watch it closely and keep a tested way back. New features keep shipping the whole time.

This guide is for owners and operations leads at small and mid-size businesses whose core software has become hard to change.

Why big-bang rewrites put delivery at risk

The tempting plan is to freeze the old system, rebuild it "properly" and switch over in one weekend. But the business doesn't freeze. Staff still need fixes and customers still need features, so either the rewrite chases a moving target or the business goes months without improvements.

A rewrite also delivers no value until the day it replaces the old system, and that day carries all the risk at once. Martin Fowler describes the alternative: replace the legacy system piece by piece, so that "both investment and returns occur gradually and visibly."

Start with a modular monolith, not a fleet of microservices

Modular services don't have to mean dozens of microservices. Each separately deployed service adds operational cost: deployments, monitoring, network failures and data consistency. Fowler calls this the "microservice premium" and argues it only pays off for systems complex enough to need it.

For many small and mid-size businesses, the best first step is a modular monolith: one deployable application, reorganized so that each business area (orders, billing, scheduling, reporting) lives in its own module with a clear public interface and its own data. Modules talk to each other through those interfaces, not by reaching into each other's tables or internal code.

This step pays off whether or not you ever split anything out:

  • The code gets easier to change, because each module has fewer hidden dependencies.
  • You learn where the real boundaries are before paying for network calls and separate deployments.
  • When a module does need to become its own service (because it scales differently, changes often or needs stricter security), the hard work of untangling it is already done.

Fowler's "monolith first" advice makes the same point: boundaries are hard to get right, and far cheaper to fix inside one codebase than between services.

Find the seams and draw the boundaries

Close-up of a monitor showing source code in a dark editor
Photo by Luis Gomes on Pexels

A seam is a place where you can split the system without breaking what's on either side. In a well-designed system, seams already exist. In most legacy systems you have to find or create them.

Good places to look:

  • Business capabilities. Ask how the business describes its work: quoting, invoicing, inventory, customer accounts. Domain-driven design calls these bounded contexts: areas where words like "customer" have one consistent meaning.
  • Rate of change. Parts of the system that change every month are worth isolating first. Parts that never change can wait.
  • Edges of the system. Notifications, reporting exports, authentication and document generation often have fewer dependencies on the core. In her guide to breaking up a monolith, Zhamak Dehghani recommends starting with simple edge capabilities so the team can practice deploying and operating a new service before tackling the core.

Two patterns help old and new code coexist. Branch by abstraction puts an interface in front of the code you plan to replace, so both implementations can live side by side. An anti-corruption layer translates between the legacy data model and the new one, so old quirks don't leak into new code.

Use the strangler fig pattern to move traffic gradually

The strangler fig pattern takes its name from a vine that grows around a host tree until it can stand on its own. In software, the new system grows around the old one until the old one can be retired.

Microsoft's Azure Architecture Center describes the mechanics:

  1. Put a façade in front of the legacy system. This is usually a reverse proxy, API gateway or routing layer. At first it sends every request to the old system, and users notice nothing.
  2. Move one capability. Build or extract the new version of a single capability, then point the façade's route for that capability to it.
  3. Repeat. Over time, more routes point to new components and fewer to the legacy system.
  4. Retire the old system once nothing depends on it.

The façade is what keeps delivery going. Each step is a routing change, not a cutover, so the team keeps shipping features in both old and new parts. Microsoft and AWS also note when the pattern doesn't fit: if requests to the legacy system can't be intercepted, or the system is small enough to replace outright, a gradual migration adds complexity without much benefit.

Migrate data without a "freeze weekend"

Moving code is the easy part. Data is where most migrations get stuck, because the legacy database is often shared by everything. If the new service keeps reading and writing the old shared tables, you haven't really separated anything. Dehghani's guide warns against moving the façade and the logic while leaving the data behind.

A safer sequence for each capability:

  1. Give the module one owner for its data. Other parts of the system read that data only through the module's interface, not directly from its tables.
  2. Expand. Create the new data store or schema alongside the old one. Nothing is removed yet.
  3. Sync and compare. Copy existing records, then keep the two stores in step, either by writing to both or by capturing changes from the old store. Run regular reconciliation checks: row counts, totals and spot checks of real records.
  4. Migrate readers and writers. Switch consumers to the new store one at a time, behind flags.
  5. Contract. Only when nothing uses the old tables do you stop syncing and remove them.

This is parallel change (expand and contract) applied to data. Each phase can be released and reversed independently, which is what removes the need for a downtime window. Agree on a "source of truth" for each table at each phase, and write it down.

Release with feature flags, not release dates

Feature flags (also called feature toggles) separate deploying code from releasing it. New code goes to production switched off, then gets turned on for internal staff, then a small group of users, then everyone.

In a migration, flags let you:

  • Ship partly finished work without waiting for a big merge.
  • Route a small share of traffic to a new service (a canary release) and compare it with the old path.
  • Turn a new path off in seconds if something looks wrong, with no redeploy.

Pete Hodgson's article on feature toggles stresses that flags have a carrying cost. Treat migration flags as temporary. Give each one an owner and a removal date, and delete it once the new path is fully live. Old flags left in the code become their own kind of legacy.

Build observability before you move anything

Laptop screen showing line and bar charts of performance analytics
Photo by Luke Chesser on Unsplash

You can't move traffic safely if you can't see what it's doing. Before the first route changes, make sure you can answer these questions for both the old path and the new one:

  • How fast are requests, and how many fail?
  • How much traffic is each path handling?
  • Is anything running out of capacity?

Those are the "four golden signals" from Google's Site Reliability Engineering book: latency, traffic, errors and saturation. Add business checks too, such as orders created, invoices sent or appointments booked, because a service can look healthy while quietly doing the wrong thing.

Once requests cross between the monolith and new services, add distributed tracing. OpenTelemetry is an open, vendor-neutral standard for traces, metrics and logs. Capture a baseline from the old system first, or you can't tell whether the new path is better or worse.

Plan rollback for every step

Every step in a gradual migration should have a written answer to the question "how do we undo this?" before it ships.

  • Routing changes roll back by pointing the façade at the old system again.
  • Feature-flagged releases roll back by turning the flag off.
  • Data changes roll back only if the old store is still current. This is why the "contract" phase comes last, and why you keep syncing until you're confident.

Set the rollback triggers in advance, for example an error rate or latency level, or a failed reconciliation check. Then decide who can make the call. Rehearse the rollback at least once in a test environment. A rollback plan that has never been tried is a guess.

A practical sequence for a small or mid-size team

Putting it together, a typical order of work looks like this:

  1. Map the business capabilities and the data each one owns.
  2. Add monitoring, tracing and a performance baseline to the existing system.
  3. Refactor toward a modular monolith, one module at a time.
  4. Put a routing façade in front of the application.
  5. Extract one low-risk edge capability as the first separate service, if a separate service is justified at all.
  6. Migrate its data with expand, sync, migrate and contract.
  7. Release behind a flag, widen gradually, and keep rollback ready.
  8. Remove the old code path and the flag, then pick the next capability.

Feature work continues throughout, so the migration becomes part of normal delivery rather than a project competing with it.

When modular services aren't worth it

Not every system needs to be broken up. If the application is small, changes rarely and works well, a modular monolith on supported, patched technology may be the right end state. The goal is software your business can change safely, not a particular architecture diagram.

Planning a modernization?

If your business depends on software that has become risky or slow to change, BITRIDGE can help you plan a phased modernization that keeps your team shipping. We're based in Albuquerque, NM.

Explore our custom software development services

Share X LinkedIn
Hayden Jackson

Hayden Jackson

Get engineering notes in your inbox

Practical AI, cloud, and security delivery notes - no fluff, no sales spam.