Architectural Styles and Their Tradeoffs
Most arguments about architecture are really arguments about which problems you're willing to have. Every style solves something and creates something else. The job of a lead is not to pick the "modern" option but to pick the option whose problems you can afford.
This chapter walks the main styles from simplest to most distributed, then gives you a way to choose.
The monolith
A monolith is a single deployable unit: one codebase, one build, one process (or a set of identical processes behind a load balancer) that contains all of the application's logic. It has an unfairly bad reputation. For most teams below a certain size, it is the correct starting point.
What it buys you: a single place to look, atomic refactors across the whole system, no network between your components (so no network failures, no serialization overhead, no distributed transactions), trivial local development, and one thing to deploy and monitor.
What it costs you: as the codebase and team grow, everything becomes entangled if you aren't disciplined; one slow or buggy module can take down the whole process; you scale the entire application even if only one part is hot; and a single deploy pipeline becomes a queue that the whole team waits in.
The failure mode people fear is the big ball of mud — a monolith with no internal boundaries, where everything reaches into everything. But that's a discipline problem, not an inherent property of monoliths. Which leads to the option most teams should reach for before microservices.
The modular monolith
A modular monolith is a single deployable unit with strong internal boundaries. Modules talk to each other through explicit, narrow interfaces, not by reaching into each other's internals or sharing each other's database tables. It's still one process, one deploy — but internally it's organized as if it could be pulled apart later.
This is the sweet spot for a surprising number of teams. You keep nearly all the operational simplicity of a monolith (no network, easy local dev, atomic deploys) while getting the conceptual clarity that makes a system understandable and changeable. And critically, if you ever do need to extract a service, well-defined module boundaries are exactly the seams you'd cut along. A modular monolith is the best possible preparation for microservices, and often it means you never need them.
flowchart LR
subgraph MM["Modular Monolith — one process"]
direction TB
O[Orders module] -->|public interface| P[Payments module]
O -->|public interface| I[Inventory module]
P -.->|no direct DB access| I
end
MM --> DB[(Shared DB,<br/>but module-owned schemas)]
Microservices
Microservices split the system into independently deployable services, each owning its own data, communicating over the network (HTTP, gRPC, or messaging). Each service can be built, deployed, scaled, and even written in a different language by a different team.
What they buy you: independent deployment (teams ship on their own cadence without a shared queue), independent scaling (scale only the hot service), fault isolation (one service degrading needn't take down others, if you design for it), and clear ownership boundaries that scale with org size.
What they cost you — and this list is the whole point:
- The network is now in your design. Calls fail, time out, and arrive out of order. Everything that was a function call is now a fallible remote call.
- Data consistency gets genuinely hard. You can no longer wrap a change across services in one database transaction. You're now in the world of eventual consistency, sagas, and compensating actions (see Chapter 3).
- Operational overhead explodes. Service discovery, distributed tracing, dozens of deploy pipelines, more infrastructure to run and secure.
- Debugging spans machines. A single user action might touch eight services; understanding what went wrong requires correlation across all of them.
The honest rule of thumb: adopt microservices when organizational and scaling pain forces you to — when teams genuinely block each other on a shared deploy, or when parts of the system have wildly different scaling needs. Adopting them earlier means paying all of the costs to buy benefits you don't yet need. "Microservices" is an answer to a people-and-scale problem at least as much as a technical one — see Conway's Law in Chapter 6.
Event-driven architecture
Instead of services calling each other directly (request/response), components communicate by emitting and reacting to events through a message broker or log (e.g. a queue or a streaming platform). A service announces "an order was placed"; other services react in their own time.
What it buys you: loose coupling in time and knowledge — the order service doesn't know or care who consumes the event; you can add new consumers without touching the producer. It absorbs load spikes (the queue buffers), and it naturally models domains that are event-shaped (audit logs, analytics, workflows).
What it costs you: the system's behavior becomes emergent and harder to follow — there's no single call stack to read; you reason about choreography instead. Debugging "why didn't this happen?" is harder. You inherit eventual consistency by default, plus the need to handle duplicate and out-of-order events (idempotency, covered in Chapter 4).
flowchart LR
A[Order Service] -->|"OrderPlaced event"| B(((Message Broker)))
B --> C[Email Service]
B --> D[Inventory Service]
B --> E[Analytics Service]
B -. "add new consumers<br/>without touching producer" .-> F[Loyalty Service]
Serverless / Functions-as-a-Service
Serverless pushes the unit of deployment down to individual functions that the platform runs on demand, scaling from zero to many automatically and billing per invocation. "Serverless" is a misnomer — there are servers, you just don't manage them.
What it buys you: no infrastructure to provision, automatic scaling, and a cost model that's near-zero when idle — excellent for spiky, unpredictable, or low-volume workloads, and for small teams who'd rather not run servers.
What it costs you: cold starts (latency when a function spins up from idle), execution-time and resource limits, vendor lock-in (your functions are written against a specific cloud's primitives), and an operational model that can become a sprawl of hard-to-trace functions. Cost can also flip: at sustained high volume, per-invocation pricing may dwarf the cost of just running a server.
Choosing: a practical guide
There's no flowchart that outputs the answer, but these questions get you most of the way:
These styles also compose. A real system is often a modular monolith for the core, a couple of extracted services where scaling demanded it, an event stream for analytics, and a few serverless functions for glue and scheduled jobs. Purity is not the goal; fit is.
What this means for you as a lead
When someone on your team proposes splitting things into services, your job isn't to say yes or no — it's to surface the costs in the list above and ask which problem the split actually solves. If the honest answer is "it's cleaner" or "it's how modern systems are built," that's a signal to slow down. If the answer is "the payments team and the catalog team keep colliding on deploys and payments needs to scale 10x independently," that's a real reason. Make the team articulate the forcing function before paying the distributed-systems tax.