# Monolith vs Microservices

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

## 2. What 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).

![Monolith: one process, in-process calls, one shared database. Microservices: N processes talking over HTTP, each with its own database.](diagrams/01-monolith-vs-microservices.svg)

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

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

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

## 6. Decision 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 Hood: What 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.

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