Monolith vs Microservices
Is a function call or a network call the right boundary? Compare monoliths and microservices across deployment, scaling, data and operations, and see the hidden third outcome every network call adds: unknown.
🎯 What you'll learn
- What monoliths and microservices really are
- The trade-offs: scaling, deployment, data, operations, team ownership
- Why premature decomposition creates a distributed monolith
- What changes when a function call becomes a network call
📚 Before you start
1What a Monolith Is
A monolith is a system deployed as a single unit: one codebase, one build artifact, one running process (or a set of identical replicas of that same process behind a load balancer). OrderService, InventoryService, and PaymentService might exist as separate classes or modules internally, but they all run inside the same process and talk to each other via in-process function calls.
┌─────────────────────────────────────────┐
│ order-api (1 process) │
│ ┌───────────┐ ┌───────────┐ ┌─────────┐ │
│ │ Orders │ │ Inventory │ │ Payments│ │
│ │ module │ │ module │ │ module │ │
│ └─────┬─────┘ └─────┬─────┘ └────┬────┘ │
│ └──────function calls──────┘ │
└─────────────────────┬─────────────────────┘
│
┌─────▼─────┐
│ 1 database │
└───────────┘A function call between Orders and Inventory here is a normal call: same memory space, same stack, microsecond latency, and if it throws, the caller catches it directly.
2What Microservices Are
Microservices split the same functionality into independently deployable services, each with its own process, its own deployment lifecycle, and — critically — usually its own database. Services talk to each other over the network: HTTP/REST, gRPC, or asynchronous messaging (queues, event streams).
┌───────────┐ ┌───────────┐ ┌───────────┐
│ orders │──HTTP──▶│ inventory │ │ payments │
│ service │ │ service │ │ service │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ orders │ │inventory│ │payments │
│ DB │ │ DB │ │ DB │
└─────────┘ └─────────┘ └─────────┘The same Orders → Inventory interaction that was a function call in the monolith is now a network call: serialize a request, send it over a socket, wait, deserialize a response — and it can fail in ways a function call never can (see Under the Hood).
3Side-by-Side Comparison
| Dimension | Monolith | Microservices |
|---|---|---|
| Deployment | One artifact, one deploy | N artifacts, N independent deploys |
| Inter-module calls | In-process function calls (µs, can't partially fail) | Network calls (ms+, can time out, drop, partially fail) |
| Database | Usually one shared schema | Usually one database per service |
| Scaling | Scale the whole process, even for one hot module | Scale only the service that's hot |
| Team ownership | Often one team, or several teams in one codebase | Each service ownable by one small team |
| Local dev | git clone + run one thing | Need to run/mock N services, or accept complexity |
| Failure blast radius | A bug can crash the whole process | A failure in one service usually doesn't take down others (if isolated correctly) |
| Transactions | Native DB transactions (ACID) across modules | Cross-service transactions require Sagas / 2PC (see Distributed Transactions topic) |
| Refactoring | Rename a function, IDE fixes every call site | Changing a service's API is a versioning/coordination problem |
| Operational complexity | Low — one thing to deploy, log, monitor | High — service discovery, distributed tracing, N sets of logs/metrics |
4Why Teams Reach for Microservices
- Independent scaling. If
Inventorygets 100x the traffic ofPayments, scale onlyInventory's replicas instead of the whole monolith. - Independent deployment. Team A ships ten times a day without coordinating a release train with Team B.
- Fault isolation. A memory leak or crash loop in
Recommendationsshouldn't be able to takeCheckoutdown with it, if the boundary is real. - Technology heterogeneity. One service can be Python for ML, another Go for a hot path, another Node for I/O-bound work.
- Organizational scaling. Conway's Law in action: many small teams work best when the system is decomposed to match team boundaries, not one giant shared codebase everyone steps on.
PetPal ran one Node monolith with 6 engineers for two years. Then the search feature needed 10× the CPU of everything else and its own release cadence, owned by a new team of 4.
They extracted only search into a service (with its own index) and kept orders, payments and accounts in the monolith. One clear reason led to one service, not twelve.
5Why Microservices Are Not Free
- The network is unreliable. Every inter-service call is now subject to latency, timeouts, partial failure, and retries — none of which existed for a function call (see Under the Hood).
- Data consistency gets hard. No more
BEGIN; UPDATE orders; UPDATE inventory; COMMIT;— see the Distributed Transactions and CAP Theorem topics. - Operational overhead multiplies. You now need service discovery, distributed tracing, centralized logging, per-service on-call, contract testing, and a deployment pipeline per service.
- Debugging a single request means tracing it across N services, N sets of logs, and N potential points of failure.
- Premature decomposition is a common and expensive mistake — splitting a system before its module boundaries are well understood usually produces a distributed monolith: all the network overhead of microservices with none of the independence, because services still have to change together.
6Decision Guide
| Signal | Leans monolith | Leans microservices |
|---|---|---|
| Team size | 1 small team | Multiple independent teams |
| Domain understanding | Still evolving / early-stage product | Well-understood, stable bounded contexts |
| Scaling needs | Uniform load across the system | Wildly different load per component |
| Ops maturity | Limited platform/SRE investment | Dedicated platform team, existing service mesh/observability |
| Release cadence needs | Coordinated releases acceptable | Independent, frequent releases required per team |
A common, pragmatic path: start with a modular monolith (next topic) — a single deployable with strict internal module boundaries — and extract a module into its own service only once it has a real, proven reason (a scaling mismatch, an ownership boundary that keeps causing conflict, a genuine need for independent deployment).
Under the HoodWhat Actually Changes When a Function Call Becomes a Network Call▾
This is the concrete cost that microservices always pay, whether or not the org is ready for it.
The monolith case: orders.createOrder() calls inventory.reserve()
function createOrder(cart) {
const reserved = inventory.reserve(cart.items); // in-process call
if (!reserved) throw new Error("out of stock");
return db.insertOrder(cart);
}This is a direct jump in the same address space: push a stack frame, run the callee's instructions, pop the frame, return. It cannot time out. It cannot arrive twice. It cannot "partially succeed" — either reserve() returns/throws, or the whole process has already crashed (in which case createOrder never got a chance to observe anything, consistent state). Latency: nanoseconds to low microseconds, bounded almost entirely by the work reserve() actually does.
The microservices case: orders-service calls inventory-service over HTTP
async function createOrder(cart) {
const res = await fetch("http://inventory-service/reserve", {
method: "POST",
body: JSON.stringify({ items: cart.items }),
signal: AbortSignal.timeout(2000),
});
if (!res.ok) throw new Error("reservation failed");
return db.insertOrder(cart);
}Walk through what actually has to happen for this one call, that never happened before:
- Serialize the request body to JSON (CPU + allocation cost proportional to payload size).
- Resolve
inventory-serviceto an actual IP:port — DNS or a service registry lookup. - Establish or reuse a TCP connection — a fresh connection means a 3-way handshake (and a TLS handshake on top, if encrypted) before a single application byte moves.
- Send the bytes, cross a NIC, cross the network (possibly multiple hops, possibly a different availability zone with real added latency), arrive at
inventory-service's socket buffer. inventory-servicedeserializes, does its own work, serializes a response, sends it back — same trip in reverse.orders-servicedeserializes the response.
Every one of those steps can fail independently, in ways a function call structurally cannot:
- Timeout — the request is sent,
inventory-serviceis alive and even reserved the stock, but the response is lost or slow, and the 2-second budget expires.orders-servicenow doesn't know whether the reservation happened or not — a state a function call can never produce (a function call is always resolved, one way, before control returns). - Partial failure —
inventory-servicemight process the reservation, then crash before sending the response. The work happened; the caller thinks it didn't. - Retry-caused duplication — if
orders-serviceretries after a timeout, and the original request actually succeeded, inventory can be reserved twice for one order (this is exactly why the Retry and Idempotency material in later topics matters here). - Connection-level failures — the TCP connection can be reset, the DNS entry can be stale, the load balancer can route to an instance that's already draining for a deploy.
This is the real reason "just add a network call" is never a small change: a function call has exactly two outcomes (return a value, or throw), resolved before the caller's next line runs. A network call has a third outcome that swallows a huge amount of engineering effort in real distributed systems: unknown — the request may or may not have been applied, and the caller has to design for that ambiguity explicitly (idempotency keys, at-least-once semantics with dedup, sagas with compensations). Every topic later in this fundamentals group — retries, backoff, circuit breakers, distributed transactions — exists to manage this one fact.
⚠️ Common Pitfalls
- Decomposing into services before the domain boundaries are understood — produces a distributed monolith: services that must deploy together anyway, but now pay full network overhead for every interaction.
- Treating a network call like a function call — no timeout, no retry policy, no handling for the "unknown" outcome described above.
- Splitting services along technical layers (e.g. "database service," "validation service") instead of business capabilities — causes every real feature to require changes across multiple services.
- Sharing a database across "independent" services — silently recreates a monolith's coupling while paying for microservices' operational cost, with none of the isolation.
- Choosing microservices for scale before scale is actually a measured problem — most systems never hit the point where a monolith can't be scaled with more replicas behind a load balancer.
Splitting by technical layer
validation-service, database-service, email-service, business-logic-service → every feature touches four services and four deploys.
Splitting by business capability
orders, catalog, payments, shipping. Each owns its data and its rules, so most features change one service.
📋 Quick Reference
Monolith: 1 process, in-process calls, 1 DB, low ops overhead, hard to scale/deploy parts independently
Microservices: N processes, network calls, N DBs, high ops overhead, easy to scale/deploy parts independently
Function call outcomes: success | throw (always resolved before caller continues)
Network call outcomes: success | failure | UNKNOWN (timeout) (caller must design for "did it happen?")
Default advice: start monolith (or modular monolith) → extract a service only with a proven, specific reason🧠 Key takeaways
- A monolith is one deployable with in-process calls and one database; microservices are independently deployed services with their own data.
- Microservices buy independent scaling, deployment and ownership at the cost of network failures, data consistency and operational load.
- Split by business capability, never by technical layer, and don't share databases between services.
- A network call has a third outcome, unknown. Default to a (modular) monolith and extract services for proven reasons.
🎤 Interview Q&A
Q1When would you choose microservices over a monolith?core▾
When the organization has several teams that need to deploy independently, parts of the system have very different scaling or reliability needs, or domain boundaries are well understood. For a small team or a new product, a (modular) monolith is usually faster and cheaper; microservices trade code complexity for operational and distributed-systems complexity.
- What's Conway's law got to do with it? System boundaries tend to mirror team communication structure, so service boundaries should follow team ownership.
Q2What are the main costs of microservices?core▾
Network latency and partial failures, distributed data (no cross-service transactions, eventual consistency), harder debugging (need tracing), deployment and versioning of many services, duplicated infrastructure concerns, and the operational overhead of CI/CD, monitoring and on-call per service.
- What's a distributed monolith? Services that must be deployed together because they're tightly coupled: all the costs, none of the benefits.
Q3How do you decide service boundaries?core▾
Around business capabilities / bounded contexts (orders, payments, catalog), each owning its data and with high cohesion inside, low coupling outside. A good sign: most features change one service; a bad sign: chatty synchronous calls between services for every request.
- Should services share a database? No; shared databases couple schemas and deployments. Share via APIs or events.
Q4How would you migrate a monolith to microservices?senior▾
Incrementally with the strangler fig pattern: put a routing layer in front, extract one well-bounded capability at a time (often starting with something low-risk or with clear scaling needs), give it its own data via migration and sync, route traffic gradually, and delete the old code. Never big-bang rewrite.
- What do you extract first? A capability with clear boundaries and real benefit from independence, not the most tangled core.
Q5How do microservices handle data that spans services?core▾
Each service owns its data; others get it via APIs or by subscribing to events and keeping local read copies. Workflows spanning services use sagas (local transactions + compensating actions) rather than distributed transactions.
- Cost of local copies? Eventual consistency and the need to handle late or duplicate events.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.
✅ Check yourself
Answer the questions — feedback is instant.