Week 15: Microservices Architecture & Service Decomposition

Every design so far in this course has quietly assumed one deployable backend. Most real systems at the scale this course targets are actually many independently deployed services — and that split isn't free. This week covers when splitting a monolith earns its complexity, how to draw the boundaries so services don't end up tightly coupled anyway, and the hardest new problem decomposition introduces: a "transaction" that used to be one database commit now has to span multiple services that can each independently fail.

Module 12 of 24 Week 15 of 28 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Decide, with concrete criteria, when a monolith should actually be split
  • Decompose a system by bounded context instead of by technical layer
  • Design a saga to keep a multi-service operation consistent without a distributed transaction

1. When to Split a Monolith

A monolith isn't a mistake — it's the right starting point for almost every system, including large ones. Splitting it into services trades one hard problem (a growing codebase) for several new ones (network calls that can fail, data that's no longer in one transaction, and operational overhead per service). That trade is only worth making when a specific pain is actually being felt:

  • Independent scaling needs. One part of the system (Week 14's fan-out workers, say) needs 50x the compute of everything else. In a monolith, scaling that means scaling the whole thing.
  • Independent deployment needs. One team ships several times a day; another team's code, bundled in the same deploy, forces them to wait on a slower release cadence or risk breaking something they don't own.
  • Genuinely different reliability requirements. A payments path that must never silently fail sits in the same process as a recommendations feature that's fine to degrade — a bug in the second can take down the first.

None of these is "the team feels like microservices are more modern." A team of five engineers splitting a working monolith into twelve services because that's what a blog post recommended usually ends up spending more time on service-to-service networking than on the product — this is the single most common system design anti-pattern in real organizations, and naming it unprompted in an interview reads as real experience rather than a memorized diagram.

"Should this be microservices?" has a default answer of no

The strongest interview answer treats a monolith as the default and asks what specific problem justifies paying the distributed-systems tax — not the other way around. Naming the actual scaling, deployment or reliability pain a split solves is a stronger answer than assuming the split is self-evidently correct at any scale.

2. Decomposing by Bounded Context

Once splitting is justified, the boundaries matter more than the decision to split at all. The most common mistake is decomposing by technical layer — a "database service," a "business logic service," a "UI service" — which just re-adds network calls to what used to be function calls, without actually separating anything that changes independently.

wrong: decomposing by layer
[API Service] --> [Business Logic Service] --> [Database Service]

Every single feature touches all three services. Deploying
one small change to checkout logic still requires coordinating
a release across all three -- the monolith's coupling problem,
now with network calls added on top.

The correct axis is bounded context — a term for "a part of the domain with its own consistent model and vocabulary." An e-commerce system's Order, Inventory and Shipping concepts each have their own rules, their own data, and their own reasons to change, even though they interact:

right: decomposing by bounded context
[Order Service]     -- owns order state & the checkout flow
[Inventory Service]  -- owns stock levels & reservation
[Shipping Service]   -- owns fulfillment & carrier integration
[Notification Service] -- owns emails/SMS, used by all three

Each service owns its own data store -- no service reaches
directly into another's database. A change to how shipping
carriers are chosen never requires touching Order or Inventory
code, and never requires their teams to coordinate a release.

"Owns its own data store" is the load-bearing rule here: if two services share one database, they're still coupled at the schema level even if they're deployed separately — a column rename in the shared table can still break a service that never asked for that change. Each service being the only writer (and usually only reader) of its own data is what makes the deployments actually independent.

3. Coupling Traps: The Distributed Monolith

A system can be physically split into many deployed services and still behave like a monolith in every way that matters — this failure mode has a name, the distributed monolith, and it's worse than the monolith it replaced: all the coupling, plus network latency and partial-failure modes on top.

  • Synchronous call chains. Service A calls B, which calls C, which calls D, all synchronously in one request. If D is slow, A's request is slow; if D is down, A's request fails — a single point of failure spread across four deploys instead of one.
  • Shared databases. Two services reading and writing the same tables means a schema change to satisfy one service can silently break the other.
  • Lockstep deployment. If deploying Service A always requires deploying Service B in the same release window because their APIs are too tightly versioned, the two are not actually independently deployable, whatever the architecture diagram claims.

The fixes each map to a tool from earlier in this course: replace long synchronous call chains with asynchronous events over a queue (Week 8) where the caller doesn't need an immediate answer; give each service exclusive ownership of its data (Section 2); and version APIs so a service can evolve without forcing a coordinated release — exactly the kind of inter-service contract Week 16 covers in depth.

Count the synchronous hops in a critical path

A quick diagnostic: trace one user-facing request and count how many services it calls synchronously, in sequence, before responding. Four or more is a strong signal of a distributed monolith — each hop adds latency and a new way for the whole chain to fail, and it's worth asking in an interview whether any of those hops could become asynchronous instead.

4. Deep Dive: Sagas for Cross-Service Consistency

Once Order and Inventory own separate databases (Section 2), a single ACID transaction spanning both is gone — there's no single database to commit against. But "place an order" still needs to behave atomically from the user's point of view: reserve inventory, charge payment, confirm the order, and if any step fails, undo the ones that already succeeded.

the saga pattern — order placement
Step 1: Order Service creates order in PENDING state
Step 2: Inventory Service reserves stock
          success --> continue to Step 3
          failure --> compensate: cancel order (undo Step 1)
Step 3: Payment Service charges the customer
          success --> continue to Step 4
          failure --> compensate: release stock (undo Step 2),
                       cancel order (undo Step 1)
Step 4: Order Service marks order CONFIRMED

Each step has a matching COMPENSATING action that undoes it.
There is no global lock across all four steps -- consistency
is reached eventually, by explicitly undoing partial work,
not by a single atomic commit.

This is the multi-service generalization of the eventual-consistency vocabulary from Weeks 9–10: instead of a single strongly-consistent transaction, the saga guarantees the system reaches a consistent end state (fully completed, or fully rolled back) even though it passes through genuinely inconsistent intermediate states along the way — an order briefly exists with stock reserved but payment not yet confirmed, and that's an accepted, designed-for state, not a bug.

Sagas are typically coordinated one of two ways: choreography, where each service listens for the previous step's event and reacts on its own (no central coordinator, but the overall flow is harder to see in one place), or orchestration, where a dedicated saga coordinator service explicitly calls each step and handles failures (easier to reason about, but the coordinator itself becomes a critical component). Most production systems favor orchestration once a saga has more than two or three steps, purely for debuggability.

A compensating action isn't always a perfect undo

"Release stock" cleanly reverses "reserve stock," but "refund payment" doesn't perfectly reverse "charge payment" — the customer's card was genuinely charged for real minutes, and a refund is a separate operation with its own failure modes and delay. Naming this asymmetry — some compensations are exact, some are approximate and take time — is a stronger answer than treating every saga step as trivially reversible.

5. Hands-on Exercise

Hands-on

Decompose a monolithic ride-booking backend

A single monolithic backend currently handles user accounts, trip booking, driver matching, pricing and receipts, all sharing one database.

Requirements:

  1. Propose a service decomposition by bounded context (Section 2), naming each service and the one thing it owns. Justify why each boundary is drawn where it is, not just what the services are called.
  2. Identify which two of your proposed services are most at risk of becoming a distributed monolith (Section 3) if built carelessly, and explain specifically what carelessness would cause it.
  3. Design a saga (Section 4) for the "book a trip" flow across at least three of your services, listing each step and its compensating action.
  4. Decide whether your saga should use choreography or orchestration, and justify the choice against the number of steps involved.
  5. Name one piece of data that's tempting to duplicate across two services rather than have one own it and the other call it — and state whether you'd duplicate it or not, and why.
Hint

For the last requirement: a small amount of duplicated, denormalized data (like caching a driver's name inside the Trip service instead of calling the Driver service on every read) is often a deliberate, correct tradeoff — it trades a bit of staleness for removing a synchronous cross-service call from a hot read path. The question worth asking isn't "should data ever be duplicated," it's "is this specific piece of data allowed to be briefly stale."

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What's wrong with justifying a microservices split purely on the grounds that "microservices are the modern approach"?

Splitting a system introduces real, ongoing costs — network calls that can fail, no more single-database transactions, and per-service operational overhead — that only pay for themselves against a specific, concrete pain like independent scaling, independent deployment, or genuinely different reliability requirements. Without one of those, the split is pure added cost with no corresponding benefit.

Q2

Why does decomposing services by technical layer (API layer, business logic layer, database layer) fail to actually solve the monolith's coupling problem?

Every feature still has to touch all three layer-based services together, so a small change still requires coordinating a release across all of them — the same coupling the monolith had, except function calls have been replaced with slower, less reliable network calls. Decomposing by bounded context instead groups each service around a part of the domain that actually changes independently.

Q3

Why is a long chain of synchronous service-to-service calls (A calls B calls C calls D) considered a "distributed monolith" symptom?

The chain behaves like one tightly coupled unit despite being four separate deployments: A's request latency is the sum of all four services, and if D fails or is slow, that failure propagates all the way back to A. The services gained deployment independence on paper but not in the runtime behavior that actually matters to users.

Q4

Why can't a "place an order" operation spanning Order, Inventory and Payment services use a single database transaction the way it could in a monolith?

Each service owns its own separate database, so there's no single database engine that could wrap all three writes in one atomic commit. A saga replaces that single transaction with a sequence of local transactions, one per service, each paired with an explicit compensating action that undoes it if a later step fails — reaching a consistent end state through explicit rollback logic rather than a single atomic operation.