Distributed Systems
Microservices: When to Split, and How to Draw the Lines
A practical checklist for deciding when service boundaries help, and how to design them when they do.
2026-06-05
Why microservices are a trade, not a default
Microservices are often sold as the grown-up way to build software. Independent deploys, smaller codebases, teams that own their slice of the product. Those benefits are real. So are the costs: every call becomes a network hop, every release needs more plumbing, and every shared business workflow has to survive partial failure.
The hard part is not drawing boxes on a whiteboard. The hard part is deciding whether a split is worth the operational bill, then drawing boundaries that will not fight you six months later. This note is a practical checklist for that work. It is decision-first, then design-oriented: figure out whether you should split at all, and only then how to split well.
A useful baseline looks like this:
- Start from pain, not fashion
- Prefer a modular monolith until the pain is real
- Split along business capability, not tech layers
- One service owns its data
- Decide sync vs async and failure behavior up front
- Keep interfaces boring and versioned
- Account for operational cost before celebrating the split
The rest of this note expands each point.
Start from pain, not fashion
A new service should remove a concrete problem. Good reasons tend to look like this:
- One part of the system needs a different release cadence than the rest
- A failure domain should stay isolated so an outage in billing does not take down browsing
- A team needs clear ownership of a capability without coordinating every change through a shared monolith release train
Weak reasons are usually social. “Peer companies use microservices.” “The architecture diagram looks more modern.” “We want to try Kubernetes.” Those may be fine as learning goals, but they are poor justifications for production complexity.
Before you create a service, write down the pain in one sentence. If you cannot name the friction, you are probably optimizing for aesthetics. Microservices amplify structure. They do not invent good structure for you.
Prefer a modular monolith until the pain is real
A modular monolith is one deployable with deliberate internal boundaries: clear modules, explicit interfaces, and limited cross-module reach into private details. You get many of the design benefits of services without paying the distributed systems tax on day one.
That pattern is not a compromise for teams that are “not ready.” It is often the correct default. Inside one process you can refactor boundaries cheaply, keep transactions simple, and debug with a single stack trace. You can still enforce ownership with package rules, code review, and module APIs.
Extract a service when the monolith’s modularity is no longer enough. Typical signals:
- Releases are blocked because unrelated modules ship together
- Scaling one hotspot forces you to scale everything
- Team coordination cost dominates feature work
- A failure in one module routinely cascades in ways you cannot contain with ordinary process isolation
Until those signals show up, prefer modules over network boundaries. It is much easier to extract a well-bounded module later than to stitch together services that were split too early.
Split along business capability, not tech layers
When you do split, split by what the business does, not by technical layer. “User service,” “order service,” and “inventory service” map to capabilities. “Frontend service,” “backend service,” and “database service” usually do not. Neither do splits that mirror an org chart of controllers, repositories, and workers copied into separate repos.
A useful test is language. If product people already talk about “checkout,” “identity,” or “notifications” as distinct areas with their own rules, those are candidates for service boundaries. If the only way you can describe the split is “we put all the controllers over here,” the boundary is probably wrong.
Watch for chatty designs after a split. If fulfilling one user action requires five synchronous hops and shared in-memory assumptions that used to live in one process, you may have cut through a single business workflow. High chatter is a smell that the boundary ignored cohesion. Prefer services that can complete most of their work with their own data and a small number of well-defined collaborations.
One service owns its data
The fastest way to turn microservices back into a distributed monolith is a shared database. If two services read and write the same tables, you have not decoupled deployments. You have moved the coupling into schema migrations, lock contention, and informal knowledge about who is allowed to touch which column.
Data ownership means one service is the source of truth for a given set of entities. Other services get what they need through APIs or events, not by joining across someone else’s tables. That sounds strict because it is. It forces you to design integration deliberately.
The trade is consistency. Cross-service reads are often eventually consistent. That is acceptable for many product flows if you design the UI and workflows for it: show pending states, retry safely, and avoid pretending every screen is a single ACID snapshot of the whole company. What is not acceptable is pretending services are independent while they share a schema behind the scenes.
Decide sync vs async and failure behavior up front
Service boundaries force a choice on every collaboration: synchronous request/response, or asynchronous messaging. Sync is simpler when the caller needs an answer now and the dependency is in the critical path. Async is better when you want temporal decoupling, smoother load, or workflows that can progress without every participant being online at the same moment.
Either way, failure is part of the design, not an afterthought. Decide:
- What timeout is acceptable
- Whether retries are safe, and who owns them
- What the user sees if a dependency is down
- Whether duplicate delivery can create duplicate side effects
Retries without idempotency are especially dangerous across service hops. If you need a deeper baseline for bounded retries, backoff, jitter, and idempotent writes, pair this note with the retries learning in this series. The short version for service design is: never assume “call again” is harmless unless the contract says it is.
A boundary is incomplete until you can answer what happens when the other side is slow, unavailable, or succeeds after the caller has already given up.
Keep interfaces boring and versioned
Good service interfaces are a little dull on purpose. Stable request and response shapes, predictable error codes, and clear ownership of fields age better than clever abstractions that leak storage details. Neighbors should depend on your contract, not on your tables, caches, or internal DTOs.
Versioning is how you keep that contract evolvable. Whether you use explicit versions in paths, headers, or carefully additive changes, the goal is the same: callers should not break because you renamed a column or refactored an internal model. Publish what is public. Hide what is private. Treat accidental exposure of internals as a design bug.
Boring also means documented. Teams integrate faster when the happy path, the failure modes, and the compatibility rules are written down somewhere obvious. Clever protocols that only live in one engineer’s head become distributed outages later.
Account for operational cost before celebrating the split
Every new service adds a bill: build pipelines, deploy targets, dashboards, alerts, on-call ownership, and the cognitive load of tracing a request across more hops. If that bill has no owner, the architecture is unfinished.
Before you celebrate a split, check the operational basics:
- Who deploys it, and how often can they do so safely?
- Who gets paged when it fails?
- Can you follow a single request across the new boundary with logs or traces?
- What is the rollback story?
Microservices without ownership become orphaned complexity. The codebase looks distributed, but the responsibility is still centralized in whoever happens to notice the fire. Prefer fewer services with clear owners over a constellation nobody wants to touch.
Use the checklist as a gate
Microservices amplify whatever structure you already have. Clear capabilities, owned data, and honest failure design get stronger when you put a network between them. Fuzzy ownership, shared tables, and unspoken retry behavior get worse in the same move.
Use the checklist as a gate, not a slogan. Start from real pain. Prefer a modular monolith until extraction is earned. Split by business capability. Give each service its data. Decide sync, async, and failure up front. Keep interfaces boring and versioned. Pay the operational cost on purpose.
If a proposed service cannot pass those checks, keep the boundary inside the monolith a little longer. That is not a lack of ambition. It is how you keep distributed systems complexity attached to problems that actually need it.