Reference · Software design
A working catalog of the patterns engineers actually reach for — what each one is, the shape it takes, what it costs, and when it earns its keep.
Architecture patterns are usually presented as a single menu you pick one item from. That framing is wrong. They sit on at least three different axes, and a real system uses several at once, at different layers.
One axis is application structure — how the code inside a single deployable is organised (layered, vertical slice, hexagonal, microkernel). A second is distributed topology — how independent processes are split and talk (microservices, event-driven, CQRS). A third is data and ML system design — how data is processed and refined at scale (pipelines, Lambda, medallion). They are not alternatives to each other; they compose. The reason the catalog feels endless is that people keep flattening these axes into one list.
Read this first — DDD is not on any of these axes
Domain-Driven Design is a modeling methodology, not an architecture. It is a toolkit for shaping a complex domain — strategically (bounded contexts, context maps, ubiquitous language) and tactically (entities, value objects, aggregates, repositories, domain events). It says nothing about dependency direction or where infrastructure lives.
Hexagonal and Onion are architectures that DDD often lives inside. Onion ≈ Hexagonal (structurally near-identical); DDD ≠ either. They answer different questions: DDD asks what shape should the domain model be; the rings ask which direction may dependencies point. You can do DDD inside a layered monolith, inside microservices, or inside vertical slices — and you can build a perfectly hexagonal app with a thin, non-DDD domain.
Part IApplication structure
Stack the code by technical role; each layer depends only on the one below it.
The default monolith structure and still the most common architecture in the wild. Presentation sits on application logic, which sits on data access, which sits on the database. It is simple to start and easy to explain — but because changes arrive as features, not as "all the repositories at once," every feature change smears across all four layers. This scattering is precisely what vertical slices and hexagonal are reacting against.
Organise by feature; each slice owns its own endpoint → logic → data access.
Coined by Jimmy Bogard. The insight: a layered tree minimises coupling along the wrong axis, so slices cut vertically through every layer instead. Everything for create_translation lives in one folder. Maximise cohesion within a feature, minimise coupling between features, and add abstraction only where a slice actually earns it. The payoff is parallelism — two developers on two features barely touch the same files — and deletability. The risk lives in what is shared: the database schema and any common kernel must be governed deliberately, or the slices drift apart.
Concentric rings; the domain sits at the centre; all dependencies point inward.
Three closely related architectures with different motivations and the same structural fixpoint. Hexagonal (Ports & Adapters) — Cockburn, ~2005 — isolates the core behind ports (interfaces the core owns) implemented by adapters (infrastructure); its real insight was symmetry, that there is no privileged "top" or "bottom," only driving and driven actors. Onion — Palermo, 2008 — built the same rings explicitly on DDD layering. Clean Architecture — Martin — is the synthesis that names hexagonal, onion, BCE and DCI as inputs. The honest caveat: the promised "swap the database" almost never happens, so on a thin domain you pay the abstraction tax without ever cashing the benefit.
A minimal core plus features added as plugins against a stable contract.
The core stays small and stable; new capability is added without touching it, by writing a plugin that satisfies a published contract. IDEs, browsers, and agentic plugin suites are all built this way. The hard part is the contract: get it right and plugins compose freely; get it wrong and every plugin leaks core internals. The "no framework, own-your-orchestration" constraint some teams impose is exactly the discipline that keeps a microkernel a microkernel — capability lives in plugins, the kernel stays thin.
Part IIPresentation
A triangle of responsibilities — not a whole-system architecture.
These govern the inside of the presentation layer, not the system. MVC is the classic: the controller handles input, updates the model, and the model's changes drive the view. MVP routes everything through a presenter. MVVM adds two-way binding between view and view-model — which is effectively what a modern component framework with reactive state does, where hooks and state play the view-model role. A fourth idiom, unidirectional data flow (Flux / Redux, and Elm's MVU), pushes the opposite way to MVVM's two-way binding: state moves in one direction only — action → store → view — trading a little ceremony for updates that are predictable and easy to trace.
Microservices applied to the browser: a shell composes independently built and deployed UI fragments.
The frontend counterpart to microservices. Instead of one monolithic SPA, a thin shell owns routing and composition, and each area of the UI is built, tested and shipped by the team that owns that domain — stitched together at build time, at runtime via module federation, or through web components. The win is the same as on the backend: team autonomy and independent release. So is the tax: design-system drift between fragments, duplicated dependencies shipped to the browser, and version coordination across teams. It earns its keep past a certain org size; for a single team it is pure overhead.
Part IIIDistributed topology
Independently deployable services, each owning its data — versus the same boundaries in one deployable.
Microservices buy independent scaling and team autonomy, paid for in distributed-systems pain: network failure, eventual consistency, observability, and transactions that no longer fit in one database. The pragmatic counterpart is the modular monolith — the same strong module boundaries, one deployable, one database — which gives most of the cleanliness without the tax. Reach for microservices when org size or scale genuinely demands it; start with a modular monolith otherwise.
A handful of coarse-grained domain services sharing one database — the pragmatic middle between monolith and microservices.
The most under-rated topology, and the natural next step when a modular monolith outgrows one deployable but the team isn't ready for the distributed-data tax. You split into a few coarse-grained services — typically along domain lines — each independently deployable, but they keep sharing one database. That single decision dodges the hardest parts of microservices: no per-service data duplication, no eventual consistency, no saga needed for a cross-service transaction, because a real database transaction still spans them. You give up independent data scaling and accept the shared schema as a coupling point. For most teams "splitting because the org demands it," this is the honest first split, not full microservices.
Components publish events to a bus; subscribers react. Producers never call consumers directly.
Decoupling is the whole point: you add an audit-log consumer without touching the service that emits the event. The cost is that flow becomes implicit — tracing what happens after an event, reasoning about ordering, and debugging across a broker all get harder. Common with Kafka, RabbitMQ, or cloud equivalents.
Split the write path from the read path; optionally store the events themselves as the source of truth.
CQRS gives commands a write model optimised for validation and consistency, and queries a separate read model optimised for fast, denormalised reads; the two stay in sync via events or projections. Event Sourcing goes further — the sequence of events is the truth, and current state is a replay. Powerful for read-heavy or audit-heavy systems, and overkill for simple CRUD.
Coordinate a business transaction across services with compensating actions on failure.
Once data is split per service, you no longer have one database transaction. A saga is the answer: a sequence of local steps, each with a compensating action that undoes it if a later step fails. Two styles — orchestration, where a central coordinator drives the steps, and choreography, where each service reacts to the previous one's events. Orchestration is easier to reason about; choreography couples less but is harder to follow.
Keep the working set in a replicated in-memory grid; write through to the database asynchronously, out of the request path.
The answer to the one bottleneck that scaling tiers cannot fix: the database under extreme, spiky load. Instead of every request hitting shared storage, each processing unit holds the data it needs in an in-memory data grid that is replicated across units, so you scale simply by adding more units. A data pump drains changes to the database asynchronously, off the request path, and a messaging grid keeps the units' caches in sync. The result is near-linear elasticity and very high throughput; the price is real complexity — replication, cache consistency, and eventual (not immediate) durability to the database. Reach for it only when load is genuinely extreme and bursty.
These are real and popular but mostly variants or infrastructure around the above, and don't need their own diagram:
Part IVData & ML systems
Data flows through a chain of independent stages, each transforming and passing it on.
The mental model behind ETL, build systems, and agent orchestration alike. Each filter is independent and testable; the pipeline is just their composition. An agent workflow — ingest, translate, review, publish — is a pipeline whose filters happen to be model calls, and frameworks that model orchestration as a graph are a generalisation of this (a directed graph of stages rather than a straight line).
Lambda: a batch layer for correctness plus a speed layer for freshness, merged at serving. Kappa: drop the batch layer and treat everything as one stream.
Both answer the tension between accurate, reprocessable results and low-latency freshness. Lambda runs two paths and merges them — robust, but you maintain two codebases for the same logic. Kappa simplifies by making the stream the only path and reprocessing by replaying it — fewer moving parts, at the cost of expressing everything as streaming.
Part VCross-cutting styles
Part VIDecide
Because these patterns compose rather than compete, this doesn't end with one answer. Walk the questions and it assembles a stack — one choice per axis, skipping the axes that don't apply to you. Treat the result as a starting point to argue with, not a verdict.
The honest takeaway: "choosing an architecture" almost never means picking one item from one list. It means composing four or five of these across different layers.
A typical modern service is a modular monolith organised as vertical slices, with an MVVM-style frontend, a pipeline (or agent graph) for orchestration, a broker/queue for long-running jobs, and a single hexagonal port at the one volatile seam — typically the external model or provider boundary. Each of those choices lives on a different axis, so they stack rather than compete. And DDD floats above all of them as the modeling discipline you apply within whichever structure you picked — never a substitute for it.
Pick the structure for how your code changes (feature-driven → slices; rich domain → rings; extensibility-driven → microkernel). Pick the topology for how your org and load actually scale (start monolith; split only when forced). Pick the data architecture for your latency-versus-accuracy needs. Then resist adding any pattern that isn't paying for itself.