Appearance
Chapter 2: Architectural Styles
General Track · Foundational · ~35 min read + exercise
Why this chapter exists
In the last chapter you learned to see every architectural decision as a trade-off: you raise one quality attribute and pay with another, inside a box drawn by your constraints. This chapter takes that exact lens and points it at the biggest structural decision you'll ever make about a system — its architectural style.
An architectural style is the top-level shape of a system: how it's divided into parts, how those parts are deployed, and how they talk to each other. Is it one deployable unit or fifty? Do the parts call each other directly and wait for answers, or fire messages into the void and move on? Is the whole thing organized in horizontal layers, or carved into vertical business capabilities? These choices set the ceiling on what your system can be good at. Pick a style whose strengths match what you need, and a lot of later decisions get easier. Pick one whose weaknesses land exactly where you're vulnerable, and you'll spend years fighting your own foundation.
Here's the thing to understand before we look at any specific style: a style is not a level of sophistication. It's a bundle of trade-offs. There's a strong cultural pull in our industry to treat architectural styles as a ladder — layered monoliths at the bottom as the "beginner" choice, microservices at the top as what "real" engineers build, and the job being to climb. That framing is wrong, and it produces some of the most expensive mistakes you'll ever watch a team make. Microservices aren't more advanced than a monolith; they're a different bundle of trade-offs that's right for some situations and catastrophically wrong for others. A three-person startup that adopts microservices because that's what the conference talks praised hasn't leveled up — they've bought distributed-systems complexity to solve organizational problems they don't have yet, and paid for it with the development speed they desperately need.
Consider the same product built two ways. Imagine a Laravel application for the Sysops Squad ticketing system from one of our source books — customers file support tickets, the system assigns experts, experts resolve them, customers get surveyed. Built as a modular monolith, it's one Laravel codebase, one deployment, one database, with clear internal module boundaries (ticketing, billing, customer profiles, knowledge base). When you want to ship a change, you deploy the whole app. A bug in the billing module shares a process with ticketing, so a memory leak in one can take down the other. But — and this matters — a developer can trace a ticket-assignment flow from HTTP request to database write by reading code in a single repository, the whole thing runs on their laptop, and a feature that touches three modules is one pull request and one deploy.
Now build the same product as microservices: a ticketing service, a billing service, a customer service, a notification service, each its own deployment, each its own database, talking over the network. Now the billing service can be deployed, scaled, and fail independently of ticketing — a billing crash doesn't touch ticket assignment. The teams that own each service can work and release without coordinating. But that same ticket-assignment flow now crosses three network boundaries, which means it can fail in ways a function call never could (timeouts, partial failures, the notification service being down). A feature touching three services is three pull requests, three deploys, and a versioning dance. And no developer can run the whole system on their laptop without significant tooling.
Neither version is more advanced. They made opposite bets. The monolith bought simplicity and local reasoning, and paid with coupled deployment and shared failure. The microservices bought independent deployment and fault isolation, and paid with distributed-systems complexity and operational overhead. Which is "better" is exactly the wrong question — the right question is which set of trade-offs fits this team, this product, this moment, and that's the same reasoning you practiced in Chapter 1, now operating at the largest scale.
What this chapter covers
This chapter gives you a working map of the major architectural styles and — more importantly — the dimensions that actually distinguish them, so you can reason about a style you've never seen by asking the right questions about it.
- The two dimensions that matter most — how a system is partitioned, and how its parts communicate. Almost everything else follows from these.
- The major styles — layered, modular monolith, microservices, and event-driven — each as a bundle of trade-offs, not a rung on a ladder.
- How to choose — the questions that actually drive the decision, and why the honest default for most teams is less exotic than the internet suggests.
By the end you'll do a hands-on exercise classifying and critiquing a real system's style — quite possibly one you work on — and reasoning about what a different style would have bought or cost it.
Let's start with the two dimensions that separate every style from every other.
Two dimensions that define every style
If you try to memorize architectural styles as a list of named things, you'll be lost the moment you meet one that isn't on your list — and there are dozens of named styles, with new ones coined every few years. The durable skill isn't memorizing the catalog. It's understanding the small number of dimensions the catalog varies along, so you can place any system — named or not — by asking a couple of questions about it.
Two dimensions do most of the work.
Dimension one: how is the system partitioned? When you divide a system into parts, you can cut it along one of two grains. Technical partitioning groups code by its technical role: all the controllers together, all the services together, all the data-access code together. This is the classic layered approach — the horizontal slices of a typical Laravel or Spring app. Domain partitioning groups code by business capability: everything about ticketing together (its controller, service, and data access), everything about billing together, regardless of technical role. This is the vertical-slice approach, and it's the organizing principle behind microservices and modular monoliths done well.
The distinction sounds academic until you notice what each optimizes. Most change requests arrive in domain terms — "change how billing works," "add a field to tickets" — not technical terms. Nobody files a ticket that says "modify the persistence layer." So domain partitioning tends to localize change: a billing change touches the billing slice and stops there. Technical partitioning tends to spread change: a single business change ripples across the controller layer, the service layer, and the data layer, because the thing you're changing was smeared across all of them. That's a real, recurring cost of layered architectures, and it's the main reason the industry drifted toward domain-oriented styles.
Dimension two: how do the parts communicate? Two parts can talk synchronously — the caller sends a request and waits for the response before doing anything else, like a function call or a typical HTTP request to an API. Or they can talk asynchronously — the caller sends a message and moves on, not waiting, with the response (if any) arriving later through some other channel, like dropping a job on a queue. We met this exact distinction in Chapter 1's frontend example (await the server vs. optimistic update); here it operates between backend components, and it's one of the heaviest trade-offs in all of architecture. Synchronous communication is simple to reason about — the call returns, you have your answer, the code reads top to bottom. But it couples the caller's fate to the callee's: if the thing you're calling is slow or down, you're slow or down too, and you wait. Asynchronous communication decouples that fate — you fire the message and your work continues whether or not the recipient is healthy right now — but you pay for it with a more complicated mental model, harder debugging (the flow no longer reads top to bottom), and a new set of questions about ordering, delivery, and what happens to a message nobody successfully processed.
Here's the payoff of thinking in dimensions rather than names: you can place any architecture on this 2×2 grid, and its rough trade-off profile falls out of where it sits.
A traditional layered monolith sits in the technical-partitioning, synchronous corner: simple to build and reason about, but change spreads across layers and everything shares one fate. A well-structured modular monolith moves along the partitioning axis — domain-partitioned internally, still mostly synchronous and still one deployment — keeping local reasoning while localizing change. Microservices push further into domain partitioning and often lean async, buying independence and fault isolation at the cost of distributed complexity. Event-driven systems live in the async-heavy region, organized around the flow of events rather than direct calls, trading the ability to reason about a linear flow for extreme decoupling and the ability to absorb bursts.
Notice that none of these positions is "higher" than another. They're coordinates, and each coordinate is good at some things and bad at others. The grid isn't a difficulty scale; it's a map of where different trade-off bundles live. With it in hand, the named styles become much easier to understand — they're just recognizable, well-trodden positions on this map, each with a body of accumulated wisdom about how to make that particular set of trade-offs work. Let's walk the four you'll meet most.
The four styles you'll actually meet
For each style below, the shape is the same: what it is, what it's good at, what it costs, and when to reach for it. Resist the urge to find the "best" one as you read — by now you know that's the wrong question. Read instead for which trade-off bundle each one represents.
Layered architecture
The layered style organizes code into horizontal tiers by technical role — typically presentation, business logic, persistence, and the database — where each layer may only talk to the one directly below it. It's the architecture most of us learned first, and for good reason: it maps onto how frameworks are built. A default Laravel app (controllers → services → Eloquent models → database) or a typical Express/Nest app is layered out of the box.
Good at: It's simple, familiar, and requires almost no architectural decisions to get going — the framework already laid out the layers. Separation of technical concerns is clear: all the data-access code lives in one place, all the HTTP handling in another. For a small app or a team learning a stack, this low starting cost is genuinely valuable. Don't sneer at it.
Costs: As we saw with the partitioning dimension, layered architecture is technically partitioned, so business changes spread across layers — adding a field to "ticket" touches the controller, the service, and the persistence code. As the app grows, the layers thicken and the business logic gets smeared horizontally, making it hard to see where any one feature lives. There's also a subtler failure mode: layered systems tend to grow a "fat" business layer that becomes a big ball of mud with internal layer boundaries that everyone agreed to respect and nobody actually does. And because it's one deployment with one shared fate, it shares the monolith's deployment and failure coupling.
Reach for it when: the system is small, the team is small or new to the domain, and you value getting moving over long-term change-localization. It's a perfectly respectable place for a system to start — and for many systems, to stay.
Modular monolith
A modular monolith keeps the single-deployment, single-database simplicity of a monolith but partitions the code internally by domain rather than by technical layer. Instead of a Controllers/, Services/, Models/ structure, you get a Ticketing/, Billing/, CustomerProfile/, KnowledgeBase/ structure, where each module contains its own controller, service, and data access, and modules talk to each other only through defined interfaces — not by reaching into each other's internals.
This style has had a real renaissance, and it deserves it, because it captures most of what people actually wanted from microservices without most of the cost. You get change localization (a billing change stays in the billing module), clear ownership boundaries, and the ability for a team to reason about one module in isolation — while still deploying one artifact, running one process, sharing one database with real transactions, and letting a developer run the whole thing on their laptop.
Good at: local reasoning and change localization without distributed-systems complexity. In-process calls between modules are fast, reliable, and transactional. You can refactor a module boundary with a compiler or a test suite catching your mistakes, rather than coordinating a cross-service contract change. For the large majority of teams and products, this is the sweet spot — and critically, it keeps the option of extracting a module into a service later if a real need appears, because the domain boundaries are already drawn.
Costs: discipline is doing all the work, and discipline erodes. Nothing at runtime stops the billing module from importing a ticketing class directly and quietly recoupling what you separated — the boundaries are a convention, not a wall, unless you enforce them (which is exactly what fitness functions, from the governance chapter, are for). You also still have one deployment and one shared fate: a memory leak in one module can still take the whole process down, and you can't scale one module independently of the others.
Reach for it when: you want clean boundaries and the ability to grow, but you don't have the operational maturity, team count, or genuine independent-scaling needs that justify the cost of going distributed. For most teams most of the time, this is the honest default.
Microservices
Microservices partition the system by domain and deploy each partition independently. The ticketing service, billing service, customer service, and notification service each run as separate processes, each owns its own database, and they communicate over the network — synchronously via APIs, asynchronously via messaging, or both.
The defining property — the thing that makes a microservice a microservice rather than just a small service — is independent deployability. Each service can be built, tested, deployed, scaled, and rolled back on its own schedule, without coordinating with the others. Everything microservices are good at flows from this, and so does everything they cost.
Good at: independent deployment (a team ships its service whenever it's ready), fault isolation (a crash in billing doesn't take down ticketing), independent scalability (scale the notification service for a campaign without scaling everything), and technology heterogeneity (one service can be Go, another PHP, if there's a real reason). Crucially, microservices are also an organizational tool: they let many teams work in parallel without stepping on each other, which is why they tend to pay off at organizations with many teams — and rarely before. This is Conway's Law (from Chapter 1's constraints) turned to advantage: align services with teams and the architecture stops fighting the org chart.
Costs: you have signed up to operate a distributed system, and distributed systems are hard in ways that aren't optional. The network is unreliable, so every cross-service call can time out, fail partially, or arrive twice — which is why the next chapter on reliability patterns exists. Data no longer lives in one database, so the comfortable single-database transaction is gone, replaced by distributed transactions and eventual consistency (the consistency tension from Chapter 1, now unavoidable). Debugging a flow that crosses five services requires distributed tracing you have to build. Local development needs serious tooling. And every one of these costs is paid whether or not your product actually benefits from the independence — which is why adopting microservices prematurely is one of the most expensive mistakes in the field.
Reach for it when: you have multiple teams that need to deploy independently, genuine differential scaling needs (parts of the system have wildly different load), or hard fault-isolation requirements — and the operational maturity to run distributed systems. Notice these are mostly organizational and scale pressures, not technical-elegance ones. If your reason for wanting microservices is "it's the modern way," you don't yet have a reason.
Event-driven architecture
Event-driven architecture organizes the system around the production and consumption of events — notifications that something happened ("ticket created," "payment received") — rather than direct commands between components. Components emit events without knowing or caring who consumes them, and other components react to events without knowing who emitted them. Communication is predominantly asynchronous, usually through a message broker or event stream (Kafka, RabbitMQ, SQS, Laravel's queue with events).
There are two flavors worth knowing. In choreography, there's no central coordinator — each component reacts to events and emits its own, and the overall workflow emerges from the chain of reactions. In orchestration, a coordinator component directs the workflow, telling each participant what to do and tracking progress. (We go deep on this distinction in the communication-patterns chapter; for now, know that choreography maximizes decoupling but scatters the workflow across many components, while orchestration centralizes the workflow at the cost of a coordinator that everything depends on.)
Good at: extreme decoupling — emitters and consumers don't know about each other, so you can add a new reaction to "ticket created" (say, a new analytics consumer) without touching the ticketing code at all. It absorbs bursts gracefully, because the queue buffers work that consumers process at their own pace, which makes it excellent for spiky load and for smoothing a fast producer feeding a slow consumer. And it's naturally resilient: if a consumer is down, events wait in the queue rather than being lost.
Costs: you lose the ability to reason about a workflow by reading a linear sequence of code — the flow is now scattered across emitters and consumers connected only by events, which makes the system genuinely harder to understand and debug. You inherit all the hard questions of async messaging: ordering (did these events arrive in the right sequence?), delivery guarantees (at-least-once? exactly-once?), and dead-letter handling (what happens to an event no consumer could process?). And eventual consistency is the default, not an option, because reactions happen after the fact. It is a powerful style that is easy to make a mess with.
Reach for it when: you have workflows with many independent reactions to the same occurrence, spiky or bursty load that benefits from buffering, or a need to add and remove consumers of an event without disturbing its producer. Many systems use it partially — a mostly-synchronous modular monolith or microservices system that uses events for the specific flows that benefit (notifications, analytics, audit logs) — which is often wiser than going fully event-driven everywhere.
Seeing them side by side
Walking the styles one at a time makes each clear but obscures the comparison. Here they are against the trade-offs that distinguish them — read this as a quick-reference, not a scorecard, because "high" is not the same as "good":
| Layered | Modular monolith | Microservices | Event-driven | |
|---|---|---|---|---|
| Partitioning | Technical | Domain | Domain | Domain (by event) |
| Communication | In-process, sync | In-process, sync | Network, sync + async | Async, via events |
| Deployment | One unit | One unit | Independent per service | Independent components |
| Change localization | Low | High | High | High |
| Operational complexity | Low | Low | High | High |
| Fault isolation | Low (shared fate) | Low (shared fate) | High | High |
| Local reasoning about a flow | Medium | High | Low | Low |
| Independent scaling | No | No | Yes | Yes |
| Honest "reach for it when" | Small system or team | The common default | Many teams, real scale needs | Many reactions, bursty load |
The single most important row is the last one. Most of the regret you'll see in real systems comes from a mismatch between the style's "reach for it when" and the team's actual situation — usually reaching for microservices or full event-driven when a modular monolith was what the situation called for. Which brings us to how you actually choose.
Choosing a style
There's no formula that takes your requirements and outputs the correct style — if there were, this would be a lookup table, not a skill. But there is a reliable process, and it's the trade-off reasoning from Chapter 1 applied at the structural scale: figure out which quality attributes this system most needs to be good at, understand your constraints, and pick the style whose trade-off bundle aligns — accepting that you're choosing what to be bad at, too.
A few questions drive most of the decision. They are deliberately not technical-elegance questions — they're about your situation, because the situation is what determines which trade-offs you can afford.
How many teams need to work on this independently? This is often the single most decisive question, and it's organizational, not technical. One team, even a large one, can move fast in a modular monolith and is usually slowed down by microservices. Many teams that block each other on a shared deployment are the textbook case for services. If you have one team and you're reaching for microservices, stop and find a different justification — "independent deployment" buys nothing when there's no one to be independent from.
What are the real scaling and fault-isolation needs? Not imagined ones — real ones. Does part of the system genuinely have load characteristics so different from the rest that scaling them together is wasteful or impossible? Does a failure in one capability genuinely need to be isolated from another for safety or compliance? If yes, that's a real pull toward independent deployment. If the honest answer is "everything scales together fine and a shared failure is acceptable," that pull isn't there, no matter how appealing the architecture diagrams look.
What's your operational maturity? Distributed systems demand capabilities a monolith doesn't: CI/CD per service, container orchestration, distributed tracing, service discovery, centralized logging, on-call practices that can diagnose cross-service failures. If your team doesn't have these and isn't resourced to build them, adopting microservices means failing at two hard things at once — the product and the platform. Be honest about what you can operate.
How well do you understand the domain? This one is underrated. Drawing service boundaries requires knowing where the domain's natural seams are — and early in a product's life, you usually don't know yet, because the domain is still being discovered. Boundaries drawn too early, on a misunderstood domain, become the worst kind of coupling: services that have to change together but can't, because they're separated by a network. This is the strongest argument for starting with a modular monolith even when you suspect you'll eventually want services — get the boundaries right in-process first, where they're cheap to move, and extract them only once they've proven stable.
That last point generalizes into the most useful default heuristic in this whole chapter:
Start simpler than the internet tells you to, and earn your way to complexity. The honest default for a new system built by a small-to-medium team is a modular monolith with clean domain boundaries. It gives you most of the structural benefits people want from microservices, at a fraction of the operational cost, and — because the domain boundaries are already drawn — it preserves the option to extract services later if and when a real, evidence-backed need appears. The reverse move is far more painful: teams that started with microservices and discovered they'd bought enormous complexity to solve problems they didn't have spend years trying to consolidate back, a journey painful enough that it has its own well-known name, the distributed monolith. A distributed monolith is a system split into separate deployable services that still have to be changed and deployed together because they're tightly coupled — the worst of both worlds, carrying all the operational cost of services and all the coupling of a monolith.
None of this means microservices are wrong. They're exactly right for the situations in their "reach for it when" — many teams, real differential scale, genuine fault-isolation needs, operational maturity to match. The point is that those situations are a specific, identifiable subset, not the universal endpoint of architectural maturity. Choose the style that fits where you actually are, document why in an ADR (you have the format now), and revisit the decision when your situation changes — because the right style for a system at ten engineers may genuinely not be the right style at a hundred. Architecture evolves; the goal isn't to pick the style you'll have forever, it's to pick the one that fits now and keep the seams that let you change later.
Practice
The skill this chapter builds is classifying a system by its real trade-off profile and reasoning about whether its style fits its situation. The exercises move from identifying styles in the wild to making and defending a recommendation. Budget about two hours.
Exercise 1: Classify a system you know (20 min)
Pick a system you work on or know well. Place it on the 2×2 grid: how is it partitioned (technical or domain), and how do its parts mostly communicate (sync or async)? Name its style. Then answer one question in two or three sentences: does its style match its situation — the number of teams, the scaling needs, the operational maturity — or is there a mismatch? Don't fix anything yet; just practice seeing the style and judging the fit.
Exercise 2: Argue the opposite style (45 min)
Take the same system and imagine it had been built in a different style — if it's a monolith, imagine microservices; if it's microservices, imagine a modular monolith. Write a short comparison: what would the alternative style have bought this system, and what would it have cost? Be specific to this system, not generic. The goal is to feel the trade-off concretely — to see that the alternative isn't simply better or worse, but a different bundle that would have helped some things and hurt others.
If you use an AI assistant to generate the alternative's pros and cons, treat the output as a starting list to challenge against what you actually know about the system's teams, load, and constraints — the same interrogate-don't-accept discipline from Chapter 1.
Exercise 3: Make a recommendation with an ADR (45 min)
Imagine a greenfield version of your system, or a real new project. Decide which style you'd start it in, and write an ADR (the format from Chapter 1) capturing the decision. The context section must name the situational facts that drove it — team count, scaling needs, operational maturity, domain maturity — and the consequences section must honestly state what the chosen style will be bad at, not just good at. If your consequences section has no real downsides, you've either chosen dishonestly or haven't finished thinking.
Deliverable
A one-page ADR recommending an architectural style for a real or realistic new system, with a context section grounded in situational facts (teams, scale, ops maturity, domain maturity) and a consequences section that honestly names what the style sacrifices. A reviewer will read it in about fifteen minutes and respond to one question: does the recommendation follow from the situation described, or from the appeal of the style itself?
Check yourself
Before you consider this chapter done, you should be able to:
- Place any system on the partitioning × communication grid and name its rough style.
- Explain why "microservices are more advanced than a monolith" is a category error.
- For each of the four styles, state what it's good at and what it costs, without notes.
- Name the situational questions that should drive a style choice — and explain why they're organizational and scale questions more than technical ones.
- Explain what a "distributed monolith" is and why it's the worst outcome.
If any feel shaky, reread that style's section and redo Exercise 1 on a second system — the concepts cement by classifying real systems, not by rereading.
What a strong answer demonstrates: a strong self-assessment derives the style from the situation — team count, real scaling and fault-isolation needs, operational and domain maturity — rather than from the appeal of the style itself, and its ADR's consequences section names what the chosen style will be bad at. The clearest signal you've understood the chapter is that you reach for the modular monolith as the honest default and can articulate exactly what evidence would justify earning your way to distribution.
Going deeper (optional)
This chapter distilled ideas from several sources. If you want the long form:
- Fundamentals of Software Architecture (Richards & Ford) — the chapters on architecture styles (layered, service-based, event-driven, microservices) walk each in far more detail, including styles this chapter skipped (microkernel, space-based, pipeline). Chapter 18, on choosing the appropriate style, is the direct expansion of this chapter's final section.
- Building Microservices (Newman), Chapters 1–3 — the definitive treatment of what microservices are, how to model their boundaries, and how to split a monolith into them when the time genuinely comes.
- Software Architecture: The Hard Parts — the whole book is about the trade-offs of going distributed; its early chapters on architectural modularity and decomposition are where the "earn your way to complexity" wisdom comes from. We return to it heavily in the synthesis and capstone chapters.
Primary sources (the foundational texts beneath the styles, if you want the theory on-ramp):
- Melvin E. Conway, "How Do Committees Invent?" (Datamation, April 1968) — the original Conway's Law paper, and the deepest reason style choice is an organizational question (how many teams) more than a technical one.
- David L. Parnas, "On the Criteria To Be Used in Decomposing Systems into Modules" (Communications of the ACM, December 1972) — the seminal case for decomposing by what changes together, which is exactly the domain-vs-technical partitioning dimension.
- James Lewis & Martin Fowler, "Microservices: a definition of this new architectural term" (martinfowler.com, 2014) — the article that pinned down what "microservices" actually means; the canonical reference behind this chapter's microservices section.
None of this is required to continue.
Key takeaways
- An architectural style is the top-level shape of a system — and it's a bundle of trade-offs, not a rung on a ladder. No style is more "advanced" than another.
- Two dimensions place almost any style: how it's partitioned (technical vs. domain) and how its parts communicate (sync vs. async).
- The four you'll meet most — layered, modular monolith, microservices, event-driven — each excel at some attributes and pay for it in others; the comparison table is your quick reference.
- Choosing is trade-off reasoning applied at scale, driven by situational questions: how many teams, what real scaling and fault-isolation needs, what operational and domain maturity.
- The honest default for most new systems is a modular monolith with clean domain boundaries. Start simpler than the hype suggests, keep the seams, and earn your way to distribution — going the other direction (the "distributed monolith") is far more painful.
Next chapter: Data Fundamentals — every style above has to put data somewhere, and the choices about how you model, index, and keep that data consistent shape the system as much as the style does. We turn from the shape of the code to the shape of the data underneath it.