Lessons
1 · System Design Foundations
2 · APIs and Communication
3 · Capacity Estimation
4 · Core Building Blocks
5 · Databases
- 12Relational Databases and Data Modeling for System Design
- 13Database Indexes and Query Performance: How to Make Reads Fast
- 14Transactions, Isolation Levels and Concurrency Control: Preventing Race Conditions
- 15Database Replication: Primary-Replica, Read Replicas, Lag and Failover
- 16Database Sharding and Partitioning: How It Works and When You Need It
- 17SQL vs NoSQL: Data Models and How to Choose the Right Database
6 · Distributed Systems
- 18Why Distributed Systems Are Hard: Partial Failure, Timeouts and Clocks
- 19CAP Theorem and Consistency Models Explained Without the Myths
- 20Idempotency, Retries and Message Delivery Guarantees
- 21Distributed Transactions: Two-Phase Commit, the Outbox Pattern and Sagas
- 22Distributed Locks, Leases, Leader Election and Consensus
7 · Reliability and Operations
8 · Architecture and Security
9 · Interview Method and Foundation Case Studies
10 · Real-World Case Studies
- 33Design a Notification System: Email, SMS and Push at Scale
- 34Design a Chat System Like a Messaging App
- 35Design a News Feed: Fan-Out, Ranking and the Celebrity Problem
- 36Design Search Autocomplete (Typeahead)
- 37Design a Distributed Job Scheduler
- 38Design a Ticket Booking System Without Overselling
- 39Design a Payment System: Idempotency, Ledgers and Reconciliation
11 · Large-Scale Case Studies
- 40Design a Video Streaming Platform: Upload, Transcoding and Adaptive Streaming
- 41Design a Ride-Hailing System: Location, Matching and Real-Time Updates
- 42Design a Food Delivery Platform: Orders, Restaurants and Delivery Partners
- 43Design an E-Commerce System: Catalog, Cart, Checkout, Inventory and Flash Sales
- 44Design a Web Search Engine: Crawling, Inverted Indexes and Ranking
12 · Interview Readiness
Lesson 26 · Architecture and Security
Monolith vs Modular Monolith vs Microservices: How to Choose
An honest comparison of monoliths, modular monoliths and microservices: what each gives and costs, why a modular monolith is often the best start, how to draw service boundaries and own data, synchronous versus asynchronous communication, API gateways, service discovery, service meshes, the distributed-monolith trap, and migrating with the strangler fig pattern.
For
SDE-2 candidates asked to justify an architecture style, and developers and tech leads deciding whether and how to split an application into services.
Level
Before you start
Read Load Balancers (article 08), Message Queues (article 11) and Distributed Transactions (article 21). Observability (article 25) explains one of the main costs of many services.
On this page
Key takeaways
- Microservices buy independent deployment, team autonomy, independent scaling and fault isolation, at the cost of network calls, distributed data, harder testing and much more operational work.
- A modular monolith (one deployable with strict internal boundaries and module-owned data) is often the best starting point, and makes later extraction straightforward.
- Draw boundaries around business capabilities, give each service its own data, and prefer asynchronous events for propagating changes between services.
- API gateways centralise cross-cutting edge concerns, and service discovery lets callers find healthy instances; both must stay thin and highly available.
- Services that share a database or must deploy together are a distributed monolith: all the costs of microservices with none of the benefits.
A startup with six engineers builds its first product as fourteen microservices, because "that is how big companies do it". Six months later, a one-line change to how usernames are displayed requires coordinated deployments of four services, local development needs twenty containers, every bug hunt crosses three repositories and five dashboards, and a single slow service regularly takes the product down. The team spends more time on plumbing than on features.
Meanwhile, a different company with three hundred engineers runs one enormous application that every team deploys together once a week; a bug in any team's code blocks everyone's release.
Both teams chose an architecture that did not fit their situation. This article compares the options honestly, explains how to draw boundaries if you do split, and covers the infrastructure that comes with services: API gateways, service discovery and service meshes.
What you will learn
#- What monoliths, modular monoliths and microservices are.
- What microservices buy, and what they cost.
- Why a modular monolith is often the right first architecture.
- How to choose, based on team, domain and requirements.
- How to draw service boundaries and own data.
- Synchronous and asynchronous communication between services.
- API gateways, service discovery and service meshes.
- The distributed-monolith anti-pattern, and migrating with the strangler fig pattern.
Three styles
#Monolith
#One application, built and deployed as a single unit, usually with one database. All features live in one codebase and call each other with ordinary function calls.
Modular monolith
#Still one deployable unit, but internally divided into modules with strict boundaries: each module has a public interface, hides its internals, and owns its own tables (other modules do not read or write them directly). Modules call each other through their interfaces, in the same process.
Microservices
#The application is split into independently deployable services, each owning its data and running in its own processes, communicating over the network. Martin Fowler and James Lewis's article describes the style as building an application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, built around business capabilities and independently deployable (Microservices). Microsoft's architecture guidance describes the same characteristics and lists the challenges that come with them (Microservices architecture style).
MONOLITH MODULAR MONOLITH MICROSERVICES+-------------+ +---------------------+ [Accounts]--DB| everything | | Accounts | Photos | [Photos]----DB network| one DB | | Social | Feed | [Social]----DB <-- calls and+-------------+ | Notify | | [Feed]------DB events | each owns its tables | [Notify]----DB +---------------------+ one deployable, one database (separate schemas per module)What microservices buy
#Benefit | What it means | Matters when |
|---|---|---|
Independent deployment | A team releases its service without coordinating with others | Many teams, frequent releases |
Team autonomy | A team owns a service end to end, including its data and on-call | Organisation has grown beyond a few teams |
Independent scaling | Scale the hot service, not the whole application | Parts of the system have very different load profiles |
Fault isolation | A crash or leak in one service does not take the process of others down (if designed with bulkheads and timeouts) | Some features are risky or resource-hungry |
Technology choice per service | Use a different language or store where it fits | Rarely the main reason; often a cost |
A widely cited observation, often called Conway's law, is that systems tend to mirror the communication structure of the organisations that build them. Microservices work best when service boundaries match team boundaries, so that most changes stay inside one team's services.
What microservices cost
#Cost | Why |
|---|---|
Network calls instead of function calls | Every call can be slow, fail or time out (article 18); latency adds up |
Distributed data | No transactions across services; sagas, outbox, eventual consistency (article 21) |
Operations | Many deployables to build, deploy, configure, scale, secure and monitor |
Observability | You need distributed tracing and centralised logs just to debug a request (article 25) |
Testing | End-to-end tests need many services running; contract tests between them |
Versioning | Service APIs change independently; compatibility must be managed (article 04) |
Local development | Running the system on a laptop gets harder |
Cognitive load | Understanding a feature means understanding several services and their interactions |
These costs are paid up front and continuously, while the benefits arrive only when the organisation and system are large enough to need them.
Why a modular monolith is often the right start
#Martin Fowler's "Monolith First" essay observes that many successful microservice systems started as monoliths that were later split, and that starting with microservices risks drawing boundaries before the domain is understood (MonolithFirst). The essay also acknowledges counter-arguments; the point is not that microservices are wrong, but that service boundaries are expensive to move, and early in a product's life you do not yet know where they belong.
A modular monolith gives you most of the design discipline at a fraction of the cost:
- Clear boundaries and ownership inside one codebase.
- Module-owned data: each module has its own schema or tables; no cross-module joins in application code.
- In-process calls: fast, no network failures, real transactions across modules where genuinely needed.
- One deployment, one thing to monitor.
- Extraction later is mechanical: a module with a clean interface and its own tables can become a service when there is a reason.
A modular-monolith boundary in Java might look like this (an illustration):
// photos module: the only thing other modules may usepublic interface PhotoCatalog { PhotoSummary find(long photoId); List<PhotoSummary> recentByOwner(long ownerId, Cursor cursor, int limit);}// feed module depends on the interface, never on photos' tables or internal classesclass FeedBuilder { private final PhotoCatalog photos; FeedBuilder(PhotoCatalog photos) { this.photos = photos; }}Build tools and architecture tests can enforce that modules only depend on each other's public interfaces.
How to choose
#Question | Points towards a (modular) monolith | Points towards microservices |
|---|---|---|
How many engineers and teams? | One to a few teams | Many teams that block each other |
How well is the domain understood? | Still being discovered | Stable, well-understood boundaries |
Do parts scale very differently? | Similar load profiles | One part needs 50 times the capacity of others |
Do parts need isolation? | Similar risk and compliance | A part has strict security, compliance or reliability needs |
Release cadence | Shared cadence is acceptable | Teams need to release independently many times a day |
Operational maturity | Limited tooling for CI/CD, tracing, on-call | Mature platform: automated deployment, observability, service templates |
A reasonable default: start with a modular monolith, keep asynchronous and resource-heavy work (media processing, notifications) in separate workers from day one, and extract services when a specific pressure (team independence, scaling, isolation) justifies the cost.
Drawing service boundaries
#When you do split, boundaries matter more than anything else.
- Split by business capability, not technical layer. "Payments", "Catalogue", "Orders", "Notifications" are good services. "Database service", "Validation service" and "Frontend-facing service" are not: every feature would need to change all of them.
- Each service owns its data. The microservices.io description of the "Database per service" pattern explains why: a service's data is private and accessed only through its API, so services stay loosely coupled and can change their schemas independently (Database per service). Ownership can mean a separate database, or separate schemas with strict access rules.
- High cohesion, low coupling: things that change together belong together. If two services must always be deployed together, they are probably one service.
- Keep transactions inside a service. If an operation must be atomic, its data should live in one service (article 21).
Querying across services
#Without shared tables, how do you answer "orders with customer names and product titles"?
- API composition: a service (or the gateway) calls the order, customer and product services and combines the results. Simple; adds latency and failure points.
- Replicated read models: a service subscribes to other services' events and keeps its own read-optimised copy (CQRS, article 27). Fast reads; eventually consistent.
Communication between services
#Style | Use for | Watch out for |
|---|---|---|
Synchronous (REST or gRPC) | Queries and commands where the caller needs the answer now | Chains of calls: latency and availability multiply; use timeouts, breakers, bulkheads (article 23) |
Asynchronous (events and queues) | Propagating changes, triggering follow-up work, decoupling in time | Eventual consistency, duplicates, ordering; outbox and idempotent consumers (articles 20 and 21) |
A healthy pattern: synchronous calls for the few things a request truly needs, events for everything that can follow later. A request that fans out into a deep chain of synchronous calls is the classic way microservices become slow and fragile.
API gateways
#Clients should not need to know about dozens of services. An API gateway is a reverse proxy (article 08) that serves as the single entry point for clients and routes each request to the right service. Microsoft's Gateway Routing pattern describes routing requests to multiple services through a single endpoint, so clients are decoupled from how services are split and deployed (Gateway Routing pattern).
Typical gateway responsibilities:
- Routing by path or host (
/v1/photos/*to the photos service). - TLS termination and edge security.
- Authentication (validate tokens once at the edge) and coarse authorisation.
- Rate limiting and quotas per client (article 31).
- Request and response shaping, API versioning, CORS.
- Aggregation for specific clients: a backend for frontend (BFF) gateway that combines several service calls into one response tailored to the mobile app, reducing round trips (article 03).
- Observability: access logs, metrics and the start of traces.
What should not live in the gateway: business logic. A gateway that accumulates domain rules becomes a new monolith that every team must change. It must also be highly available and fast, because every request passes through it.
Service discovery
#Service instances come and go (autoscaling, deployments, failures), so their addresses change constantly. Service discovery lets a caller find healthy instances of a service by name.
- Server-side discovery: the caller sends requests to a stable name or address (a load balancer or virtual IP), which forwards to a healthy instance. Kubernetes Services work this way: a Service gives a set of pods a stable name and virtual address and routes traffic to them, while pods come and go (Kubernetes: Service).
- Client-side discovery: the caller looks up healthy instances in a service registry and load-balances itself. Registries are often built on consistent stores such as etcd or ZooKeeper (article 22), or tools such as Consul.
Either way, instances register (or are registered by the platform), health checks remove broken ones, and callers cache lookups briefly.
Service mesh
#As service counts grow, every service needs the same networking features: mTLS between services, retries, timeouts, circuit breaking, load balancing and telemetry. A service mesh moves these into a proxy deployed next to each service instance (a sidecar), configured centrally. Envoy is a widely used proxy for this role; its documentation describes it as an L7 proxy and communication bus designed for large service-oriented architectures (What is Envoy).
Benefits: consistent policies without changing application code. Costs: another complex system to operate, extra latency per hop, and debugging that spans application and mesh. It is worth it at significant scale; it is rarely the first thing a small team should add.
The distributed monolith
#The worst outcome is a system split into services that still behave like one application:
- services share a database and read each other's tables;
- a change requires deploying several services together in a fixed order;
- a request passes through long synchronous chains, so one slow service stalls everything;
- services share large common libraries containing domain models, forcing lockstep upgrades.
This distributed monolith has all the costs of microservices (network failures, operational load) and none of the benefits (independent deployment, isolation). If you see these symptoms, either merge the tightly coupled services or fix the boundaries and data ownership.
Migrating: the strangler fig
#Rewriting a large monolith from scratch is risky. The strangler fig approach, named by Martin Fowler after vines that gradually grow around a tree, replaces a system incrementally: put a facade (often the API gateway) in front of the old system, build new functionality or extract one capability at a time as a new service, route that capability's traffic to it, and repeat until the old system can be retired (StranglerFigApplication).
Step 1: Clients -> Gateway -> Monolith (everything)Step 2: Clients -> Gateway -> /notifications/* -> Notification service -> everything else -> MonolithStep 3: Clients -> Gateway -> several extracted services, smaller monolith ...Extract first what has the clearest boundary and the strongest reason (different scaling, different release cadence, isolation), and migrate its data ownership too, not only its code.
Worked example: Pixly's architecture over time
#Today (one team of eight engineers): a modular monolith API (modules: accounts, photos, social graph, feed, likes), stateless and horizontally scaled behind a load balancer, plus separate worker deployments for thumbnails, moderation and notifications consuming from queues. One PostgreSQL cluster with a schema per module. This is not "naive"; it is the right shape for this team.
Pressure arrives (three teams, feed load growing fast):
Pressure | Response |
|---|---|
Notifications need different scaling, providers and retry behaviour, and failures must not affect the API | Extract the notification service (already a worker; now owns its data and API) |
The feed needs a different data store (precomputed feeds in Redis and a wide-column store) and its own scaling | Extract the feed service, fed by photo and follow events |
Accounts, photos, social graph and likes change together often and are owned by one team | Keep them together in the core modular monolith |
Clients -> API gateway (auth, rate limits, routing, mobile BFF) |-- /v1/feed/* ----------> Feed service ------> feed stores |-- /v1/notifications/* --> Notification service `-- everything else ------> Core (modular monolith) --> PostgreSQLCore --outbox--> event broker --> Feed service, Notification service, search indexerThe design records why each split happened, and leaves the rest together until a similar reason appears.
Failure modes
#Failure | Effect | Mitigation |
|---|---|---|
Long synchronous call chains | Latency and availability multiply; cascades | Fewer hops, async events, timeouts, breakers |
Shared database between services | Lockstep changes, hidden coupling | Database per service; APIs and events |
Gateway outage | Everything unreachable | Redundant, multi-zone gateway; keep it thin |
Registry stale or down | Calls to dead instances, or no routing | Health checks, client caching, highly available registry |
Incompatible API change | Callers break after a deployment | Backward-compatible changes, versioning, contract tests |
Too many services for the team | Slow delivery, operational overload | Merge services; modular monolith |
Trade-offs
#Choice | You gain | You pay |
|---|---|---|
Monolith | Simplicity, speed early on, easy transactions | Coupling grows; shared releases; scaling the whole unit |
Modular monolith | Boundaries and ownership without network costs | Discipline to enforce boundaries; still one deployment |
Microservices | Independent teams, deployments, scaling, isolation | Network, data consistency, operations, testing |
API gateway | One entry point, centralised edge concerns | A critical component; risk of logic creep |
Service mesh | Uniform networking and security policy | Operational complexity, latency |
Common mistakes
#- Choosing microservices for a small team or an unclear domain.
- Splitting by technical layer instead of business capability.
- Sharing a database between services.
- Replacing in-process calls with long synchronous network chains.
- Putting business logic into the gateway.
- Big-bang rewrites instead of incremental extraction.
In an interview
#- Do not start with "microservices". Start with requirements and team context, then justify the style.
- If you propose services, name each with its business capability and the data it owns.
- Say which interactions are synchronous and which are events.
- Mention an API gateway at the edge and how services find each other.
- If asked "monolith or microservices?", answer "a modular monolith first, extracting X and Y because...", with the reasons.
Exercises
#Exercise 1
Choose the style
#Choose an architecture style and justify it:
- A four-person startup building a booking app for local gyms.
- A bank's digital platform with 40 teams, where the payments component has strict compliance and separate audits.
- A video platform whose transcoding pipeline needs 100 times the compute of the rest of the system, with very different scaling.
Exercise 2: Spot the distributed monolith
#A system has 12 services. Symptoms: all services read the users table directly; a release plan lists 7 services that "must be deployed in this order"; the checkout request makes 9 synchronous calls in sequence; there is one shared domain-models library that every service imports. Explain what is wrong and what you would change first.
Exercise 3: Draw the boundaries
#Propose service boundaries for a food delivery platform (customers, restaurants, menus, orders, payments, delivery partners, live tracking, notifications, ratings). For each service, name the data it owns, and say which interactions are synchronous and which are events.
Exercise 4: Gateway or not?
#Decide whether each belongs in the API gateway, and why:
- Validating JWT signatures.
- Calculating discounts for a cart.
- Rate limiting per API key.
- Combining profile, feed and notification count into one mobile response.
- Retrying failed calls to the payment service.
Show solutionHide solution
- Monolith or modular monolith. Four people, an unproven domain; speed and simplicity matter most. Use managed services for the database and email; keep background work in a simple job queue.
- Microservices (at least for major domains), with payments as a separate, isolated service with its own data, access controls, audits and release process. Forty teams need independent deployment; compliance needs isolation.
- Separate the transcoding pipeline as its own service and worker fleet (it is already a different workload: queue-driven, compute-heavy, autoscaled independently). The rest of the platform can remain a modular monolith unless team size also demands splitting.
Exercise 2
Show solutionHide solution
It is a distributed monolith:
- Shared
userstable: services are coupled through the database; any schema change risks breaking many services. - Ordered multi-service releases: services are not independently deployable.
- 9 sequential synchronous calls: checkout latency is the sum of nine calls, and its availability is the product of nine services' availability.
- Shared domain library: changes force coordinated upgrades.
First steps: (1) give user data a single owner (an accounts service) and replace direct table reads with an API or with events that other services use to maintain the user fields they need; (2) shorten checkout's synchronous path: identify what truly must be synchronous, move the rest to events, and parallelise independent calls; (3) replace the shared domain library with per-service models and explicit API contracts; (4) consider merging services that always change together.
Exercise 3
Show solutionHide solution
Service | Owns | Notable interactions |
|---|---|---|
Customer accounts | Customers, addresses | Sync: auth and profile reads |
Restaurants and menus | Restaurants, menus, availability, opening hours | Events: |
Search and discovery | Read model of restaurants and menus | Consumes menu events; sync queries from apps |
Orders | Orders and their state machine | Sync: create order; orchestrates the order saga; emits |
Payments | Payment intents, attempts, ledger | Sync step in the saga (pivot); events: |
Dispatch | Assignments of orders to delivery partners | Consumes |
Delivery partners | Partner profiles, availability | Sync reads by dispatch |
Live tracking | Partner locations, ETA | High-rate location ingestion; pushes to customer apps |
Notifications | Templates, preferences, delivery logs | Consumes order events |
Ratings | Ratings and reviews | Consumes |
Synchronous calls stay on paths the user waits for (placing an order, the payment step, showing tracking); everything else propagates through events. Article 42 designs this system.
Exercise 4
Show solutionHide solution
- Yes. Signature validation is a cross-cutting edge concern; doing it once at the gateway is efficient. Services still check authorisation for their own resources.
- No. Discounts are business logic belonging to a pricing or cart service. In a gateway they would couple every release to the gateway.
- Yes. Per-key rate limiting at the edge protects everything behind it.
- Yes, in a BFF layer (a gateway or service dedicated to the mobile client). It reduces round trips. Keep it to composition, not business rules.
- Usually no. Payment retries must be idempotent and are best handled by the service that owns the payment flow and its idempotency keys; blind gateway retries can duplicate charges and amplify outages.
Where to go next
#- Next: Event-Driven Architecture, CQRS and Event Sourcing, the asynchronous half of service communication.
- Later: Security in System Design covers authentication at the gateway and between services.
Sources & further reading
- Technology
- Software Engineering
- #backend
- #distributed systems
- #microservices
- #roadmap
- #scalability
- #system design
- #system design interview
Connected lessons
Linked from 1 guide
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
Start Here: Your Roadmap from Zero to Job-Ready DSA
Guide · Technology
Web Development Roadmap: How to Become a Web Developer From Scratch
Guide · Career
A LinkedIn Profile That Gets Recruiter Messages

