Start with a well-structured monolith and clear internal module boundaries. Extract a service when a specific part has a genuinely different scaling profile, release cadence or ownership — not because the architecture diagram looks better.
What splitting actually costs
A distributed system replaces function calls with network calls. Everything that was previously guaranteed becomes something you have to engineer.
- Calls now fail partway, so retries, timeouts and idempotency become mandatory
- Transactions no longer span the operation, so you are writing compensation logic
- Debugging requires distributed tracing to answer questions a stack trace used to answer
- Each service needs its own pipeline, monitoring, alerting and on-call coverage
- Local development becomes an exercise in orchestration
None of these are unsolvable. All of them are ongoing costs, and they are paid whether or not the benefits arrive.
Three things that justify a service boundary
- A different scaling profile. One component needs ten times the capacity of the rest, or needs hardware the rest does not — a video encoder, a model inference path, a bulk import.
- A different release cadence. A component that must ship several times a day held back by a system released weekly, or the reverse.
- A different owner. A separate team with real accountability for the component. Conway's law is not a warning here; it is the actual justification.
Absent one of these, a module boundary inside a single deployable gives most of the benefit and none of the operational cost.
The version that keeps your options open
A modular monolith is a single deployable with strict internal boundaries: modules that communicate through defined interfaces, do not reach into each other's tables, and could be lifted out without unpicking the rest.
This is the position of maximum optionality. Splitting later is then a mechanical exercise rather than an archaeology project — and in a great many cases you will find you never need to.
The database is the real boundary
Teams routinely split services while leaving them sharing one schema. That produces the operational cost of microservices with the coupling of a monolith — the worst available combination.
If a service does not own its data, it is not a service. If splitting the data is unacceptable — because you need transactional consistency across it — that is a strong signal the boundary is in the wrong place.
Where to start
For nearly every new system: one deployable, strict module boundaries, one database with clear ownership per schema, a real pipeline and real observability from the beginning. Extract when a specific, nameable pressure appears. Architecture earned that way is defensible; architecture chosen in advance usually is not.
Software Engineering
Most software does not fail at launch. It fails eighteen months later, when a reasonable change turns out to cost more than the original build.
Software Engineering