Part IV · Chapter 6 · Several Services · lesson 1
Should you split this service in two?
14 min
Builds on Say what would change your mind, before you look at options
Part IV · Chapter 6 · Several Services · lesson 1
14 min
Builds on Say what would change your mind, before you look at options
The checkout service is about forty thousand lines of Python. Two teams commit to it. One ships on Fridays after a manual regression pass; the other wants to ship four times a day and cannot. Someone has drawn a diagram with the service cut in two down the middle of the order model, and nobody in the room can say what the cut buys.
One of these four facts, on its own, justifies the split.
Predict
Each of these is a claim you can check, and each has a cheaper partial fix that you should try first.
Different scaling profile. Two workloads inside one process share a memory
limit, a worker pool and one autoscaling signal. If one of them needs far more
of any of those, the fleet is sized for the greedy workload and every other
request is billed for headroom it never uses. Check it by taking peak
concurrency and peak memory per endpoint — a Kubernetes kubectl top pod
reading will not tell you this, so you want per-route numbers from an APM agent
such as Datadog, Grafana Pyroscope or the OpenTelemetry metrics your framework
already emits.
Different teams. Two teams in one deployable share a release train, which means the faster team's cadence is pinned to the slower team's confidence. This is the reason that is actually about people, and it is the one most likely to be real. Check it by counting how many of the slow team's releases the fast team waited on last quarter, and how many rollbacks each team caused in the other's code.
Different failure tolerance. If one path must stay up when the other is broken — the order write must survive the recommendation widget being down — then they cannot share a process, because a process is the unit that dies. Check it by naming the worst thing that happens when each path is unavailable. If the two answers are "we lose money" and "the page looks a bit plain", that is a real boundary.
"The codebase feels big." This is a navigation problem and a package boundary fixes it. A split makes the same code harder to read, because now half of it is behind a network call you cannot step into with a debugger.
"This talk said so." A talk describes a company's answer to a constraint they had, usually at a size and an org chart you do not have. The talk is evidence about their problem, not about yours.
"Other teams want to reuse it." Reuse wants a library or an endpoint. A shared library you version is almost always the cheaper first move — a private package on your own index, or a Git submodule if you must.
"We might need to scale it later." The option to split later is cheap to keep and the split itself costs you for every quarter you hold it. Keep the seam clean — one module, no cross-imports, its own tables — and buy the split on the day a number forces it.
This is the point at which the word microservices usually enters the room, and it is worth noticing that it has stopped describing anything. A monolith with three extracted services and a microservices architecture with four large services are the same system drawn by two people who want different outcomes from the meeting. The three reasons above survive the relabelling; the argument about which word applies does not.
Two readings
Separate deployables from near the start, because a boundary that was never a network call is much harder to make into one once code has grown across it.
Keep everything in one process and one deploy, and split only when a specific, measured pain arrives that a split is the cheapest fix for.
Both sides accept
Both sides agree that the number of deployables is a cost, not a score; that a seam inside one process is worth keeping clean whether or not it ever becomes a network boundary; and that the worst outcome is a split along a boundary that turns out to be wrong, because an incorrect network boundary is far more expensive to move than an incorrect module boundary. They disagree about when you know enough to place the line.
Which is which? Put each one on a side.
A six-month-old product with four engineers and a domain model that changed three times this quarter
A ledger that must be auditable, has a regulator asking who can deploy to it, and is written to by three product teams
Two teams in one repository, one releasing weekly behind a manual pass, the other blocked on it about half the time
Before splitting the checkout service, I would separate the receipt-rendering path into its own Kubernetes Deployment running the same container image, routed by path at the ingress — Envoy Gateway or the ingress-nginx controller, whichever the cluster already runs — with its own resource requests and its own horizontal pod autoscaler target.
That buys the whole of the scaling-profile benefit for about a day of work. One image, one build, one test suite, one version of the code: the greedy workload now gets memory-heavy nodes and the cheap requests stop paying for them.
What it does not buy is independent deploys, because there is still one artefact and one release train. If the actual pain is the Friday regression pass, this move does nothing for it, and you should be splitting the repository rather than the Deployment. The two problems look identical in a meeting and have different fixes, which is why the three reasons above have to be named separately rather than bundled into "the service got big".
What would you do
orders table inside one transaction: promotions reads it to apply a discount and writes a row to applied_promotions in the same unit of work.Check
A team extracted their notification sending into its own service for the failure-tolerance reason, which was correct: a down mail provider should not block an order write. They did the work well. The extraction took three weeks and the order path stopped depending on the mail path.
Then the same four engineers owned both services. Every change to a notification template needed a coordinated release because the templates lived in the caller. Both services deployed together, from one pipeline, in one order, every time. The pager still rang for the same four people.
They had bought failure isolation, which they wanted, and they were paying for deploy independence and team independence, which they did not have and could not use. Two of the three reasons were absent, and the costs of a split do not come itemised: you pay all of them and collect only the benefits that apply.
The fix, eventually, was to move the templates into the notification service and give it a contract that changed less often than the caller did. That is the work that makes a split pay — and it is work that happens after the extraction, which is why extraction estimates are always wrong.
Everything above assumes the two halves have, or will have, different owners.
If one team owns both halves and deploys them together — same pipeline, same release, same pager — then the team reason is absent and the deploy-independence benefit cannot be collected. You are left with whichever of the scaling and failure-tolerance reasons actually applies, and those two are both satisfiable without a code split: separate Deployments of one image for the first, a bulkhead and a timeout for the second.
So the honest version of the rule is narrower than it sounds. A split pays when two different groups of people need to move at different speeds, or when a number about memory, concurrency or failure says one process cannot hold both. One owner and one release train removes the first and leaves you arguing the second on its own merits, where it usually loses.