Architecture Patterns: The Six Shapes a System Can Take (and Why They Are Not a Menu)

Event-driven, layered, monolithic, microservices, MVC, primary-replica — teams compare these six as if they were alternatives, which is why the meeting never ends. They are not alternatives. They sit on three different levels: the deploy unit, the code organisation, and the data topology. Here is the sort, proved on a real 246-line FastAPI service where four of the six are true at the same moment, with the price of each level measured rather than argued.

Banner

Prefer to watch? ▶ The full 16-minute episode ⚡ The 2-minute version ✈ Telegram

There is a meeting that cannot end. Someone says "we should go event-driven." Someone else says "no, layered is fine." A third person says "honestly this should just be microservices." Everyone is annoyed, nobody is wrong, and the whiteboard slowly fills with six words:

event-driven · layered · monolithic · microservices · MVC · primary-replica

The reason that meeting cannot end is that it is being run as a vote between six options. It is not a vote. Those six words are not alternatives, and the question "which architecture pattern should we use?" has no answer — the same way "should I drive or should I go fast?" has no answer.

The one-line mental model:

  • The six names sit on three different levels of the system.
  • A real system answers all three at once.
  • So the useful question is never which pattern — it is which level am I deciding on right now.

The three levels

Sort the six words and the argument dissolves:

LevelThe question it answersThe names that live here
Deploy unitHow many independently deployable processes is this?monolithic, microservices
Code organisationHow is the code inside one unit arranged, and who may call whom?layered, MVC, event-driven
Data topologyHow many copies of the data are there, and who may write?primary-replica

Those are three genuinely independent axes. You can be a monolith with an event-driven interior. You can be microservices whose services are each internally layered. You can move any one of them without touching the other two. That independence is the whole point — and it is why comparing "layered vs microservices" is a category error, not a trade-off.


The proof: four of six, on one small program

Arguments about architecture are cheap, so let's not have one. Take a single small service — a bookshop API written in FastAPI: 12 files, 246 lines, one uvicorn process, one socket on 127.0.0.1. A browser talks to routes, routes call a service layer, the service calls a repository, the repository talks to SQLite.

Now hold each of the six names up against that one program:

NameTrue of this program?Why
monolithicYesOne deployable unit, one process, one socket.
layeredYesroutes → services → repo → data, and imports only ever point downward.
MVCYesThe three roles are all present and separated.
event-drivenYesCheckout publishes an event; two subscribers react without being called.
microservicesNoThere is exactly one deploy unit. This is the axis it answers differently.
primary-replicaNot decidedOne SQLite file. The data-topology question simply has not been asked yet.

Four of the six names are simultaneously true of the same 246 lines. That is the fact that kills the menu framing. If they were alternatives, a program could only be one of them.

One honesty note that matters: the folders in this app are literally routes/, services/, repo/ — not model/, view/, controller/. The MVC label reads the roles, not the filenames. A pattern that only applies when you name your directories after it is not a pattern, it is a naming convention.


Level one: the deploy unit, and what the boundary costs

Moving a call across a deploy boundary is the most expensive edit in this entire article, and it is usually the one made most casually.

Take the exact same service call and run it two ways on the same machine: in-process, and over loopback HTTP.

The same callCost
In-process function call0.11 ms
Over loopback HTTP1.67 ms
The boundary itself+1.55 ms ≈ 15×

Fifteen times, for changing nothing but where the code lives. And that is the friendly number: this is loopback on one six-core laptop, with no network, no TLS, no load balancer, no other tenant. A real network hop is worse.

Two things worth being precise about, because sloppy versions of this measurement circulate widely:

  • Of that 0.11 ms in-process call, about 98 µs is SQLite open/close — real work, not overhead. The pure business rule with no I/O at all runs in 0.33 µs. So the honest same-work comparison is 15×, not the ~1500× you get by comparing a bare function against a full HTTP round trip.
  • A fresh TCP connection costs only +93.5 µs (1.06×), not the 20–300× a naive first run suggests. That large early number turned out to be httpx client construction, not networking. Measure the thing you mean.

None of this says "don't split." It says the split buys you independent deployment and independent scaling, and it charges you a millisecond and a distributed system. Pay it deliberately. (The full monolith-versus-microservices trade-off deserves its own treatment — here the only claim is which level that decision lives on.)


Level two: code organisation

This is the level where most day-to-day design decisions actually happen, and it holds three of the six names.

Layered — and the fact that it is checkable

"Layered" sounds like a diagram, but it is really a single enforceable rule: imports point one way only. Routes may import services; services may import the repository; nothing ever imports upward.

That rule is a property of the source, not a philosophy, which means a program can verify it. An 80-line import-direction checker walks the app, builds the edge list, and fails on any upward edge. On the clean app it reports 11 intra-app edges, 0 upward — PASS. Add a single upward import and it exits non-zero and prints the exact offending line.

This is the most under-used idea in architecture: if your layering is real, you can put it in CI, and it stops being a thing people erode one pull request at a time.

MVC — and the fat-controller bill

MVC separates three roles: the model (data and rules), the view (presentation), the controller (request handling). The failure mode has a name — the fat controller, where business logic creeps into the route handler because that is where the request already is.

The bill, measured:

  • The fat route is 21 lines instead of 5.
  • A unit test of the pricing rule exercises 17 lines of the service and 0 lines of the fat route.
  • Testing that rule through the service takes 0.11 ms; testing it through HTTP takes 1.41 ms12×, and at 100 cases it stretches to 53×.

The logic in a fat controller is not untested by accident. It is unreachable by anything cheaper than a full HTTP request. That is the cost, and it compounds every time the suite runs.

Event-driven — which does not mean Kafka

The single biggest misconception about event-driven design is that it requires a broker. It does not. In this app, checkout publishes an event and two subscribers handle it — and the measurement says the publisher and both subscribers run on the same process id, with zero new network connections and zero brokers.

Event-driven is a code-organisation decision. Whether the bus is an in-process list of callbacks or a Kafka cluster is a deploy-unit decision. Conflating those two is exactly the confusion this whole article is about.

And it has a real price, which shows up in the traceback. When the handler raises:

Direct callThrough the bus
Traceback depth4 frames3 frames
Contains the request handler?YesNo
What the caller sawHTTP 500HTTP 200, already returned in 3.32 ms

That is the trade in one table. You bought decoupling; you paid with a stack trace that no longer contains the thing that caused the failure, and a client that was told everything went fine. This is why event-driven systems live or die on their observability, not on their broker.

(For completeness: the bus itself hands the event over in 0.133 ms. A larger figure you might see quoted for "the event path" is the subscriber's own SQLite commit — that is the subscriber's work, not bus overhead.)


Level three: the data topology

Primary-replica answers a question the other five never touch: how many copies of the data exist, and which of them may accept writes. One primary takes writes; replicas take reads and follow along.

The point here is not how replication works — it is which level the decision belongs to. You can add replicas to a monolith. You can run microservices against a single database. The data topology is a genuinely separate dial, and treating it as an alternative to "layered" or "MVC" is what produced the six-item whiteboard in the first place.


The reframe: the same three levels in an LLM system

This sort is not a legacy-backend concern. Point it at an AI serving stack and the three levels are immediately recognisable — which is useful, because "what is the architecture of our LLM app?" is currently the same unanswerable meeting.

LevelThe LLM-system decision
Deploy unitIs the model in-process, behind your own vLLM/TGI server, or a third-party API? Each step out is another boundary — and the 1.55 ms you measured on loopback becomes tens or hundreds of milliseconds against a hosted endpoint.
Code organisationIs retrieval → prompt build → inference → post-process a layered pipeline with one-way dependencies? Are your evals, logging and cost tracking wired as event subscribers rather than stuffed into the request handler — the RAG equivalent of a fat controller?
Data topologyHow many copies of the vector index exist, which one accepts writes during re-embedding, and do readers see the old index or the new one?

An agent framework that "does everything" is usually a fat controller with a nicer name: tool selection, retrieval, prompt assembly and business rules all in the handler, reachable only by running the whole thing. The fix is the same fix as 2005 — push the rules into a layer a unit test can call in 0.11 ms.


The verdict

Stop asking which pattern. Ask which level you are deciding on.

When the next meeting opens with "should we be event-driven?", the productive move is a question, not an opinion: are we talking about how the code inside this service is organised, or about whether this becomes a second deploy unit? Those are different decisions, with different costs, made by different people, at different times. Half the architecture arguments in the industry are two people confidently answering two different questions.

The three things worth taking away:

  1. The six names are not a menu — four of them were true of one 246-line program at once.
  2. Each level has a measurable price, so you can stop arguing and go measure: 15× for a deploy boundary, 12× on test cost for a fat controller, a stack trace that loses the culprit for an in-process bus.
  3. Layering is checkable in CI. Eighty lines of import-direction checker is the cheapest architecture enforcement you will ever write.

Everything quoted here was executed on a real running service before it was written down, which is why a couple of the "obvious" numbers ended up smaller than the internet's version of them.


Watch the reel: the 2-minute version frames the problem, and the full 16-minute episode builds the bookshop service and runs every measurement on screen.

Architecture Patterns: The Six Shapes a System Can Take (and Why They Are Not a Menu) | Software Engineer Blog