Skip to content

10.17 — Architecture Styles Compared

The same product — an online shop with a catalogue, a cart, checkout, payments and email — can be built in five completely different shapes. Each shape makes some things cheap and other things expensive, and the expensive parts do not appear until months later.

This page is about what each one actually costs, told honestly, because the loudest advice in this area is usually the least honest.

1. Client-server, the shape underneath everything

A client asks, a server answers, and the server owns the data. Every style below is a variation on it, so it is worth naming the two properties it establishes.

The server is the authority. The client can be lied to, modified, or replaced, so every rule that matters is enforced on the server. A price check in the browser is a convenience; the price check that counts happens where the client cannot reach it.

The client is untrusted and unreliable. It disappears mid-request, retries, and runs an old version for months.

Everything else on this page changes how the server side is arranged. None of it changes those two facts.

2. The monolith

One deployable unit. All the code, one process, usually one database.

The word is used as an insult, which has made people bad at describing its genuine advantages. Here they are.

A function call is not a network call. Checkout calling inventory is nanoseconds, cannot fail halfway, cannot time out, and needs no retry logic. This one difference removes an enormous category of work.

One transaction covers everything. Reserve stock, create the order and record the payment in a single database transaction, and either all of it happens or none of it does. In a distributed system this becomes a saga with compensations (10.8.4) — weeks of work to replace something the database was doing for free.

Refactoring is safe. Renaming a field is one commit and the compiler checks it. Across services it is a coordinated release with a compatibility window.

Debugging is a stack trace. One error, one place, the whole causal chain visible. No correlation ids, no tracing infrastructure, no reconstructing what happened from five services' logs.

Deployment is one artefact. One version to reason about, one thing to roll back.

What actually goes wrong, and note that none of these are about performance.

Any change requires deploying everything. A typo fix in the email template redeploys the payment code, so releases become large, rare and frightening — and rare releases make each one riskier, which makes them rarer.

Teams collide. Twenty engineers in one codebase means merge conflicts, coupled release schedules, and the need to agree on everything.

Scaling is all-or-nothing. If image processing needs more CPU you must run more copies of the entire application, including the parts that were idle.

One bug takes everything down. A memory leak in report generation kills checkout too, because they share a process.

The technology choice is frozen. One language, one framework, one runtime, for everything, forever.

The honest summary: a monolith is the right starting point for almost every system, and the problems above are about organisational scale far more than technical scale. A team of five should not be running microservices, and a team of two hundred cannot comfortably share one codebase.

3. The modular monolith

One deployable unit, with enforced internal boundaries. Modules communicate through defined interfaces, each owns its own tables, and no module reaches into another's internals.

Big ball of mud — everything touches everythingcartorderspaymailstockAny change can affect anything. Nobody can predict the blast radius.Modular monolith — one process, real boundariesCatalogueown tablesOrdersown tablesPaymentsown tablesNotificationsown tablesStill one deploy, one transaction, one stack trace — but you know what depends on what.
Figure 1 — The distinction that matters is not one process versus many. It is whether the boundaries are real. A monolith's bad reputation belongs to the left-hand picture.

You keep every advantage of the monolith and gain the one thing that actually motivates most microservice migrations: knowing what depends on what. And if you later need to extract a module into its own service, the boundary already exists, so the extraction is mechanical rather than archaeological.

Why this is under-used: it requires discipline with no enforcement from infrastructure. In a separate service, calling another team's database is impossible. In a modular monolith it is one import away, and unless the build fails on it, someone will do it under deadline pressure. The fix is to make it fail the build — module boundaries checked automatically — because a rule nobody can break is worth ten rules everyone agrees with.

This is the right answer far more often than it is chosen. The usual sequence should be: monolith, then modular monolith, then extract the two or three modules that genuinely need independent scaling or independent teams. Most systems never need step three.

4. Microservices

Many small services, each independently deployable, each owning its own data, communicating over the network.

What you genuinely buy.

Independent deployment. A team ships without coordinating with anyone. This is the real prize, and it is an organisational benefit rather than a technical one.

Independent scaling. Image processing gets twenty machines, checkout gets four.

Fault isolation, if you build it. Report generation dying does not stop checkout — provided checkout does not call it synchronously, which requires deliberate design (10.9).

Technology choice per service. Genuinely useful for the one service that needs a different language, and a liability if used freely.

Clear ownership. This team owns this service, its data and its on-call.

What you pay, and this list is why microservices go wrong.

Every call can fail, be slow, or happen twice. What was a function call is now a network request needing a timeout, a retry, a circuit breaker and an idempotency key (10.9). Multiply by every call site.

You lose transactions. This is the biggest and most underestimated cost. Reserve stock and take payment atomically becomes a saga with a compensation for every step, and now you must decide what happens when the compensation itself fails (10.8.4).

Data is scattered. "Show me orders with their customer names" was a join. Now it is two calls and manual stitching, or duplicated data with a synchronisation problem.

Debugging needs infrastructure. You cannot understand a failure without distributed tracing and correlation ids (10.10). That is not optional tooling; without it a cross-service bug is close to undebuggable.

Operational surface multiplies. Forty services is forty deployment pipelines, forty sets of dashboards, forty on-call rotations or one very tired one.

Local development gets hard. Running the whole system on a laptop stops being possible somewhere around service fifteen.

Versioning is forever. You can never deploy two services simultaneously, so every interface change needs a compatibility window in both directions.

The honest rule: microservices trade technical simplicity for organisational independence. That is a good trade when you have enough teams to need the independence, and a terrible one when you do not — you have taken on all the costs and bought nothing.

The signal that you actually need them is not the size of the codebase. It is that teams are blocked on each other: releases queue behind one another, a change needs three teams to agree, or one component's scaling needs are wildly different from the rest. Absent those, the modular monolith is doing the job.

5. Serverless

You write functions. The platform runs them on demand, scales them from zero to thousands automatically, and bills by execution rather than by machine.

What is genuinely good.

No servers to operate. No patching, no capacity planning, no fleet.

Scale to zero. An endpoint used twice a day costs almost nothing, which makes low-traffic services and internal tools economical in a way they were not.

Automatic scaling. A traffic spike is absorbed without a decision from you.

What is genuinely bad.

Cold starts. An idle function must be started before it runs — tens of milliseconds for a small runtime, seconds for a heavy one. The first user after a quiet period waits, and this is the objection you must be able to discuss (3.2).

The cost curve inverts. Cheap at low volume and expensive at sustained high volume. A function running constantly costs several times what a machine running constantly costs. The crossover is real and you should compute it rather than assume either direction.

Connections are hard. Each concurrent execution wants a database connection, and a thousand concurrent executions want a thousand — which most databases refuse. This forces a connection proxy in front, which is machinery you adopted serverless to avoid.

Limits are hard walls. Maximum execution time, maximum memory, maximum payload. A job that takes eleven minutes against a ten-minute ceiling needs redesigning, not tuning.

Local development and debugging are worse. You are debugging something that only exists while it runs.

Where serverless clearly wins: spiky or unpredictable traffic, event-driven glue between systems, scheduled jobs, image and file processing on upload, and small internal tools. Where it clearly loses: steady high traffic, latency-sensitive paths where a cold start is unacceptable, and long-running work.

6. Event-driven

Services publish events describing what happened and other services react, rather than calling each other directly.

The change is in the direction of knowledge. In a request-driven system, checkout knows it must call inventory, payments and email. In an event-driven one, checkout publishes OrderPlaced and does not know who is listening.

What you buy. Adding a fourth reaction — a loyalty service — requires no change to checkout at all. The publisher and the subscriber are decoupled in time as well: a subscriber that is down catches up later from the queue, so a failure becomes a delay rather than an error.

What you pay. Nobody knows the whole flow, because it is not written down in any one place — following what happens after an order means reading five services. Debugging means reconstructing a chain from events across systems. Ordering and duplicate delivery become your problem (10.4). And everything becomes eventually consistent, so the order exists a moment before the inventory reflects it, which the user interface has to be honest about.

The distinction worth having ready: choreography means services react to each other's events with no coordinator, which is loosely coupled and hard to follow. Orchestration means one coordinator holds the process and calls each step, which is easy to follow and puts the knowledge in one place. Most mature systems use orchestration for anything involving money or more than about three steps, and choreography for reactions that are genuinely independent (10.8.4).

7. Peer-to-peer

No central server. Participants connect to each other, and every one is both client and server.

How it works. A new participant needs to find others, so there is usually a small central piece for introductions — a tracker or a signalling server — after which traffic flows directly between peers. Data is spread across participants, and popular data is on more of them, so capacity grows as the network grows rather than shrinking under load.

Where it genuinely wins. File distribution, where every downloader also uploads and a popular file gets faster with demand. Live video and voice between two people, where routing audio through a central server would add pointless latency. Systems that must survive without any single operator.

Why almost no business application uses it. You cannot enforce rules on machines you do not control, and every rule that matters — pricing, permissions, payment — needs an authority. You cannot deploy a fix, because participants run whatever version they feel like. Peers behind home routers cannot accept incoming connections without help. And there is no reliable way to make data durable when every copy lives on a machine that may be switched off tonight.

Where you will actually meet it: direct browser-to-browser audio and video, file sharing, and blockchain-based systems (10.13). Knowing why it is rare is more useful than knowing how it works.

8. Choosing

SituationShape
New product, small teamMonolith
Growing codebase, one or two teamsModular monolith
Many teams blocked on each otherExtract the contended modules
Wildly different scaling per componentExtract those components
Spiky or rare trafficServerless functions
Many independent reactions to one eventEvent-driven
No central authority possiblePeer-to-peer

The sequence that goes wrong is starting at microservices because that is what large companies use. Large companies arrived there by growing into it, and they carry the costs because they also have the teams, the tooling and the operations staff that make the costs bearable. Adopting the destination without the journey means paying every cost and receiving none of the benefit.

And the sequence that works: start with a monolith. Impose module boundaries and enforce them in the build. When a specific team or a specific component is genuinely blocked, extract that one. Repeat only as needed. Most systems stop after one or two extractions, and that is a success rather than an unfinished migration.

9. What the interviewer will push on

"Monolith or microservices?" The wrong answer is either word on its own. The right answer asks how many teams there are and what is actually blocked. Microservices trade technical simplicity for organisational independence, so they pay off when teams are blocked on each other and cost you dearly when they are not. Naming the modular monolith as the usual right answer is what separates a considered response from a fashionable one.

"What do you lose when you split a monolith?" They want the transaction, first and specifically. Reserve stock and take payment atomically becomes a saga with compensations, and you now have to decide what happens when a compensation fails. After that: joins become network calls, debugging needs tracing infrastructure before it is possible at all, and every interface change needs a compatibility window because two services can never deploy simultaneously.

"When is serverless the wrong choice?" Steady high traffic, where the cost curve inverts and a function running constantly costs several times a machine running constantly. Latency-sensitive paths where a cold start is unacceptable. Long-running work that hits the execution ceiling. And anything with a database connection per execution, which forces a proxy — machinery you adopted serverless to avoid.

"Event-driven or request-driven?" They are checking whether you know the cost of decoupling. You gain the ability to add a subscriber without touching the publisher, and you lose the ability to read the flow in one place. Say the choreography-versus-orchestration distinction, and give the practical rule: orchestration for money and for anything over about three steps.

"Why don't business applications use peer-to-peer?" Because you cannot enforce rules on machines you do not control, cannot deploy a fix, and cannot make data durable when every copy is on a machine that may be switched off. Knowing why it is rare demonstrates more than knowing how a distributed hash table works.

"How would you split this monolith?" By what changes together and by who owns it, never by technical layer. Extracting "the database layer" as a service is the classic wrong answer, because every feature still needs a change in two places — you have added a network hop without reducing coupling at all.

The thing to volunteer that nobody asks for: extract the first service as an experiment rather than as step one of a plan. Pick a component with a genuinely clean boundary and little shared data, run it for a quarter, and count what it actually cost in deployment work, debugging time and on-call load. That number is the input to whether you extract the next twelve, and teams that skip this measurement usually discover the cost around service eight, when turning back is expensive.

Next: 10.18 — the structures that answer questions approximately, in a fraction of the memory, and why that trade is often exactly right.

Recall

  • Monolith's real advantages: function calls instead of network calls, one transaction, safe refactoring, a single stack trace, one artefact to deploy. Its problems are organisational — coupled releases, team collisions, all-or-nothing scaling, shared failure.
  • Modular monolith keeps all of that and adds enforced internal boundaries. It is the right answer far more often than it is chosen. Enforce the boundaries in the build, because a rule that cannot be broken beats one everyone agrees with.
  • Microservices trade technical simplicity for organisational independence. Good when teams are blocked on each other; a pure loss when they are not. The biggest hidden cost is losing transactions — atomic becomes a saga with compensations.
  • Serverless wins on spiky traffic, glue and scheduled jobs. It loses on steady high volume (the cost curve inverts), latency-sensitive paths (cold starts), long jobs (hard ceilings) and per-execution database connections.
  • Event-driven buys you adding a subscriber without touching the publisher, and time decoupling. It costs you a flow nobody can read in one place, plus ordering and duplicates. Orchestration for money and for more than ~3 steps; choreography for genuinely independent reactions.
  • Peer-to-peer grows capacity with participants and is unusable for business rules, because you cannot enforce anything, deploy anything, or guarantee durability on machines you do not control.
  • Split by what changes together and who owns it — never by technical layer. Extracting "the data layer" adds a network hop and removes no coupling.

Self-test: Name three genuine advantages of a monolith. What is the single biggest thing you lose by splitting one? When does serverless become expensive? What is the difference between choreography and orchestration, and when do you use each? Why is splitting by technical layer wrong?

Quiz Bank

FoundationalGive the honest case for a monolith, then the honest case against it.

For it, and none of these are about performance.

A function call is not a network call. Checkout calling inventory takes nanoseconds, cannot time out, cannot partially succeed, and needs no retry logic, no circuit breaker and no idempotency key. Every one of those becomes necessary the moment that call crosses a network, and they are needed at every call site.

One transaction covers the whole operation. Reserve stock, create the order and record the payment either all happen or none do, because the database does it for you. Replacing that in a distributed system means a saga with a compensating action for each step, plus a decision about what happens when a compensation itself fails — weeks of work to recover a property you had for free.

Refactoring is safe. Renaming a field is one commit and the compiler finds every use. Across services it is a coordinated change with a compatibility window in both directions, because two services never deploy at the same instant.

Debugging is a stack trace. One error, one process, the whole causal chain visible without correlation ids or tracing infrastructure.

One artefact. One version to reason about, one thing to roll back.

Against it, and notice these are organisational rather than technical.

Every change deploys everything. A change to an email template redeploys the payment code, so releases grow large and infrequent — and infrequent releases are riskier, which makes teams release even less often.

Teams collide. Twenty engineers in one codebase means constant merge conflicts and a release schedule everyone shares.

Scaling is all or nothing. More CPU for image processing means more copies of the entire application.

Shared failure. A memory leak in reporting kills checkout, because they share a process.

The technology choice is frozen for everything, forever.

The conclusion worth stating. A monolith is the correct starting point for almost every system, and its failure mode is organisational scale rather than technical scale. Five engineers should not be running microservices. Two hundred cannot comfortably share one codebase. And most of the pain attributed to monoliths actually belongs to monoliths with no internal boundaries — which is a different and more fixable problem.

AppliedA team of eight has a monolith that takes forty minutes to deploy and where two people constantly conflict on the same files. They want microservices. What do you advise?

Ask what the actual problem is first, because the two symptoms have different causes and neither one is solved by microservices.

The forty-minute deploy is almost never architectural. It is nearly always slow builds, a slow pipeline, or a deployment process that could be running in parallel and is not. Splitting into eight services gives you eight deploy pipelines, each of which is still slow, plus the coordination overhead of releasing across them. If the build is the problem, fix the build — it is days of work rather than quarters, and it benefits every future architecture too. A team that splits a monolith to fix deploy time has usually spent nine months to arrive at eight slow pipelines.

The merge conflicts are more interesting, because they are a genuine signal, but of a boundary problem rather than a deployment-unit problem. Two people editing the same files constantly means either the code has no clear ownership, or those files are doing too many jobs. In a well-modularised codebase, two people working on different features touch different modules and rarely meet.

So the advice: build the boundaries first, without splitting the deployment.

Identify the natural modules — catalogue, orders, payments, notifications — by what changes together and who owns it. Give each one its own directory, its own public interface and its own tables. Then enforce it in the build, so an import that crosses a boundary illegally fails automatically. That enforcement is the part that makes it stick, because a convention with no enforcement is one deadline away from being broken.

This gets you the thing the team actually wants — clear ownership, changes confined to one area, and knowing what depends on what — while keeping the single transaction, the single stack trace and the single deployment.

Why microservices specifically would be a bad trade for a team of eight. They would gain independent deployment, which needs enough teams to be blocked on each other before it is worth anything. And they would pay for it with: distributed transactions replaced by sagas, tracing infrastructure before cross-service debugging is even possible, eight pipelines and eight sets of dashboards, a local development story that stops working, and a compatibility window on every interface change forever. Eight people cannot absorb that operational load and also ship features.

What would change my advice. If the team grows to forty across five product areas, or if one component's scaling needs diverge sharply from the rest — image processing needing twenty machines while everything else needs four — then extracting that specific component becomes justified. And because the module boundaries already exist by then, the extraction is mechanical rather than an excavation.

The sentence to leave them with: microservices are a solution to an organisational problem, and this team's problem is a build pipeline and a missing set of boundaries. Fix both, and revisit in a year with real evidence about what is blocked.

InterviewWhat exactly do you lose when you split a monolith into services, in order of how much it hurts?

One, the transaction, and this is by a wide margin the biggest. In a monolith, reserving stock, creating an order and recording a payment are one database transaction: all of it or none of it. Split across services, no such guarantee exists. You replace it with a saga — a sequence of steps, each with a compensating action to undo it — and you must then answer questions the database used to answer silently. What if the compensation fails? What does the customer see while the process is half done? How do you avoid running a step twice when a retry arrives? This is weeks of work and a permanent source of subtle bugs, and it is routinely underestimated because in the monolith it was invisible.

Two, the join. "Orders with customer names" was one query. Now it is a call to orders and a call to customers, stitched in application code — with two failure modes, two latencies and no consistent snapshot. The alternative is to duplicate customer names into the order service, which means a synchronisation problem and stale data. Neither option is as good as what you had.

Three, debuggability. A failure in a monolith is a stack trace. Across services it is invisible without correlation ids propagated through every call and distributed tracing collecting them (10.10). That is not optional tooling to add later; without it a cross-service bug is close to undebuggable, and teams that split before building it spend months in the dark.

Four, refactoring safety. Renaming a field inside a monolith is one commit and the compiler checks it. Across a service boundary it is: add the new field, deploy, migrate consumers one at a time, wait for all of them, then remove the old field. Because two services can never deploy at the same instant, every interface change needs backward compatibility in both directions, forever.

Five, operational load. Forty services means forty pipelines, forty sets of dashboards and alerts, forty things to patch, and either forty on-call rotations or one very tired one.

Six, local development. Running everything on a laptop stops being possible somewhere around fifteen services, and every workaround — shared environments, mocked dependencies, service virtualisation — is its own maintenance burden.

And the framing to close on: you are trading technical simplicity for organisational independence. That is a genuinely good trade when teams are blocked on each other, because organisational friction compounds and is very expensive. It is a pure loss when they are not, because you have paid every cost above and received nothing in return.

StaffYou are asked to move a stable monolith to microservices because the CTO read that it is best practice. Make the case for what you would actually do.

Take the concern seriously first, because there is usually a real one behind the request. "We should do microservices" is rarely about services; it is about something that hurts. Releases are slow, teams are blocked, one component keeps falling over, or the system feels unchangeable. Find out which, because the right fix depends entirely on the answer and none of them is necessarily microservices.

Then be specific about what a full migration costs, in terms that are hard to wave away.

Every synchronous call across a boundary now needs a timeout, a retry policy, a circuit breaker and an idempotency key. Every atomic operation spanning two former modules needs a saga with compensations and a decision about partial failure. Before any cross-service bug can be debugged you need correlation ids and tracing in place. You need a service template, a deployment pipeline per service, and dashboards and alerts per service. And local development needs a new story.

That is typically a year of work for a mid-sized system, during which feature delivery slows rather than speeds up. If we are not confident about what we are buying, that is a very expensive experiment.

What I would propose instead, as a sequence with decision points.

First, measure what actually hurts. How long does a change take from commit to production, and where does the time go? How often do teams block each other, and on what? Which component's scaling is genuinely mismatched? These are cheap to gather and they turn an argument about architecture into a conversation about evidence.

Second, fix the pipeline if the pipeline is the problem, which it usually is. A forty-minute deploy is almost never architectural, and splitting the system gives you many slow pipelines instead of one.

Third, impose module boundaries and enforce them in the build. This delivers most of what people want from microservices — clear ownership, confined changes, a known dependency graph — while keeping the single transaction, the single stack trace and the single deployment. It is weeks rather than a year.

Fourth, extract one service as a real experiment. Choose the component with the cleanest boundary, the least shared data and the strongest independent reason to move — usually something with a genuinely different scaling profile. Run it in production for a quarter and measure what it cost: engineering time, incident count, on-call load, debugging time.

Fifth, decide with that number. If the first extraction was cheap and the benefit clear, extract the next. If it was painful, we have learned that for the price of one service instead of forty, and the modular monolith is still there and still fine.

The organisational point worth making to a CTO specifically, because it reframes the whole question: microservices are an answer to teams blocked on each other. If we have four teams and they are not blocked, we would be paying an organisational tax to solve an organisational problem we do not have. If we plan to grow to twenty teams next year, the boundaries work above is exactly the preparation, and we can extract on demand as each boundary comes under real pressure.

And the sentence that usually lands: the large companies held up as examples arrived at microservices by growing into them, one extraction at a time, and they carry the costs because they also have the platform teams and tooling that make the costs bearable. Adopting the destination without the journey means paying every cost on day one and collecting the benefits, if ever, on day four hundred.

Flashcards

FlashMonolith's real advantages

Function calls not network calls · one transaction · compiler-checked refactoring · one stack trace · one artefact. Its problems are organisational, not technical.

FlashModular monolith

One deploy, enforced internal boundaries, each module owning its tables. Keeps every monolith advantage and adds a known dependency graph. Enforce boundaries in the build or they will be broken.

FlashThe microservices trade

Technical simplicity traded for organisational independence. Worth it when teams block each other; a pure loss when they do not. Biggest hidden cost: atomic operations become sagas with compensations.

FlashWhen serverless loses

Steady high traffic (cost curve inverts) · latency-sensitive paths (cold starts) · long jobs (hard ceilings) · a database connection per concurrent execution.

FlashChoreography versus orchestration

Choreography: services react to each other's events, no coordinator, loosely coupled, hard to follow. Orchestration: one coordinator holds the process. Use orchestration for money and anything over ~3 steps.

FlashHow to split

By what changes together and who owns it. Never by technical layer — extracting "the data layer" adds a network hop and removes no coupling.