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.

Left: one process box shop.jar containing five modules Catalog, Orders, Inventory, Payments and Shipping, connected by function calls, above one shared database. Right: an API gateway in front of five separate service boxes, connected by network calls, each with its own small database
A monolith joins its modules with function calls and one database; microservices join them with network calls, and each owns its data.

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.

Two timelines of one checkout. Monolith: 1 ms hop in, Orders 2 ms, Inventory 3 ms, Payments 4 ms, Shipping 2 ms, 1 ms hop out, 13 ms in total. Microservices: the same work blocks with a 1 ms hop before and after each call to Inventory, Payments and Shipping, 19 ms in total
The work is the same 11 ms; the microservices pay 2 ms for each of the extra network round trips.

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

demomonolithmicroservicesbetter here
one checkout13 ms, 1 network call19 ms, 4 network callsmonolith
Payments crashes, then browse + checkout0 of 2 requests OK: everything is down1 of 2 OK: browsing works, checkout failsmicroservices
deploy a one-line Shipping change250 MB, 5 modules restarted, ~20 min40 MB, 1 service restarted, ~3 minmicroservices
4× catalog traffic4 copies × 5 cores = 20 coresCatalog ×4 + 4 others = 8 coresmicroservices
a lost reply13 ms (a function call cannot lose it)65 ms, 5 calls, charged once thanks to the keymonolith
Scaling for 4 times the catalog traffic. Monolith: four full copies of shop.jar, each with all five modules, 20 cores, three quarters of the Orders, Inventory, Payments and Shipping copies idle. Microservices: four Catalog copies and one copy of each other service, 8 cores
A monolith can only copy all of itself; microservices copy just the part that is hot.

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?

MonolithMicroservices
call between partsfunction call, ~1 µs, never lostnetwork call, ~1–2 ms, can time out or be lost
dataone database, one transactiona database per service, compensations, eventual consistency
a bug crashes a partthe whole process goes downonly that service goes down
deployeverything at once, one pipelineeach service on its own
scalingcopies of the whole appcopies of the hot service only
operationsone thing to run, debug and monitormany: gateway, discovery, tracing, mesh
good fora small or young product, one team, unclear boundariesmany 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).