Engineering Notes
Home/Distributed Systems/Fundamentals/Monolith vs Microservices
Distributed Systems · Fundamentals

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.

🕸️ Distributed Systems● Beginner⏱ 9 min read▶ 2 interactive🛠️ 2 exercises🎤 5 interview Q&As

🎯 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

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.

text
┌─────────────────────────────────────────┐
│              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).

text
┌───────────┐        ┌───────────┐        ┌───────────┐
│  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).

Monolith: one process, in-process calls, one shared database. Microservices: N processes talking over HTTP, each with its own database.
Monolith: one process, in-process calls, one shared database. Microservices: N processes talking over HTTP, each with its own database.

3Side-by-Side Comparison

DimensionMonolithMicroservices
DeploymentOne artifact, one deployN artifacts, N independent deploys
Inter-module callsIn-process function calls (µs, can't partially fail)Network calls (ms+, can time out, drop, partially fail)
DatabaseUsually one shared schemaUsually one database per service
ScalingScale the whole process, even for one hot moduleScale only the service that's hot
Team ownershipOften one team, or several teams in one codebaseEach service ownable by one small team
Local devgit clone + run one thingNeed to run/mock N services, or accept complexity
Failure blast radiusA bug can crash the whole processA failure in one service usually doesn't take down others (if isolated correctly)
TransactionsNative DB transactions (ACID) across modulesCross-service transactions require Sagas / 2PC (see Distributed Transactions topic)
RefactoringRename a function, IDE fixes every call siteChanging a service's API is a versioning/coordination problem
Operational complexityLow — one thing to deploy, log, monitorHigh — service discovery, distributed tracing, N sets of logs/metrics

4Why Teams Reach for Microservices

  • Independent scaling. If Inventory gets 100x the traffic of Payments, scale only Inventory'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 Recommendations shouldn't be able to take Checkout down 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.
💡 Example: when PetPal split out search

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

SignalLeans monolithLeans microservices
Team size1 small teamMultiple independent teams
Domain understandingStill evolving / early-stage productWell-understood, stable bounded contexts
Scaling needsUniform load across the systemWildly different load per component
Ops maturityLimited platform/SRE investmentDedicated platform team, existing service mesh/observability
Release cadence needsCoordinated releases acceptableIndependent, 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()

js
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

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

  1. Serialize the request body to JSON (CPU + allocation cost proportional to payload size).
  2. Resolve inventory-service to an actual IP:port — DNS or a service registry lookup.
  3. 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.
  4. 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.
  5. inventory-service deserializes, does its own work, serializes a response, sends it back — same trip in reverse.
  6. orders-service deserializes the response.

Every one of those steps can fail independently, in ways a function call structurally cannot:

  • Timeout — the request is sent, inventory-service is alive and even reserved the stock, but the response is lost or slow, and the 2-second budget expires. orders-service now 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-service might process the reservation, then crash before sending the response. The work happened; the caller thinks it didn't.
  • Retry-caused duplication — if orders-service retries 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

  1. 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.
  2. Treating a network call like a function call — no timeout, no retry policy, no handling for the "unknown" outcome described above.
  3. 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.
  4. Sharing a database across "independent" services — silently recreates a monolith's coupling while paying for microservices' operational cost, with none of the isolation.
  5. 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

text
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

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

1. What's an outcome a network call can have that a function call can't?
That ambiguity drives idempotency, retries and sagas.
2. Services that must always be deployed together are…
All the network cost, none of the independence.
3. Two 'independent' services read and write the same tables. Problem?
Each service should own its data.
4. A 5-person team building a new product should usually start with…
Extract services later, for proven reasons.