Skip to content
Engineering

Migrating a Monolith to Microservices Without Stopping Feature Delivery

A practical playbook for incremental service extraction — and an honest list of situations where breaking up the monolith is the wrong call entirely.

5 min readMarch 5, 2025
Share
Migrating a Monolith to Microservices Without Stopping Feature Delivery

Every monolith-to-microservices migration proposal comes with a slide showing the target architecture. Almost none come with a plan for what the team ships in the six months it takes to get there. That gap is where most of these migrations either stall indefinitely or blow through their budget — not because the target architecture was wrong, but because nobody planned how to keep shipping features on a codebase that's being cut apart underneath them.

Start by asking whether you actually have a monolith problem

Before any extraction plan, it's worth being honest about what's actually causing pain. We've inherited migrations that were solving the wrong problem. Symptoms that genuinely point to a monolith architecture issue:

  • Deploy coupling — a change to the reporting module requires redeploying and re-testing the entire application, so release cadence is bottlenecked by the riskiest part of the system.
  • Scaling mismatch — one component (say, image processing) needs 10x the compute of everything else, but you're scaling the whole monolith to feed it.
  • Team contention — multiple teams are stepping on the same codebase and merge conflicts or shared-state bugs are a weekly occurrence.

Symptoms that look like a monolith problem but usually aren't: slow builds (often fixable with better build tooling or modularization within the monolith), unclear ownership (an org chart problem, not an architecture one), and general "this code is messy" complaints (a refactoring problem, not a distribution problem). We've talked at least two clients a year out of a full microservices migration once we dug into what was actually slowing them down — a modular monolith with clear internal boundaries solved it at a fraction of the operational cost.

Extract along seams that already exist

The strangler fig pattern works, but only if you pick extraction candidates that already have a relatively clean boundary. Trying to carve out a service whose logic is tangled through five other modules turns a 6-week extraction into a 6-month one.

We look for three signals when picking the first (and second) service to extract:

  1. A bounded data model — the module's data doesn't get directly queried or joined against by unrelated parts of the codebase.
  2. A stable interface — the module's public surface (its function signatures or API calls) hasn't changed much recently, meaning less risk the extraction fight is happening on moving ground.
  3. Independent scaling or release needs — the module actually benefits from being deployed and scaled separately, which is what justifies the operational overhead of a new service in the first place.

Notification/email sending, PDF or report generation, and search indexing are common early extraction candidates because they tend to satisfy all three. Core domain logic like order processing or account management almost never should go first — the boundaries are usually still fuzzy and the blast radius of getting it wrong is highest.

Keep the strangler fig routing dumb

The routing layer that decides whether a request goes to the old monolith path or the new service should be as simple as possible — a feature flag or a router rule, not new business logic. We've seen teams build clever adaptive routing that itself becomes a maintenance burden and a source of bugs. Simple, reversible routing beats clever routing every time during a migration.

Keep two versions of truth in sync during the transition

The hardest part of a live migration isn't the code extraction, it's data consistency during the period when both the old and new paths can be hit. Two approaches that work in practice:

  • Dual writes with reconciliation. Write to both the monolith's database and the new service's store, and run a nightly job comparing the two, alerting on drift. This catches bugs in the new path before it's trusted as the source of truth.
  • Change data capture (CDC). Stream changes from the monolith's database (via something like Debezium) into the new service, so the new service stays eventually consistent without application-level dual writes. This scales better for high-volume tables but adds operational complexity of running a CDC pipeline.

Whichever approach, define explicitly which system is the source of truth at each stage, and don't flip that switch until reconciliation has run clean for a meaningful period — we typically want at least two full weeks of zero unexplained drift before cutting traffic over for anything touching money or compliance-sensitive data.

Protect feature delivery with a parallel-track team split

The organizational mistake that kills feature velocity during migration is pulling the whole team onto the extraction for a "quick" few sprints. It's never quick, and feature work stalls, which erodes stakeholder confidence in the migration itself.

Instead, split the team: a rotating pair works the extraction while the majority stays on feature delivery, with the extraction pair rotating every 2-3 sprints so migration knowledge doesn't concentrate in one or two people who then become a bottleneck. This is slower in wall-clock time than an all-hands push, but it's the difference between a migration that finishes and one that gets shelved when the business asks why nothing new has shipped in a quarter.

Getting this balance right — enough focus to make real progress, not so much that the product roadmap stalls — is one of the more common reasons clients bring in a dedicated team specifically for the migration track, leaving the core team free to keep shipping against the existing roadmap.

Huy Hoang

Principal Engineer, Backend