The idea: the same shop, two shapes
An online shop has five parts: Catalog, Orders, Inventory, Payments and Shipping. The page builds it twice and sends the same actions to both at once. On the left it is a monolith: one deployable, one process, one database. On the right it is a set of microservices: each part is its own program with its own database, and they talk over the network. The code inside each part does the same work in both lanes (the same milliseconds); only the boundaries between the parts differ.
The scoreboard under each lane counts what the comparison is about: the latency of the last request, the number of network calls, requests that succeeded or failed, the CPU cores the shop runs on and what the last deploy cost.
The monolith
All five modules are compiled into one artifact (here shop.jar, 250 MB) and run in one JVM process. When Orders needs stock it calls inventory.reserve(cart): a plain function call, about a microsecond, which cannot time out or get lost. All modules write to the same database, so a checkout is one transaction: the stock, the payment and the order commit together or not at all.
The price is coupling at run time and at release time. The modules share one memory space, so one bad module can bring down the others (Demo: Payments crashes). A change anywhere means building, testing and restarting the whole thing (Demo: deploy a one-line Shipping change). And the only thing you can copy to handle more load is the whole application.
Microservices
Each part is a separate service: its own code base, its own process (usually a container), its own database, owned by one team. An API gateway routes outside requests to the right service. Services call each other over the network with HTTP/JSON or gRPC.
- Failure isolation. A crashing service takes down only its own feature. With Payments gone, the catalog still answers; only checkout fails.
- Independent deploys. The Shipping team builds and ships a 40 MB image in minutes without waiting for the others' release.
- Independent scaling. Only the hot service gets more copies (Demo: scale for 4× catalog traffic).
- Every boundary is now a network call, and every database is separate. That is the cost, and the next section is about it.
What a network call costs
On the page one network hop is 1 ms each way inside the data centre (serialize JSON, TCP, deserialize). The checkout needs Inventory, Payments and Shipping. The monolith makes one network hop (load balancer to the process) and three function calls: 13 ms. The microservices make four network calls (gateway to Orders, then Orders to each of the three): 19 ms (Demo: one checkout request). Real calls also pay for TLS, retries, a service mesh sidecar on each side, and the slowest of many calls; the gap grows with every hop in a chain.
A network call can also fail in ways a function call cannot. In Demo: a lost reply on the network Payments charges the card but its reply is lost. Orders cannot tell whether the charge happened, waits for its 50 ms read timeout and sends the request again with the same Idempotency-Key. Payments recognises the key and returns the stored answer, so the card is charged once, but the checkout took 65 ms instead of 19. Without the key the retry would charge twice.
Separate databases mean there is no transaction across services. When Payments is down (Demo: Payments crashes), Orders has already reserved stock in Inventory's database. It must undo that itself with a compensation (DELETE /reservations) before answering 503. Longer flows use the saga pattern for this; see Saga vs Two-Phase Commit.
Failures, deploys and scaling
| demo | monolith | microservices | better here |
|---|---|---|---|
| one checkout | 13 ms, 1 network call | 19 ms, 4 network calls | monolith |
| Payments crashes, then browse + checkout | 0 of 2 requests OK: everything is down | 1 of 2 OK: browsing works, checkout fails | microservices |
| deploy a one-line Shipping change | 250 MB, 5 modules restarted, ~20 min | 40 MB, 1 service restarted, ~3 min | microservices |
| 4× catalog traffic | 4 copies × 5 cores = 20 cores | Catalog ×4 + 4 others = 8 cores | microservices |
| a lost reply | 13 ms (a function call cannot lose it) | 65 ms, 5 calls, charged once thanks to the key | monolith |
The page's numbers are small on purpose. The trend is what holds in practice: the monolith wins on latency, simplicity and consistency; microservices win when failures, releases or load differ a lot between the parts. Microservices also add work the page does not draw: service discovery, an API gateway, distributed tracing to see one request across services (see Distributed Tracing), timeouts and circuit breakers so one slow service does not stall its callers (see Circuit Breaker and Bulkhead), and versioned APIs between teams.
Which one?
| Monolith | Microservices | |
|---|---|---|
| call between parts | function call, ~1 µs, never lost | network call, ~1–2 ms, can time out or be lost |
| data | one database, one transaction | a database per service, compensations, eventual consistency |
| a bug crashes a part | the whole process goes down | only that service goes down |
| deploy | everything at once, one pipeline | each service on its own |
| scaling | copies of the whole app | copies of the hot service only |
| operations | one thing to run, debug and monitor | many: gateway, discovery, tracing, mesh |
| good for | a small or young product, one team, unclear boundaries | many teams, parts with very different load or release speed |
Between the two sits the modular monolith: one deployable, but modules with strict boundaries (their own tables, calls only through a public interface). It keeps function calls and one transaction, and a module can later be split out along a boundary that is already clean. Many teams start there and split only the parts whose load or release speed really differs.
See also API Gateway for what the gateway in front of the services does, and Hexagonal Architecture for clean boundaries inside one service or module.
What the page leaves out
Service discovery and load balancing between service copies, asynchronous messaging (events through Kafka instead of calls), retries with backoff, timeouts on every call, circuit breakers, a service mesh and TLS between services, API versioning, data duplicated between services and kept in sync by events, the transactional outbox, rolling and canary deploys, the start-up time and memory of many small JVMs, and the team and organisation side (Conway's law).