Monolith vs. Microservices
Software Architecture Fundamentals
Chapter 4 · Monolith vs. Microservices
Chapter 3 was about who's allowed to talk to whom inside one process. This chapter asks the same question at a much bigger scale: should this system even be one process? A monolith puts everything in one deployable unit, talking via direct function calls. Microservices split it into several independently deployable processes, talking over the network. Both are legitimate — but this chapter measures, with real numbers, exactly what crossing that boundary costs, and what happens when a "microservices" system doesn't actually get the benefits it's paying that cost for.
The Measured Cost of a Network Boundary
A monolith's internal calls are direct function calls. A microservices system's calls cross a process boundary — even when both services happen to run on the same machine. How much does that boundary actually cost?
127.0.0.1 (not even a different machine — zero actual network hops), averaged 1,253.57 microseconds per call. That's the network-boundary version taking roughly 11,661× longer — for the same result, computed the same way, on the same machine. This is the honest, measured cost of choosing to split a system into separate processes, before counting anything else.
When Splitting Genuinely Pays Off
| Monolith | Microservices | |
|---|---|---|
| Deployment | One unit, deployed together | Each service deployed independently |
| Scaling | Scale the whole thing, even if only one part is under load | Scale just the part that needs it |
| Call cost | A function call — verified: ~0.11 microseconds | A network call — verified: ~1,253 microseconds, even on localhost |
| Team boundaries | Everyone works in the same codebase | Different teams can own different services independently |
| Failure isolation | One crash can take down the whole process | One service crashing doesn't necessarily crash the others |
Splitting genuinely pays off when different parts of a system need to scale, deploy, or fail independently — not by default, and not just because the codebase feels large.
The Distributed Monolith: Paying the Cost, Getting None of the Benefit
A distributed monolith looks like microservices — separate processes, separate deployments on paper — but is still tightly coupled underneath, the same way Chapter 2's layer violations were coupling hiding inside code that looked correctly organized.
Anti-Pattern 1: A Shared Database
InventoryServiceProcess's own team renames their field, product_id → sku, and ships a matching InventoryServiceProcessV2 — which works correctly. OrderServiceProcess is never touched, never redeployed. Calling its unchanged get_order_summary('ORD-1') now raises KeyError: 'product_id'. Two "independently deployable" services just proved they weren't independent at all — because both were reading the same shared table directly, exactly the cross-layer read Chapter 2 verified breaking a single process, just now breaking across a process boundary too.
Anti-Pattern 2: Chained Synchronous Calls
service_a() returns in effectively 0.0 ms. Making only service_c slow (an artificial 300ms delay, simulating a real downstream service under load) — with zero changes to service_a or service_b — makes service_a()'s own total response time 300.3 ms. A never called anything slow itself. It's simply waiting, synchronously, at the end of a chain, for a service two hops away that it may not even know exists.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| A shared database breaking an "independent" service, reusing Chapter 2's own cross-layer-read shape | Chapter 5's coupling/cohesion criteria — the concrete test for whether a split is real |
| Chained synchronous calls making a slowdown cascade upstream | Distributed Systems & Scalability's own resilience-pattern chapter (circuit breakers, timeouts) — the direct fix for exactly this finding |
| The measured ~11,661× network-call overhead, even on localhost | Technical Support's own `perfdiag1`/`appdiag1` — this is precisely the kind of cost those courses' diagnostic chapters trace back to a specific hop |
Hands-On Exercises
Extend this chapter's own HTTP benchmark to 500 calls instead of 200 for both the direct and HTTP-served versions. Verify the per-call timings stay in the same rough range as this chapter's own 200-call result, and report the new overhead ratio.
📄 View solutionFix this chapter's own shared-database anti-pattern by giving InventoryServiceProcess exclusive ownership of product data (its own separate store, no longer inside shared_db), and having OrderServiceProcess call InventoryServiceProcess's own method instead of reading product_id directly. Verify the same field rename from this chapter no longer breaks OrderServiceProcess.
Add a fourth service, service_d, called synchronously by service_c (so the chain is now A → B → C → D). Make only service_d slow (a 300ms delay), with service_c itself fast. Verify service_a's total response time still reflects the full delay, now three hops away instead of two.
Chapter 4 Quick Reference
- Measured cost: a real localhost HTTP call averaged ~1,253 microseconds vs. a direct call's ~0.11 microseconds — roughly 11,661× slower for the identical result, before counting a single real network hop
- Distributed monolith, verified twice: a shared database let one service's own internal rename break another, unredeployed service; a chained synchronous call made a downstream slowdown become the top-level caller's own measured slowdown (300.3ms, two hops away)
- The actual question: not "monolith or microservices" as a default, but whether a specific part of the system genuinely needs independent scaling, deployment, or failure isolation badly enough to pay the measured network cost for it
- Next chapter: Finding Service Boundaries — the concrete criteria (coupling and cohesion) for deciding where a real split should go