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?

# the identical computation, two ways def get_price_direct(product_id): return PRICES.get(product_id, 0) # a plain in-process function call # ...vs a real HTTP server on localhost, running in a background thread, # returning the identical price for the identical product_id
Verified directly — a real, measured benchmark, not an estimate
Timing 200 calls each way: the direct in-process call averaged 0.11 microseconds per call. The identical computation served over real HTTP, to 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.
This cost is not a reason to avoid microservices
11,661× sounds alarming, but 1.25 milliseconds is still fast enough for the overwhelming majority of real applications — the point isn't "microservices are too slow," it's that this cost is real and non-zero, and a monolith gets to skip it entirely for calls that stay inside one process. Whether that cost is worth paying is exactly what the rest of this chapter is about.

When Splitting Genuinely Pays Off

MonolithMicroservices
DeploymentOne unit, deployed togetherEach service deployed independently
ScalingScale the whole thing, even if only one part is under loadScale just the part that needs it
Call costA function call — verified: ~0.11 microsecondsA network call — verified: ~1,253 microseconds, even on localhost
Team boundariesEveryone works in the same codebaseDifferent teams can own different services independently
Failure isolationOne crash can take down the whole processOne 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

shared_db = {'orders': {'ORD-1': {'total': 100, 'product_id': 'PROD-1'}}} class OrderServiceProcess: # conceptually a separate deployment def get_order_summary(self, order_id): row = shared_db['orders'][order_id] return f"Order {order_id}: ${row['total']} for {row['product_id']}" class InventoryServiceProcess: # conceptually a SEPARATE deployment, owns product data def get_order_product(self, order_id): return shared_db['orders'][order_id]['product_id']
Verified directly — a change made entirely inside one "independent" service breaks another one, unredeployed
InventoryServiceProcess's own team renames their field, product_idsku, 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

def service_c(should_be_slow): if should_be_slow: time.sleep(0.3) return 'C says OK' def service_b(should_be_slow): result_c = service_c(should_be_slow) # B calls C synchronously and WAITS return f'B got: {result_c}' def service_a(should_be_slow): result_b = service_b(should_be_slow) # A calls B synchronously and WAITS return f'A got: {result_b}'
Verified directly — a slowdown two hops away becomes A's own slowdown, measured
With every service fast, 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.
Combine both anti-patterns and the cost compounds
A system with both a shared database and chained synchronous calls gets Chapter 2's coupling bugs, this chapter's own measured ~11,661× network overhead on every hop, and cascading latency — while still requiring the coordinated multi-service deployments a real monolith would have needed anyway for a tightly-coupled change. This is the actual, concrete failure mode "distributed monolith" describes — not a vague warning, but the specific combination this chapter just verified twice.

Where This Connects

This chapter's findingWhat it connects to
A shared database breaking an "independent" service, reusing Chapter 2's own cross-layer-read shapeChapter 5's coupling/cohesion criteria — the concrete test for whether a split is real
Chained synchronous calls making a slowdown cascade upstreamDistributed 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 localhostTechnical 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

Exercise 1

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 solution
Exercise 2

Fix 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.

📄 View solution
Exercise 3

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.

📄 View solution

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