← All posts

Architectural Styles and Their Tradeoffs

Monoliths, modular monoliths, microservices, event-driven systems, and serverless — what each buys you, what each costs, and how to choose.

architecture
tech-lead
engineering

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 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:

Start here, escalate only when forcedDefault: Modular MonolithSimple ops + clean boundaries. Fits most teams for a long time.Move toward microservices only when you can answer YES to:• Are teams genuinely blocking each other on one shared deploy pipeline?• Do parts of the system have truly different scaling profiles?• Can you afford the operational tax (tracing, discovery, many pipelines)?• Are your service boundaries already proven by clear module seams?Reach for event-drivenwhen the domain is event-shaped or needstemporal decoupling & spike absorption.Reach for serverlessfor spiky, unpredictable, or low-volumework where idle cost should be ~zero.

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.