The Hidden Cost of Just One More Service
"It should be its own service" can sound like architectural maturity. Sometimes it is. Sometimes it is just a way to turn a code organization problem into a production system problem.
A service boundary is not free because a deployment pipeline makes it easy to create one.
The thesis
Every new service is an organizational and operational commitment, not just a code boundary. Create one only when the boundary earns its ongoing tax.
The tax is paid in ownership, observability, compatibility, incident response, latency, security review, deployment coordination, and cognitive load. A principal engineer has to price that tax before the split, not after the first incident.
The production pattern
A module grows complicated. It has a distinct domain name. Different people want to change it at different speeds. Someone proposes extracting it into a service.
The upside is real: clearer ownership, independent scaling, isolated deployments, and a sharper contract. But the downside often arrives later. Local function calls become remote calls. Partial failure becomes normal. Data consistency gets negotiated. Testing requires environments. Debugging crosses boundaries. On-call now needs to understand another moving part.
The organization may have reduced one kind of complexity while increasing another.
The model
I use a boundary-worthiness checklist.
First, ownership independence. Does a distinct group own the domain, roadmap, and operational health? If ownership remains shared, a service boundary may only add coordination.
Second, change independence. Does this component need to deploy on a different cadence from its callers? If not, a package boundary may be enough.
Third, scaling independence. Does it have a meaningfully different load profile, resource shape, or availability requirement? If all traffic moves together, separate scaling may be theoretical.
Fourth, failure isolation. Can the rest of the product degrade safely when this service is slow or unavailable? If callers cannot tolerate failure, the boundary is more dangerous than it looks.
Fifth, contract maturity. Can the team define stable semantics without leaking implementation detail? If the contract is still fluid, extraction may fossilize confusion.
Sixth, operational readiness. Does the team have logging, metrics, tracing, alerts, runbooks, deployment safety, and ownership for off-hours failure?
My decision rule:
- Split for independent ownership, change, scale, or failure containment.
- Do not split merely for code neatness.
- If the boundary is needed later, design the module so extraction remains possible.
I also look for boundary debt after a split. A new service can be worse than the module it replaced if it keeps shared database writes, requires lockstep deploys, exposes unstable internals, or pushes every exception back to the original team. In those cases the system has paid the distributed systems tax without buying independence.
The cleanest service boundaries usually have three properties. The owning group can change the internals without coordinating with every caller. Callers can survive bounded failure without corrupting their own state. The contract expresses domain intent rather than implementation sequence. If any one of those is missing, the service may still be justified, but the missing property should appear as an explicit risk rather than a surprise.
The operating question is simple: after the split, who can move faster, and who now has to think harder? A good boundary improves that exchange for the whole organization, not only for the team extracting code.
Where this goes wrong
The counterpoint is that monoliths can hide cost too. A large shared codebase can make local changes risky, slow down builds, obscure ownership, and create release coupling that becomes its own tax. Keeping everything together is not automatically simpler.
There are also cases where an early service boundary creates strategic clarity. A regulated domain, a high-risk workflow, or a capability with a distinct operating model may deserve isolation before the code size demands it.
The mistake is using "microservice" or "monolith" as an identity. The useful question is whether the boundary reduces the dominant risk in the system today without creating a larger one tomorrow.
What I do now
When a team proposes a service split, I ask them to write the pager story. Who gets paged? What dashboard do they open? What can they do without contacting another owner? How do callers behave during failure? What data can be safely retried?
Then I ask for the non-service alternative. Could a library, module boundary, separate package, clearer ownership file, or internal API solve most of the pain with less operational surface?
This is not resistance to distributed systems. It is respect for them. Distributed systems are powerful when they match organizational boundaries and failure needs. They are expensive when they are used as a substitute for modular design.
Closing takeaway
Before adding a service, prove that the boundary buys enough independence to pay for its permanent operational tax.