Writing
What a team decides before its first microservice
Once splitting is justified, four decisions decide whether it works: where the boundary falls, who owns which data, what replaces the transaction, and what has to exist operationally before anything ships.
Most writing about microservices argues about whether to adopt them. That argument has an answer for small teams, and I have made it elsewhere: a modular monolith gives you the same boundaries without the distributed system, and the triggers for splitting something out are specific enough to name.
This is about what happens after one of those triggers actually fires. A second team takes ownership, a component needs its own runtime, a compliance requirement forces isolation — and now the split is justified. The failure mode from that point is not choosing microservices. It is splitting along the wrong lines and discovering it eighteen months later, when moving a boundary means a data migration plus a versioned contract plus a coordinated rollout across two teams.
Four decisions do most of that work. This is the material I train teams on when they ask how to make the split without regretting it, and it was the substance of a five-day course I delivered to a government engineering team, in Arabic, on designing and implementing microservices with .NET.
Where does the boundary actually fall?
The boundary that survives is the one drawn around a business capability that owns its own decisions — not around a noun, and not around a layer.
The noun trap is the common one. “Customer” looks like an obvious service until you notice that sales, billing, and support each mean something different by it, need different fields, and change on different schedules. A “customer service” that all three call becomes a bottleneck that every team queues behind, which is the exact problem splitting was supposed to solve.
The test that works is about change rather than structure: if a single business change routinely requires coordinated releases from two services, the boundary is in the wrong place. One new field in a checkout flow should not require three teams to agree a deployment order.
A useful smell in the opposite direction is a service that everything depends on and that depends on nothing. That usually means the boundary was drawn around a data structure rather than a capability — it is a shared database with an HTTP interface in front of it.
Who owns which data, once it is across a network?
Inside one process, “each module owns its tables” is a discipline. Across a network, it is the whole design, because the moment a service needs data it does not own, you have to pick how it gets it — and every option costs something:
| Approach | Consistency | Failure behaviour | Cost |
|---|---|---|---|
| Synchronous call to the owner | Always current | Caller fails when owner is down | Coupled availability |
| Replicate via events | Eventually consistent | Caller keeps working, data is stale | Must handle staleness explicitly |
| Shared database | Always current | — | Not a split at all |
The third row is the one teams reach for under deadline pressure, and it silently undoes the entire exercise. Two services against one schema are one system with two deployment pipelines: the coupling is still there, it is just no longer visible in the code.
The second row is usually right, and the decision it forces is the one teams skip: how stale is acceptable, per field. A product name being a few seconds out of date is fine. A credit limit being a few seconds out of date is a business decision, not a technical one — and it is exactly the kind of question that should be answered with the business rather than assumed by an engineer at eleven at night.
What replaces the transaction?
This is the decision that surprises teams most, and it is the one that generates the most production incidents when it is not made deliberately.
Inside one process, a unit of work is one database transaction: it commits or it does not. Split across services, “place an order” becomes reserve stock, then charge the card, then create the shipment — three steps that can each fail independently, and one of which involves money.
There is no distributed transaction coming to save this. The realistic options are:
- Compensating actions. Every step gets an explicit undo — release the reservation, refund the charge. Straightforward to describe, and the compensations are where the bugs live, because they are the paths that run least often.
- Make forward progress the only direction. The order is accepted in a pending state and the system retries until each step succeeds, with a human queue for what never does. Usually the better fit for commerce, because a customer would rather wait than have their order silently vanish.
Both require idempotency at every step, and that is not optional. A retried “charge the card” that is not idempotent charges twice. In practice this means every operation carries a caller-supplied key and the receiver records what it has already done with that key — designed in from the first service, not added after the first duplicate charge.
The related decision is what the user sees meanwhile. A status that is derived from the steps that have actually completed is honest and survives partial failure. A status field that something remembers to update is a field that eventually disagrees with reality.
What has to exist before the first service ships?
A modular monolith fails loudly and locally: one stack trace, one log stream, in order. A distributed system fails quietly and somewhere else. The operational floor is therefore part of the architecture, not a follow-up ticket:
- Correlation across services. One identifier, generated at the edge, present in every log line and every outbound call. Without it, “this request was slow” is not an answerable question — and adding it later means touching every service.
- One place to read the logs. Logs sitting on five machines are five investigations.
- Health and readiness that mean something, so a deployment fails on the branch rather than in production.
- A defined answer to “who gets paged” for each service, agreed before the first incident rather than during it.
None of this is individually difficult. It is constant, and it comes out of the same budget as the product — which is the honest cost of the split, and the reason the split should be triggered rather than chosen.
The order these get decided
Boundaries first, because everything else follows from them. Then data ownership, because it is what makes a boundary real rather than decorative. Then the transaction question, because it decides what the system does when the network misbehaves — which it will. Then the operational floor, because without it you cannot see any of the above working.
Teams usually do this in reverse: infrastructure first, because it is tangible and there are tools to buy. That produces a well-monitored system with the boundaries in the wrong place, and no amount of observability fixes that.
This is the material I train teams on. If your team is at this decision, the course is built around your system rather than a generic example — see team training, or read the case for not splitting yet if the trigger has not actually fired.