Architecture
When microservices are the wrong answer
Microservices solve an organisational problem, not a technical one. If you have fewer than about twenty engineers, they usually cost more than they return.
Most teams that adopt microservices do it for a technical reason and get an organisational bill.
The technical pitch is familiar: independent scaling, fault isolation, freedom to pick the right language per service. All real. All available to a well-structured monolith too, at a fraction of the operational cost.
What microservices actually solve is a coordination problem. When you have enough engineers that merge conflicts, release coordination and deployment queues become the bottleneck, splitting the deployable unit lets teams move without waiting for each other. That is a genuine and important benefit — and it only pays once you have that many teams.
This is not a contrarian position. Martin Fowler argued it in 2015 and has not softened it: "You shouldn't start a new project with microservices, even if you're sure your application will be big enough to make it worthwhile." His stated reason is empirical rather than aesthetic — "almost all the cases where I've heard of a system that was built as a microservice system from scratch, it has ended up in serious trouble" — and it rests on the microservice premium: a fixed overhead that only complex systems can absorb.
The bill you receive on day one
Before any of the benefits arrive, the costs do.
A network appears in the middle of your logic. A function call that could not fail now can. Every service boundary needs timeouts, retries with backoff, and idempotency on the receiving side, because a retried call must not double-apply. That is real code, and it exists in every service.
Transactions stop working. A single database transaction across two tables becomes a distributed workflow across two services. You are now implementing sagas and compensating actions for something the database used to do correctly for free.
Debugging requires infrastructure. A stack trace no longer tells you what happened. You need distributed tracing before you can answer "why was this request slow", and you need it working before the first incident, not after.
Local development gets worse. A new engineer cannot run the system on their laptop without either a heavy orchestration setup or a set of mocks that drift from reality.
Set against what each item replaces, the trade is easier to see:
| What you build on day one | What it replaces |
|---|---|
| Timeouts, retries with backoff, idempotency keys | A function call that could not fail |
| Sagas and compensating actions | One database transaction |
| Distributed tracing and log correlation | A stack trace |
| Orchestration setup, or mocks that drift | Running the whole system on a laptop |
| Contract tests and versioned interfaces | The compiler |
None of this is a reason never to do it. It is the reason to be honest about the timing.
One well-documented case
In March 2023, an engineer on Prime Video's video quality analysis team published an account of consolidating precisely this kind of system — independent defect detectors coordinated by serverless orchestration, with intermediate state passing through object storage — into a single process, and reported cutting infrastructure cost by over 90%.
It is worth being precise about what that case is and is not, because it is the most over-claimed example in this argument. It is one component of one product. The reported bottlenecks were orchestration overhead and the cost of shuttling video frames through intermediate storage — not microservices as an idea. Amazon did not abandon microservices, and the team said explicitly that the distributed approach was not bringing benefits in their specific use case. What makes it useful is narrower and still striking: for the same work, the distributed design cost roughly an order of magnitude more than the consolidated one.
The original write-up is no longer available — primevideotech.com now redirects to Amazon's corporate site — so the contemporaneous trade coverage is the most reliable surviving account. That is its own small lesson about depending on vendor engineering blogs as durable references.
The threshold, roughly
The number we use is about twenty engineers, or the point where you have three or more teams that genuinely need to release on independent schedules. Below that, the coordination pain that microservices relieve does not exist yet, so you are paying operational cost for a benefit you cannot collect.
To be clear about what that number is: it is a heuristic from our own project reviews, not a measured finding, and it is a proxy for the thing that actually matters — whether release coordination is your binding constraint. A tightly coupled team of thirty may not need it; three genuinely independent teams of five might.
Below that threshold there is almost always a better move: fix the module boundaries inside the monolith.
What to do instead
A modular monolith gets you most of the way, and it makes the eventual split cheap rather than speculative.
- Enforce boundaries in code. Separate modules with explicit public interfaces, and a lint rule or build check that fails if one module reaches into another's internals. This is the actual work of a service split, done without the network.
- Give each module its own schema. Same database instance, separate schemas, no cross-schema joins. When you later extract a module, its data comes with it instead of needing to be untangled first.
- Communicate through an in-process event bus. Publishing an event to a local bus and publishing to a queue look nearly identical at the call site. Swapping the transport later is a small change; introducing the concept later is not.
- Deploy one artefact. One build, one deploy, one rollback. Keep this for as long as it is not the thing slowing you down.
Because the boundary and the schema were already separate, extraction changes the transport and the deployment unit — not the design. You extract the one module that needs it, not all four.
Do this and the extraction decision becomes empirical. When one module genuinely needs different scaling or a different release cadence, it lifts out along a boundary you already enforced — and you extract exactly that one, not all fifteen.
The question worth asking
Not "are microservices better?" — that question has no answer without context. Ask instead: what is currently slowing us down?
If the honest answer is "our release process requires four teams to coordinate," microservices are pointed at your actual problem. If the answer is "we think we might need to scale later," you are buying insurance whose premium is paid in every future debugging session.
The systems we have seen struggle most were not monoliths that should have been split. They were eight services built by five engineers, where every feature touched four of them, and nobody could run the thing locally.
Questions we get asked
We already have twelve services and six engineers. What now? Consolidate the ones that always deploy together — if two services are never released independently, the boundary between them is costing you and returning nothing. Merging back is unglamorous and rarely regretted. Keep the boundaries that map to genuinely different scaling or failure requirements.
Does a modular monolith not just become a distributed monolith later? That risk is real, and the guard against it is the build check. If nothing mechanically fails when one module reaches into another's internals, the boundaries erode at the first deadline — the same dynamic that turns technical debt into an accident rather than a decision. Enforced boundaries are what make the difference.
What about independent scaling? One part of our system is much heavier. Scale the monolith horizontally first and measure. Multiple instances of one artefact behind a load balancer handle very uneven load profiles perfectly well, because the idle modules cost almost nothing. Extract when you can show that the shared deployment is the actual constraint — that is the case where the premium is worth paying, and it is also the case that is easy to demonstrate rather than assume.
Is serverless a middle ground? Sometimes, but note that it makes the same trade. You still get a network between components, still lose the local transaction, and still need tracing — which is exactly the shape of the Prime Video case above. If you would like a second opinion on a specific system, that is most of what our custom software work involves.
