TRT
Sign in
System DesignUpdated

Choosing an Architecture Style From Constraints

An architecture style shapes how components communicate, deploy, and fail.

The Runtime Theory Team5 min read#architecture#monolith#services

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:

text
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 processes

The 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:

text
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 ms

Nine 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:

ini
; .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.

Martin Fowler: Microservices

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save