Breaking the Monolith
The question isn’t whether microservices are better than monoliths. The question is whether your team is paying a cost that microservices…
Breaking the Monolith
The question isn’t whether microservices are better than monoliths. The question is whether your team is paying a cost that microservices would actually reduce.

There’s a migration I was part of that I think about often — not because it was dramatic, but because it wasn’t. No big rewrite, no “we’re shutting down the monolith on Friday.” Just a gradual, deliberate process of extracting pieces, validating them, and moving on. By the time we were done, the system looked completely different. The business barely noticed.
That outcome was not an accident. It was the result of planning the migration before writing a single line of extraction code — and resisting the urge to do everything at once.
This article is about how to get there. When to stay with the monolith, when to leave it, and how to execute the transition without turning your team’s life into a year-long incident.
The monolith isn’t the problem
Before talking about how to decompose, it’s worth being honest about why you’d want to.
Monoliths get a bad reputation that they often don’t deserve. A well-structured monolith is easier to develop, easier to test, easier to deploy, and easier to debug than a distributed system of equivalent complexity. The operational overhead of microservices — service discovery, distributed tracing, network failures between services, independent deployment pipelines for each service — is real and significant. You don’t take it on for free.
A monolith becomes a problem when it accumulates a specific kind of friction that the team can’t absorb anymore. Not “the codebase is large” — large codebases are fine if they’re well-organized. The problem is when the friction starts costing more than the alternative.
These are the signals I’ve seen and lived:
Deploy fear. Nobody wants to ship on Fridays. Every PR has a comment asking “did you check if this touches the payment flow?” The CI pipeline takes forty minutes and still doesn’t catch everything. Deploys go out and things break in unrelated areas. The team starts batching changes together to reduce the number of deploys, which makes each deploy larger and riskier.
Onboarding drag. A new engineer joins the team and needs three weeks just to understand what the codebase does before they can contribute anything useful. Not because they’re slow — because the system doesn’t have clear boundaries and everything is connected to everything else.
Team collision. Multiple squads working in the same codebase create constant merge conflicts, dependency disputes, and implicit coordination overhead. One team’s release blocks another’s. Decisions about shared code become political.
Nobody understands the whole thing. The engineers who built it have moved on. The ones who remain know their corner of the system well and avoid the rest. When something breaks in an unfamiliar area, the response is caution and guesswork rather than confidence.
If your monolith doesn’t have these problems, stop reading and go ship something useful. The monolith is working. Microservices would add complexity you don’t need.
If you recognize two or more of these, keep reading.
What you’re actually solving
The failure mode I see most often in microservices migrations is teams that extract services without clarity on what problem they’re solving.
They extract the user service because it seems self-contained. They extract the notification service because it’s annoying to have in the monolith. They extract the reporting service because someone read a blog post. Six months later they have eight services, a distributed monolith where services are tightly coupled through synchronous API calls, and all the operational complexity of microservices with none of the benefits.
Before you extract anything, you need a clear answer to: what is this migration supposed to make better?
The honest answers are usually:
- Independent deployability — teams should be able to ship without coordinating with other teams
- Independent scalability — some parts of the system have dramatically different load profiles
- Fault isolation — a failure in one area shouldn’t bring down unrelated functionality
- Team ownership — squads should have clear boundaries and full ownership of their domain
Notice what’s not on the list: “because microservices are the industry standard,” “because our investors expect it,” or “because the monolith feels bad.” Those aren’t engineering problems, they’re not worth the cost of migration.
Get specific. Write down the two or three concrete things that should be measurably better after the migration. Those become your success criteria and your filter for every extraction decision you’ll make.
Map the domains before you touch the code
The most expensive mistake in a microservices migration is drawing service boundaries in the wrong place. Once you’ve extracted a service and built things around it, the boundary calcifies. Moving it later is much harder than getting it right the first time.
You don’t need to formally adopt Domain-Driven Design to do this well — but DDD gives you useful vocabulary. A bounded context is a part of the system with its own model, its own language, and its own rules that don’t need to be shared with the rest. The important thing isn’t the terminology. It’s the practice of identifying which parts of the system have natural ownership, independent state, and concepts that mean different things elsewhere. Finding those boundaries in your monolith tells you where the natural service cuts are.
The practical exercise: get your team in a room (or a shared doc) and map out the major domains of the system. For each domain, ask:
- Does this domain have concepts that mean different things in other domains? (A “product” in the catalog service means something different than a “product” in the order service — that’s a boundary.)
- Can this domain’s data change without other domains needing to know immediately?
- Does this domain have a natural owner — a team or squad that would make sense to put in charge of it?
- If this domain went down, would it take unrelated functionality with it?
This mapping exercise usually takes a few sessions and surfaces a lot of disagreement. That disagreement is valuable — it means you’re finding the places where implicit assumptions were never made explicit. Work through it before you write code.
A rough target for service size: if you can’t describe what a service does in one sentence, it’s probably too large. If a single engineer can’t hold the service’s logic in their head, it might be too large. If the service only does one thing and that thing is trivial, it might be too small. There’s no formula — the right size is the one that gives the owning team clear ownership and independent deployability.
The Strangler Fig: how to actually execute the migration
The Strangler Fig pattern, described by Martin Fowler, is the closest thing to a standard approach for this kind of migration — and it works because it doesn’t require you to stop the world.
The core idea: instead of rewriting the monolith, you build new functionality outside of it and gradually route traffic away from the existing implementation. Over time, the monolith gets “strangled” — it shrinks as functionality is extracted, until what remains can either be shut down or left as a smaller, more focused service.
In practice, this looks like:
Step 1: Put a façade in front of the monolith. An API gateway or reverse proxy that sits between clients and the system. Initially it routes everything to the monolith. This gives you the ability to redirect specific routes to new services without clients knowing anything changed.
Step 2: Identify the first extraction candidate. Choose a domain that is:
- Well-understood — you know exactly what it does and what it touches
- Low-risk — if it breaks, the blast radius is contained
- High-value — extracting it will meaningfully reduce the friction you’re trying to solve
Good first candidates: authentication, notification, reporting, file storage. Bad first candidates: the core business logic, anything that touches the main transaction flow, anything that’s deeply entangled with everything else.
Step 3: Extract, validate, redirect. Build the new service. Run it in parallel with the monolith, handling the same requests. Validate that the outputs match. Then gradually redirect traffic to the new service — start with a small percentage, increase as confidence grows, eventually shut off the monolith path.
Step 4: Repeat. Each extraction reduces the monolith. Each successful extraction builds the team’s confidence and sharpens the process for the next one.
The key discipline: one extraction at a time. The temptation to parallelize — to run three extractions simultaneously because you have the team capacity — is real and dangerous. Parallel extractions multiply complexity, make it harder to isolate problems, and create dependencies between migrations that block each other. Finish one before starting the next.
In the migration I was part of, the first extraction looked straightforward on paper — a service with a clear boundary, minimal dependencies, low traffic. In practice, it surfaced three assumptions nobody had written down: implicit dependencies on shared utilities, a database query that joined across what should have been a boundary, and a configuration pattern that assumed everything ran in the same process. None of these were blockers. All of them were discoveries we were glad to make on a low-risk service rather than something critical. That’s exactly the point of starting small.
Separating the database is the real work
Here’s something most migration articles understate: extracting the code is the easy part. Separating the data is where migrations actually get hard.
A monolith typically has one database, and the tables are coupled in ways that aren’t always obvious from the code. A query that joins three tables across what should be two service boundaries is a simple SQL query in the monolith and a distributed data problem in a microservices world.
The database separation has to happen — without it, you don’t have real service independence, you have services that share a database and are just as coupled as before, with all the operational overhead of distributed systems added on top.
The approach that works: database-per-service, migrated gradually.
Start by identifying which tables belong to which domain. Some will be obvious. Some will be shared — and those shared tables are the places where you need to make hard decisions about ownership. For each shared table, one service will own it and others will access the data through that service’s API, not directly through the database.
Before extracting a service, move its tables to a separate schema within the same database. The service queries its schema; other code in the monolith that needs that data starts going through the service’s interface instead. This is the intermediate state — the data is logically separated even if it’s still physically in the same database. Once no monolith code is directly querying those tables, you can move them to a separate database with confidence.
This process is slow and unsexy. It also prevents the class of incidents where you’ve extracted a service but it’s sharing database connections with the monolith and a slow query in one brings down the other.
Practical planning — what to actually do first
If you’re a tech lead starting this process, here’s the order that tends to work:
Weeks 1–2: Document what exists. Service map the monolith — draw every major domain, every significant dependency between them. This is often uncomfortable because it makes visible how entangled things actually are. Do it anyway.
Weeks 3–4: Define success. What are the two or three things that should be measurably better after this migration? Write them down. Get your manager and stakeholders to agree. These become your north star.
Weeks 5–6: Choose the first extraction. Apply the criteria above — well-understood, low-risk, high-value. Write a one-pager on the proposed service: what it does, what it owns, what its API surface looks like, what data it needs to own.
Week 7+: Build the façade, then extract. Don’t skip the façade step even if it feels like overhead. It’s what gives you the ability to redirect traffic gradually and safely.
Throughout: Resist scope creep. Every extraction will surface things you want to fix while you’re in there. Resist the urge. Extract the service as it exists, with as few changes as possible, and make improvements after it’s running independently. Mixing extraction with refactoring doubles the risk and halves your ability to diagnose problems.
The observability tax you have to pay upfront
There’s a cost to microservices that migration articles consistently underplay: debugging across service boundaries is genuinely harder than debugging inside a monolith, and if you don’t invest in observability before you need it, you’ll feel that cost at the worst possible moment — during a production incident.
In a monolith, a request leaves a single trace in a single log file. In a distributed system, that same request might touch five services, and each service writes its own logs in its own format with no common thread connecting them. Without the right infrastructure, tracing a bug across services means jumping between dashboards, grep-ing through separate log streams, and guessing which service introduced the error.
Three things you need in place before you extract the first service:
Correlation IDs. Every request that enters your system gets a unique ID that propagates through every downstream call. Every log line includes that ID. When something breaks, you can filter all logs across all services to a single request in seconds. Without this, you’re reconstructing timelines manually.
Structured, centralized logging. Logs as JSON, aggregated in one place (Datadog, ELK, CloudWatch — the specific tool matters less than the discipline). Each log entry includes the service name, the correlation ID, the timestamp, and enough context to understand what happened without reading source code.
Distributed tracing. Tools like Jaeger or Zipkin (or the tracing built into Datadog and similar platforms) give you a visual representation of a request’s path through your services — which service called which, how long each step took, where failures occurred. This is essential for diagnosing latency problems that appear at the system level but originate in a specific service.
These aren’t optional polish. They’re the foundation that makes a distributed system debuggable. Budget time for them in your migration plan, not as a future improvement.
What microservices don’t fix
A few things worth being honest about before you start:
Microservices don’t fix bad code. An extracted service built on tangled logic is just tangled logic running somewhere else. If your monolith has deep structural problems, extraction will make them more visible, not less.
Microservices don’t fix team problems. If the friction in your organization is cultural — poor communication, unclear ownership, inability to make decisions — distributing the codebase will distribute the problem, not solve it.
Microservices don’t reduce complexity. They trade one kind of complexity (a large codebase) for another (distributed systems). The second kind is often harder to debug and more expensive to operate. You should be making this trade because the benefits outweigh the costs for your specific situation — not because distributed systems are inherently better.
The teams I’ve seen execute this well had one thing in common: they were solving a specific problem that microservices would actually address, and they could articulate that problem clearly before writing a single line of migration code.
One last thing
The migration I was part of that worked — it worked because we decided early that we were going to do it slowly, on purpose, without a deadline for when the monolith had to be gone. We extracted one service, learned from it, and extracted the next. We made mistakes, but we made them one at a time and fixed them before moving on.
That patience is harder than it sounds when there’s organizational pressure to “finish the migration” or when the monolith is visibly painful every day. But it’s the approach that gets you to a distributed system that actually works — rather than a distributed monolith that has all the problems of the original plus new ones you didn’t anticipate.
Start small. Stay focused. Finish what you start before starting the next thing.
The monolith will wait. What it won’t do is get easier to leave the longer you put off the plan.
· · ·
In the middle of a migration, or trying to make the case for one? Drop your situation in the comments — especially if you’re stuck on the database separation part.
메타데이터
- post_id
- d0e83bd16cdc
- slug
- breaking-the-monolith-d0e83bd16cdc
- url
- https://medium.com/@tamagno/breaking-the-monolith-d0e83bd16cdc
- canonical_url
- https://medium.com/@tamagno/breaking-the-monolith-d0e83bd16cdc
- author_url
- https://medium.com/@tamagno
- status
- ok
- fetched_at
- 2026-06-16 19:09:56