An architecture style shapes how components communicate, deploy, and fail. Choosing one is not choosing between "simple" and "scalable" — it is choosing which failure modes and operational costs you would rather carry, because every style pays all of them somewhere.
What a style actually fixes
Three axes decide most of the cost of a system, and a style is a bundle of defaults for all three at once:
axis modular monolith services
----------- ---------------------- ---------------------------
communication function call network RPC or message
deployment one artifact, one pipeline N artifacts, N pipelines
failure exception in-process partial failure + timeouts
data one database, joins local ownership per service, joins
become queries or replication
observability one process, one log stream correlation across processesThe reason this matters: you cannot change one axis independently. Split the deployment and you have automatically bought network communication, partial failure, and distributed correlation — whether or not you needed them.
The numbers that make the trade concrete
Latency is the most quantifiable axis. Take a checkout path that makes six dependency calls and a user-facing p99 budget of 120 ms:
in-process call : ~0.1 ms dispatch + work → 6 calls ≈ 1 ms
same-AZ RPC : ~1.5 ms p99 per call → 6 calls ≈ 9 ms
cross-AZ RPC : ~5–10 ms p99 per call → 6 calls ≈ 40 msNine milliseconds is affordable. The larger cost is elsewhere: failure surface compounds
multiplicatively. If each dependency succeeds with 99.9% probability and the six calls are
independent, the path succeeds with 0.999^6 ≈ 99.4% — roughly six times as many user-facing
failures as any single dependency would suggest. Add retry budgets, timeouts, and idempotency
requirements for each hop, and you have paid for that multiplication in code as well as in
latency.
Operational cost compounds the same way. One service means one build, one deploy pipeline, one set of dashboards, one on-call rotation to page. Ten services mean ten of each, plus schema compatibility between versions, plus service-to-service authentication, plus the version-skew problem where provider v1 and v2 run side by side during a rollout. Teams that count only the deployment saving and not the ownership saving consistently underestimate this.
A concrete walk-through
A small team with one release cadence benefits from a monolith whose internal modules have explicit interfaces — one artifact, one rollback story, joins stay in one database. The boundaries still matter, because they are what will let you split later without a rewrite. Enforce them mechanically so they survive deadline pressure:
; .importlinter — fails CI when a boundary is crossed
[importlinter:contract]
name = Checkout domain must not import storage drivers
type = forbidden
source_modules = [app.checkout]
forbidden_modules = [app.storage.pg, app.storage.redis]Extraction should be triggered by measurement, not by fashion. The evidence that justifies paying the network tax looks like this:
- Deployment contention. Four teams share one pipeline; median merge-queue-to-prod wait has grown past 20 minutes and releases are batched daily.
- Divergent scaling profile. The search index rebuild consumes 70% of the box's CPU while request handling needs the other 30%, and they scale for different reasons.
- Failure isolation. One subsystem's memory leak takes down every request handler, and the incident happens more than once a quarter.
- Independent data ownership. A second team needs to write the same tables under different invariants, and every change needs a cross-team schema review.
If none of those are measured, extraction is an aesthetic preference with a four-figure monthly bill.
Costs and failure cases
Microservices turn local calls into remote interactions: timeouts, retries, idempotency, data ownership, and observability requirements. The classic failure is the distributed monolith — services split by layer (a frontend service, a domain service, a datastore service) so every change still requires deploying all three in lockstep. You pay the full network cost and keep none of the deployment autonomy. The signature is simple: if a feature branch cannot ship without coordinating two other teams' release trains, the split bought nothing.
A monolith has its own failure mode: without enforced boundaries, every module can import every other, the test suite grows until CI takes 25 minutes, and a "small change" touches 14 files across 5 concerns. Splitting it does not repair that — the boundaries have to exist before the network does.
Architecture should follow constraints that exist, not scale imagined for later. The extracted service needs its own SLOs, its own dashboards, and someone paged at 3 a.m.; if you cannot name that owner, you have not finished the design.
Key takeaways
- A style is a bundle of defaults for communication, deployment, and failure. You don't get to pick them one at a time.
- Count the multiplicative cost, not the latency alone: six dependencies at 99.9% each is a 99.4% path, before retries and timeouts.
- Extract services on measured evidence — deploy contention, scaling divergence, failure isolation, data ownership — never on anticipated growth.
- Beware the distributed monolith: layer-split services that still deploy in lockstep pay the network tax and keep none of the autonomy.
- Keep boundaries enforced inside one deployment first (import rules, module interfaces); it is the cheapest rehearsal for splitting later.