InterviewKata
System design · Module 28 of 29 · Level 5: Production and architecture

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

A small restaurant has one kitchen where everyone can reach everything: fast and simple. A food court has separate stalls, each with its own kitchen, menu and opening hours: one stall can close for repairs without affecting the others, but a meal from three stalls needs three queues. A chain builds identical, self-contained branches, so a fire in one branch only affects that branch's customers.
StyleWhat it isStrengthsCosts
MonolithOne deployable application with one databaseSimple to build, test, deploy and debug; calls are function calls; transactions are easyEverything scales and deploys together; a big codebase can tangle without discipline
Modular monolithOne deployable, with strict internal modules that own their data and talk through defined interfacesMost of a monolith's simplicity, with clear boundaries you can later split alongRequires discipline to keep the boundaries; still one deployment
MicroservicesMany small services, each owning its data, deployed independently, talking over the networkIndependent deploys and scaling; failure isolation; different technologies where usefulNetwork calls (latency, failures), distributed data and transactions (sagas), much more operational work: observability, CI/CD, service discovery
Event-drivenServices react to events on a log or bus rather than calling each other directlyLoose coupling; easy to add consumers; natural audit trail and replayFlows are harder to see and debug; eventual consistency; event schemas become contracts
ServerlessFunctions and managed services run on demand; you don't manage serversScales to zero and to spikes; little operational work; pay per useCold starts, time limits, vendor lock-in, cost at sustained high load, harder local testing
Cell-basedMany identical, independent copies (cells) of the whole stack, each serving a subset of customers, with a thin routing layer in frontA failure or bad deploy affects one cell's customers only; scale by adding cellsRouting and cell placement to build; cross-cell features are harder; more infrastructure to run

2. How it works

Where to draw boundaries

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:

# 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.

5
8 ms
99.9%
5× its p50

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

The patterns up close

The default answer in an interviewDraw services along the main capabilities of the prompt (users, posts, feed, notifications), each with its own store, connected by synchronous calls for what the user waits on and events for everything else. Say briefly why it isn't one service, or why it is. Reach for event sourcing or CQRS only when audit, replay or very different read and write needs are in the requirements.

Modular monolith

The default starting point

One 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

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.

🚩 Red flags
  • Calling it a monolith but letting modules share tables: the boundaries are fiction.

Microservices

Situational · many teams, real scale

Independently 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

In the interview

Services along capabilities (users, posts, feed, notifications), each with its own store; the feed is updated from post events.

🚩 Red flags
  • A distributed monolith: services that must deploy together or call each other on every request.
  • Shared databases between services.

Event sourcing

Specialised · audit-heavy domains

Store 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 balance column; it's Opened, 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

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.

🚩 Red flags
  • 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 needs

Separate 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)

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.

🚩 Red flags
  • CQRS without saying reads are eventually consistent, then showing a user their own stale write.

Cell-based architecture

Specialised · limiting blast radius

Many 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

In the interview

At this scale, tenants are spread over cells of the full stack, so an incident affects one cell's customers.

🚩 Red flags
  • Cells for a startup-sized prompt.

6. Remember it

Memory hook: "One kitchen, food court, or chain of branches"Choose how many independent pieces the system needs, draw each boundary around a business capability that owns its data, and split only when a real need (deploys, scale, isolation) outweighs the cost.
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

FoodArchitecture
One kitchen, everyone reaches everythingA monolith with one database
Kitchen stations with their own ingredientsA modular monolith
Food court stallsMicroservices owning their data
Queueing at three stalls for one mealA synchronous call chain
A noticeboard stalls react toEvent-driven communication
Identical branches in different townsCells

Watch it on an example

  1. A marketplace starts as a modular monolith: accounts, listings, orders and payments as modules with separate tables.
  2. Search traffic grows 50× faster than everything else: search becomes its own service, fed by events, scaled on its own.
  3. Payments needs stricter security reviews and audits: it becomes a separate service with its own deploy pipeline.
  4. 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

A shared database between servicesServices that read each other's tables can't change independently. Each boundary owns its data; others use its API or events.
Splitting by technical layerA "data access service" or "validation service" puts every feature change across several services. Split by business capability.
Long synchronous chainsTen services in a row multiply latency and failure. Make non-essential steps asynchronous.
The big-bang rewriteRewriting everything at once delays value for months and usually misses behaviour the old system had. Migrate incrementally.
Undocumented decisionsWithout a record, later engineers can't tell a deliberate trade-off from an accident, and either preserve mistakes or break deliberate choices.

8. Design exercise (30 minutes)

  1. Draw boundaries for a food delivery platform: list the capabilities, which data each owns, which talk synchronously and which through events.
  2. Find the distributed monolith: in your design, is there a pair of services that every request calls together? Merge them or justify them.
  3. Plan a strangler migration for moving "notifications" out of a monolith: the routing step, the data migration, the switch-over and the clean-up.
  4. Write an ADR (half a page) for one choice in your design, using the template above.
  5. 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…

All the costs of distribution with none of the independence. Merge them, or redraw the boundary.

How should the orders service get a customer's address owned by the accounts service?

The owner's interface is the contract. (Orders may keep its own copy, fed by events, if it needs one.)

What's the strangler fig approach?

A routing layer sends one capability at a time to the new code, until the old system can be removed.

What's the main benefit of a cell-based architecture?

Each cell is an independent copy of the stack; a bad deploy or failure in one doesn't reach the others.

What's the difference between event sourcing and event-driven architecture?

You can have event-driven services with ordinary tables, and an event-sourced module inside a monolith.

Postgres for writes, with CDC feeding Elasticsearch and a Redis feed cache for reads. What pattern is this?

Separate write and read models, kept in step by change events.

10. Go deeper

ResourceTimeWhy
Microservices and MonolithFirst (Martin Fowler)45 minThe defining article, and the argument for starting simple.
Strangler Fig Application10 minThe incremental migration pattern.
Reducing the Scope of Impact with Cell-Based Architecture (AWS)1 hHow cells, routing and cell sizing work in practice.
Documenting Architecture Decisions (Michael Nygard)10 minThe short post that introduced ADRs.
Hexagonal architecture (Alistair Cockburn)20 minPorts 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

ChannelHow it worksWhat 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 deviceBest 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 serversSPF, 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
SMSAggregators deliver through carriersCost 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

Answers that show depth
  • "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?

With an estimate that never overestimates, it still finds the shortest path.

Sources