Architecture styles: monoliths, microservices, events and cells
Every earlier module was about one building block. Architecture is about the whole: how many deployable pieces the system has, where the boundaries between them go, who owns which data, and how they talk. These choices are expensive to reverse, and fashion is a poor guide. This module compares the main styles honestly, shows how to move between them safely, and how to record the reasoning so it outlives the people who made it.
1. The idea
| Style | What it is | Strengths | Costs |
|---|---|---|---|
| Monolith | One deployable application with one database | Simple to build, test, deploy and debug; calls are function calls; transactions are easy | Everything scales and deploys together; a big codebase can tangle without discipline |
| Modular monolith | One deployable, with strict internal modules that own their data and talk through defined interfaces | Most of a monolith's simplicity, with clear boundaries you can later split along | Requires discipline to keep the boundaries; still one deployment |
| Microservices | Many small services, each owning its data, deployed independently, talking over the network | Independent deploys and scaling; failure isolation; different technologies where useful | Network calls (latency, failures), distributed data and transactions (sagas), much more operational work: observability, CI/CD, service discovery |
| Event-driven | Services react to events on a log or bus rather than calling each other directly | Loose coupling; easy to add consumers; natural audit trail and replay | Flows are harder to see and debug; eventual consistency; event schemas become contracts |
| Serverless | Functions and managed services run on demand; you don't manage servers | Scales to zero and to spikes; little operational work; pay per use | Cold starts, time limits, vendor lock-in, cost at sustained high load, harder local testing |
| Cell-based | Many identical, independent copies (cells) of the whole stack, each serving a subset of customers, with a thin routing layer in front | A failure or bad deploy affects one cell's customers only; scale by adding cells | Routing and cell placement to build; cross-cell features are harder; more infrastructure to run |
2. How it works
Where to draw boundaries
- Around business capabilities (orders, payments, catalogue, accounts), not technical layers (a "database service", a "validation service"). Each boundary should change for its own reasons.
- Each boundary owns its data. Other parts ask through its API or listen to its events; they never read its tables directly. Shared databases are how services end up coupled again.
- Keep things that change together, together. If two services must always be deployed together, or every request calls both, they're one service split in two: a distributed monolith, with the costs of both styles and the benefits of neither.
- Prefer asynchronous links between boundaries where the user doesn't need an immediate answer (events, queues), and keep synchronous call chains short (try section 3).
Migrating without a rewrite: the strangler fig
Big-bang rewrites are notoriously risky. Instead, grow the new system around the old one, piece by piece, like a strangler fig around a tree. Press Play.
Recording the decision
Architecture choices outlive the reasons for them. Write them down where the code lives:
- A design document before building something significant (Module 05): context, goals and non-goals, the proposal, alternatives considered, risks, rollout.
- An architecture decision record (ADR) for each important choice: a page with the context, the decision, the alternatives, and the consequences. Numbered, kept in the repository, never edited after acceptance; superseded by a new ADR if the decision changes.
# ADR 0007: Use PostgreSQL row-level security for tenant isolation
Status: Accepted (2026-10-15)
Context: 3,000 small tenants share tables; a missed tenant filter would leak data.
Decision: Enforce tenant_id with RLS policies; the app sets app.tenant_id per connection.
Alternatives: schema per tenant (migration cost ×3,000), app-only filters (one bug from a leak).
Consequences: every query is protected by default; connection pooling must reset the setting.
3. Try it yourself
The cost of a synchronous call chain
A request passes through services in a row. Each network hop adds latency and is another thing that can fail.
Which style fits?
4. Trade-offs
Start simple
Many successful systems began as monoliths and split later along boundaries they'd discovered. Splitting before you understand the domain usually puts the boundaries in the wrong place.
Microservices are an operational commitment
They pay off when independent deployment, scaling or failure isolation is worth the cost of networking, observability, CI/CD per service and distributed data.
Events decouple, and hide
Adding a consumer needs no change to the producer, which is great, until nobody can say what happens when an order is placed. Document flows and trace events.
Cells cap the damage
Cells turn "the whole service is down" into "2% of customers are affected". Worth it for large, critical, multi-tenant services; overkill for most others.
5. In real systems
- Modular monoliths are a common, deliberate choice for products of moderate size: Shopify has written publicly (Deconstructing the Monolith) about keeping a large Rails monolith modular rather than splitting it into services.
- Microservices at scale (Netflix, Uber, Amazon) come with heavy platform investment: service meshes, tracing, deployment pipelines, contract testing and internal developer platforms.
- Cell-based architecture is described in AWS's Well-Architected guidance Reducing the Scope of Impact with Cell-Based Architecture; large SaaS and cloud providers use it to limit blast radius.
- Event-driven systems usually centre on Kafka or a managed bus (EventBridge, Pub/Sub), with schemas managed in a registry so producers can't break consumers.
- Hexagonal (ports and adapters) or clean architecture inside each service keeps business logic independent of frameworks and databases, which makes splitting and testing easier, whatever the overall style.
The patterns up close
Modular monolith
The default starting pointOne deployable application with strict internal modules that own their data and talk through defined interfaces.
How it works
Modules (orders, payments, catalogue) can't read each other's tables; they call each other's public functions or publish in-process events. Calls are fast and transactions easy, and the boundaries are already drawn if one module later needs to become a service.
Status today
A deliberate, widely used choice (Shopify writes publicly about its modular Rails monolith); tooling exists to enforce module boundaries.
Use it when
- New products, small to medium teams
- Domains whose boundaries aren't settled yet
Skip it when
- Many teams that need independent deploys and scaling
In the interview
A modular monolith first, with modules along these capabilities; we'd extract a service when a team or scaling need demands it.
- Calling it a monolith but letting modules share tables: the boundaries are fiction.
Microservices
Situational · many teams, real scaleIndependently deployed services, each owning its data, talking over the network.
How it works
Each service has its own store and API; synchronous calls for what the user waits on, events for everything else. The cost is the platform: service discovery, tracing, CI/CD per service, contract tests, sagas for cross-service workflows.
Status today
The norm at large companies (Netflix, Uber, Amazon), backed by heavy platform investment.
Use it when
- Many teams that must ship independently
- Parts with very different scaling or reliability needs
Skip it when
- A small team or an early product: the operational cost outweighs the benefit
In the interview
Services along capabilities (users, posts, feed, notifications), each with its own store; the feed is updated from post events.
- A distributed monolith: services that must deploy together or call each other on every request.
- Shared databases between services.
Event sourcing
Specialised · audit-heavy domainsStore every change as an immutable event, and derive the current state by replaying the events, instead of overwriting rows.
How it works
- An account isn't a
balancecolumn; it'sOpened,Deposited 100,Withdrew 30… Current state = replay (with periodic snapshots so you don't replay from the start). - You get a complete audit trail, time travel ("what was the balance on 1 March?"), and new views built later by replaying history.
- The costs: event schemas are forever (versioning them is real work), queries need separate read models, and deleting personal data needs care (crypto-shredding).
Status today
Used in finance, accounting ledgers, some booking and logistics systems; tools include EventStoreDB and Kafka with compaction, often paired with CQRS.
Use it when
- Audit is a core requirement (ledgers, compliance)
- You need to rebuild state or new views from history
Skip it when
- Ordinary CRUD apps: it's more work for no gain
- Most interview prompts unless audit or replay is explicit
In the interview
Only when audit or replay is a requirement: The ledger is event-sourced: every movement is an immutable event, balances are projections, and we snapshot daily.
- Event sourcing everything by default; interviewers will ask about schema evolution, replay time and deleting user data.
- Confusing it with event-driven architecture, which just means services communicate through events.
CQRS (command query responsibility segregation)
Situational · very different read and write needsSeparate the model that handles writes (commands) from the models that serve reads (queries), keeping read models up to date from events.
How it works
Writes go to a normalised store that enforces the rules. Each change publishes an event (outbox or CDC), and projectors update read models shaped for each screen: a search index, a denormalised feed table, a cache. Reads are fast and simple; they're eventually consistent with the writes.
Status today
Common in practice even without the name: "Postgres for writes, Elasticsearch and Redis for reads, fed by CDC" is CQRS.
Use it when
- Reads and writes with very different shapes or scale
- Several read views of the same data (search, feed, analytics)
Skip it when
- Simple CRUD where one model serves both
In the interview
Writes go to PostgreSQL; CDC feeds the search index and the feed cache, so reads are fast and a few seconds behind.
- CQRS without saying reads are eventually consistent, then showing a user their own stale write.
Cell-based architecture
Specialised · limiting blast radiusMany identical, independent copies of the whole stack, each serving a subset of customers, behind a thin router.
How it works
Customers are assigned to cells (by tenant or ID); a bad deploy or a poison request takes down one cell, not everyone. Deploys roll out cell by cell. Cross-cell features (global search, admin views) need separate handling.
Status today
Described in AWS's Well-Architected guidance; used by large SaaS and cloud providers.
Use it when
- Very large multi-tenant products where a global outage is unacceptable
Skip it when
- Small and medium products: it multiplies the infrastructure
In the interview
At this scale, tenants are spread over cells of the full stack, so an incident affects one cell's customers.
- Cells for a startup-sized prompt.
6. Remember it
How this hook works: what "One kitchen, food court, or chain of branches" means
What the slogan means, phrase by phrase
- "One kitchen"
- A (modular) monolith: one deployable, everything reachable in-process, simplest to run.
- "food court"
- Microservices: independent stalls, each with its own kitchen (data) and opening hours (deploys), at the cost of queues between them (network calls).
- "chain of branches"
- Cells: identical self-contained copies, each serving some customers, so one branch's fire stays local.
Why this picture fits: the trade-offs map one to one: shared space versus independence, and the cost of a meal from several stalls is the cost of a request through several services.
The story → the concept
| Food | Architecture |
|---|---|
| One kitchen, everyone reaches everything | A monolith with one database |
| Kitchen stations with their own ingredients | A modular monolith |
| Food court stalls | Microservices owning their data |
| Queueing at three stalls for one meal | A synchronous call chain |
| A noticeboard stalls react to | Event-driven communication |
| Identical branches in different towns | Cells |
Watch it on an example
- A marketplace starts as a modular monolith: accounts, listings, orders and payments as modules with separate tables.
- Search traffic grows 50× faster than everything else: search becomes its own service, fed by events, scaled on its own.
- Payments needs stricter security reviews and audits: it becomes a separate service with its own deploy pipeline.
- Everything else stays in the monolith, which is still the simplest place for it.
The trap it saves you from
Thirty microservices for a product with three main features, all deployed together, all calling each other synchronously: a distributed monolith.
It still works when…
You design AI systems (Module 29): a single service calling a model API is often right; separate the model-serving tier only when its hardware, scaling or release cycle genuinely differs.
7. Common mistakes
8. Design exercise (30 minutes)
- Draw boundaries for a food delivery platform: list the capabilities, which data each owns, which talk synchronously and which through events.
- Find the distributed monolith: in your design, is there a pair of services that every request calls together? Merge them or justify them.
- Plan a strangler migration for moving "notifications" out of a monolith: the routing step, the data migration, the switch-over and the clean-up.
- Write an ADR (half a page) for one choice in your design, using the template above.
- Use the calculator: how many services can a request pass through in a row before p99 exceeds 500 ms with 10 ms hops?
9. Self-check quiz
Services that must always be deployed together and call each other on every request are…
How should the orders service get a customer's address owned by the accounts service?
What's the strangler fig approach?
What's the main benefit of a cell-based architecture?
What's the difference between event sourcing and event-driven architecture?
Postgres for writes, with CDC feeding Elasticsearch and a Redis feed cache for reads. What pattern is this?
10. Go deeper
| Resource | Time | Why |
|---|---|---|
| Microservices and MonolithFirst (Martin Fowler) | 45 min | The defining article, and the argument for starting simple. |
| Strangler Fig Application | 10 min | The incremental migration pattern. |
| Reducing the Scope of Impact with Cell-Based Architecture (AWS) | 1 h | How cells, routing and cell sizing work in practice. |
| Documenting Architecture Decisions (Michael Nygard) | 10 min | The short post that introduced ADRs. |
| Hexagonal architecture (Alistair Cockburn) | 20 min | Ports and adapters: keeping business logic independent of infrastructure. |
11. Under the hood (optional)
Mechanisms behind specialist areas that come up in specific designs. Not needed for most interviews; open it when you want to know how the products really do it. All the deep dives are listed on the Under the hood page.
Everything elseDijkstra vs A* with a live grid lab, contraction hierarchies, push and email delivery, recommendation pipelines, edge and offline, file syncabout 20 min
A few specialist areas come up in specific designs. Each has one or two mechanisms worth knowing before an interview that touches it; the worked designs go further.
Maps and routing: Dijkstra vs A*
Shortest paths on a road graph. Dijkstra explores outward in every direction by distance; A* adds an estimate of the remaining distance (straight-line or Manhattan) so it explores mostly toward the goal. Click cells to add or remove walls:
- Continental road networks are too big even for A* per query. Contraction hierarchies precompute shortcuts around unimportant nodes, so queries search only "upward" toward major roads from both ends: milliseconds for routes across a continent (OSRM, GraphHopper). Live traffic needs customizable variants that update edge weights quickly.
- Map matching snaps noisy GPS points to roads with a hidden Markov model; vector tiles ship map geometry in small pre-cut tiles that the client styles and renders (Design D25).
Push notifications and email
| Channel | How it works | What to design for |
|---|---|---|
| APNs (Apple), FCM (Google) | Your server sends to the platform with a device token; the platform delivers over its own persistent connection to the device | Best effort, no delivery guarantee; tokens expire (remove them on errors); collapse keys replace older pending messages; respect TTLs and quiet hours |
| Email (SMTP) | A provider relays to the recipient's mail servers | SPF, DKIM and DMARC authenticate you; Gmail and Yahoo require them for bulk senders, plus one-click unsubscribe and low spam rates; handle bounces and complaints; warm up new sending IPs |
| SMS | Aggregators deliver through carriers | Cost per message, country rules, registration of sender IDs |
Design D05 builds a notification system with preferences, deduplication, retries and per-channel rate limits.
Recommendations and feeds
- Two stages: candidate generation narrows millions of items to hundreds cheaply (often a two-tower model: users and items embedded in the same space, then approximate nearest-neighbour search); ranking scores those hundreds with a heavier model using many features. A final pass applies business rules and diversity.
- Feature stores serve the same features offline (training) and online (low-latency lookups at request time), avoiding training/serving skew; real-time features ("items viewed in the last 5 minutes") come from stream processing.
- Designs D06 (news feed) and D29 (Q&A ranking) go through fan-out and ranking in detail.
Edge and offline
- Edge functions (Cloudflare Workers, Lambda@Edge, Fastly Compute) run code in CDN locations: authentication checks, redirects, personalisation, A/B assignment, close to users.
- Edge storage: Workers KV is eventually consistent (writes can take up to about a minute to appear everywhere), good for configuration and caches; Durable Objects give one single-threaded instance per key for coordination that needs a single writer (a chat room, a counter).
- Offline-first apps keep a local database and sync: CouchDB/PouchDB track document revision trees and surface conflicts; Firestore caches offline writes and replays them; CRDT libraries merge automatically (Module 21's deep dive).
File sync
Sync clients split files into chunks named by their hashes, upload only chunks the server lacks, and compare trees of hashes (Merkle trees) to find what changed. rsync's rolling checksum finds matching blocks at any offset; content-defined chunking keeps boundaries stable after inserts (the chunking lab in Module 10's deep dive shows why). Design D12 builds the full system.
Say it in the interview
- "How does a routing service answer in milliseconds?" Precomputed contraction hierarchies over the road graph, with traffic applied as edge-weight updates; A* or bidirectional search alone is too slow at continental scale.
- "Do push notifications guarantee delivery?" No: APNs and FCM are best effort, so important messages also go to an in-app inbox the client syncs.
Why does A* usually explore fewer nodes than Dijkstra?