Skip to main content

Migration Guide

Monolith to microservices — pick the boring service first.

The pattern is well known. What actually determines whether a split succeeds is choosing the right first service, building the anti-corruption layer before the traffic, and having the observability in place before anything breaks — not after.

This is a technical playbook for one architectural decision. For the commercial shape of modernizing a legacy estate, see Legacy System Modernization. For the frontend half of the same kind of estate, see JSP to React.

How splits actually fail

Four ways a split sinks the program.

The pattern itself is not the risk. Skipping one of these four steps to move faster is where the risk actually comes from.

01

The most exciting service goes first

Pricing or checkout gets extracted first because that's where the interesting engineering is. It's also the highest blast radius — the exact opposite of where a first attempt should land.

02

No anti-corruption layer, so coupling just moves

The new service calls the monolith's tables directly "to save time." The extraction happens on paper; in practice, the two are still one system with two deploy pipelines.

03

The dual-write trap

A request writes to the monolith's database and the new service's database in the same call. One write fails, the other commits, and the two stores drift with no record of when.

04

No observability before the split

Distributed tracing gets added after the first cross-service incident, which is also the first time anyone can see how a request actually flows through both systems.

05

The contract is a verbal agreement between two teams

One team changes a field name in good faith, the other finds out when their service starts throwing errors in production. A contract nobody tests in CI isn't a contract.

The first extraction

Choose the service that proves the pattern, not the one everyone wants.

01

Low fan-in, low fan-out

Few other modules call it, and it calls few others. That's the boundary where a real seam already exists in the code — not one you'd have to invent.

02

A bounded context with its own vocabulary

A part of the domain the business already talks about in its own terms, without three other modules overlapping the same nouns and verbs.

03

Tolerates degraded failure

If it goes down for an hour, browse, cart, and checkout keep working. Save checkout-critical services for after the pattern is proven, not before.

04

Specifically not the most exciting one

Pricing and checkout are where the interesting problems live — and where a first mistake costs the most. Extract something boring first: returns, reviews, notifications.

The most exciting service to extract is rarely the safest one to extract first. Pick the boring one, prove the pattern, then earn the interesting one.

What the first slice looks like

One service, extracted behind a real boundary.

The new service never reads the monolith's tables directly, and the monolith never calls the new service's internals directly. CDC keeps the service current; the anti-corruption layer keeps both models honest.

First-service extractionCDC feeding the new service
MONOLITHorders, catalog,returns, pricingsingle deployCDC STREAMcaptures DB changes as eventsANTI-CORRUPTIONLAYERtranslates monolith model→ service's own modelRETURNS SERVICEowns its own schemaown deploy, ownon-call rotationchange eventsreads & writestranslated calls

Why not just query the monolith's DB

A direct query looks faster to build and is the fastest way to re-couple two systems that were supposed to be independent. CDC and an anti-corruption layer cost more upfront and are the entire reason the split holds under change.

Log-based change-data-capture, the approach Debezium documents in depth, is what makes this reliable — the stream comes from the database's write-ahead log, not from a second write the application has to remember to make.

Distributed transactions

There is no rollback. There is only the saga.

01

Choreography for short, simple flows

Each service publishes an event, the next reacts. No central coordinator — but the flow gets hard to trace past three or four steps, and harder to change safely.

02

Orchestration once branching gets real

A saga orchestrator owns the sequence explicitly, including timeouts and compensations. Easier to reason about once refunds, partial fulfillment, or retries enter the picture.

03

Compensating transactions, not rollbacks

A distributed transaction can't roll back. Each step needs an explicit undo — refund the charge, restock the item, cancel the shipment — written and tested like the forward path.

04

Idempotency keys on every step

A retried message has to produce the same result as the first attempt. Without an idempotency key, a network retry can double-charge a customer or double-ship an order.

Two writes to two stores is not synchronization. It's two chances for the second write to fail silently.

Microsoft's Saga pattern reference and anti-corruption layer pattern are the two documents worth reading before the first line of a service boundary gets written.

Contract discipline

A schema change is a breaking change until proven otherwise.

01

Consumer-driven contracts, tested in CI

The consumer defines what it needs from a provider's API. That contract runs in the provider's own test suite, so a breaking change fails in CI — not in production.

02

Additive-only changes on a stable version

New fields and optional parameters are fine. Removing or renaming a field is a breaking change, and it gets a new version — never a silent patch to the old one.

03

Deprecation windows measured, not implied

An old contract version stays live for a fixed, published window after the new one ships. No consumer gets broken by surprise on a date nobody told them about.

04

Schema registry as the source of truth

Event schemas live in a registry every producer and consumer validates against — not in whatever the last engineer who touched the payload remembers about its shape.

Observability comes first

Wire the tracing before there's anything to trace.

01

Distributed tracing wired before the first extraction

A request's path across services has to be traceable end to end before there are multiple services to trace — or the first production incident is where the gap gets found.

02

Correlation IDs on every request and every event

One ID follows a request from the edge through every service and every async event it triggers, so a failure four hops in traces straight back to what started it.

03

Structured logs with tenant, request, and trace IDs

Free-text logs across five services aren't searchable. Structured, correlated logs are the only way an on-call engineer finds the failure in minutes instead of hours.

04

SLOs on today's monolith, as the baseline

Measure the monolith's current latency and error rate before splitting anything. That number is the bar the new microservice isn't allowed to regress below.

On a national gifting brand's estate, backend services moved to Spring Boot microservices incrementally, with the legacy platform staying live for every surface not yet migrated — no big-bang cutover, no window where observability had to catch up after the fact.

What nobody budgets for

The cost isn't the build. It's running it after.

Each new service is its own deploy pipeline, its own on-call rotation, and its own set of failure modes to reason about — multiplied by however many services the monolith eventually becomes.

Calls that used to be in-process function calls are now network calls that can time out, retry, or fail partially. Every one of them needs a timeout, a retry policy, and a fallback that someone actually tested.

The team that owned "the app" now owns a distributed system. That is a different job, with different skills, and it has to be staffed and budgeted as one — not absorbed as a side effect of the split.

None of this is a reason to avoid the split when the split is actually warranted. It is the reason the decision belongs to engineering leadership and not to whichever team is most tired of working in the monolith this quarter.

Side by side

What actually changes on both sides of the split.

DimensionMonolithMicroservices
DeploymentOne build, one deploy, one rollback.Independent deploys per service — faster for the owning team, harder to reason about system-wide.
Failure isolationA bug in one module can take the whole process down.A failing service degrades its own capability, if the calling code handles the failure — otherwise it cascades.
Data consistencyOne database, one transaction, strong consistency by default.Consistency across services is eventual, coordinated with sagas and CDC — a real engineering cost, not a footnote.
Team autonomyEvery team shares the same codebase and the same release train.Teams own their service end to end, from schema to deploy — genuine autonomy, if the contracts hold.
Debugging complexityA stack trace shows the whole call path in one process.A failure spans services. Distributed tracing isn't optional — it's the only way to see the call path at all.
Operational overheadOne thing to deploy, monitor, and keep patched.Each service is its own deploy pipeline, on-call surface, and failure mode — a cost the team has to staff for.

The honest take

When to extract — and when the monolith is right.

We build microservices when a monolith genuinely can't do what the business needs. We also tell buyers when a monolith is still the right call — the question is which constraint is real.

When to extract

  • Team ownership keeps colliding on the same codebase

    Two teams shipping to the same module, blocking each other's release train, week after week.

  • One module's cadence is blocked by an unrelated one

    A fast-moving capability is stuck waiting on a slow-moving module's release process for no domain reason.

  • A capability needs to scale independently

    Search under ten times the load of checkout is a real, specific reason to isolate — not a general instinct that microservices are more modern.

When the monolith is right

  • The team is small enough that one codebase is faster

    Splitting adds coordination overhead a five-person team doesn't have the headcount to absorb yet.

  • Domain boundaries aren't clear yet

    Splitting early locks in a boundary that's expensive to undo. A wrong seam costs more than staying whole a while longer.

  • The operational maturity isn't there

    No tracing, no per-service on-call, no CI/CD pattern proven yet. Add those first, or the split just adds risk without adding capability.

Not sure which side you're on? See Replatform vs Rebuild.
A monolith split before it needs to be is a distributed system with a monolith's problems and none of a monolith's simplicity.

FAQ

Questions engineering leads ask before the first extraction.

Q

How do we choose which service to extract first?

Low fan-in and fan-out, a bounded context with its own vocabulary, tolerance for degraded failure, and — deliberately — not the most exciting one. Pricing and checkout come later, once the pattern is proven on something boring.

Q

What's the dual-write trap, exactly?

Writing to the monolith's database and the new service's database in the same request, hoping both succeed. One write can fail silently while the other commits, and the two stores drift with no record of when or why.

Q

Do we need change-data-capture on day one?

Yes, if the new service needs to stay current with monolith data it doesn't own. CDC streams the monolith's writes as events, so the new service reads without ever querying the monolith's database directly.

Q

How much observability is actually enough before splitting?

Distributed tracing, correlation IDs, and structured logs with request and trace IDs — wired and tested before the first service goes live, not added after the first incident makes the gap obvious.

Q

What's the operational cost nobody budgets for?

Each new service is its own deploy pipeline, its own on-call rotation, and its own set of failure modes to reason about. That's staffing and process cost, not just infrastructure spend.

Q

When should we just stay a monolith?

When team size, domain clarity, or operational maturity aren't there yet. Splitting before any of those three are ready locks in a boundary that's expensive to undo and adds distributed-systems complexity nobody is staffed to run.

How an engagement starts

Three steps to a partnership

01

Intake call

30 minutes. We listen, you talk. No deck.

02

Diagnostic

We audit the surface, name the bottleneck, propose a path.

03

Kickoff

Senior engineer in your standup by week two.

Extract one service, prove it, then go further

Tell us what the monolith runs today. We'll come back with the first service to extract and the boundary it needs.