Module A-4·30 min read

Service boundaries from domain boundaries, the distributed monolith you get when you guess wrong, and the honest list of when not to split.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module A-4 — Microservices vs Monolith

What this module covers: That microservices solve an organisational problem rather than a technical one, and what follows if you adopt them without having that problem. A concrete bill for one service boundary, assembled from the mechanisms this course has already built — a network call where a function call was, partial failure, no transactions, no joins, eventual consistency, and a connection-pool multiplication that kills your database. The modular monolith as the correct first answer and the honest prerequisite for the second. The six real triggers for splitting, and the four that look like triggers and are not. Where boundaries belong: bounded contexts, and the technical rule that a service boundary must never cut through an aggregate, because that converts one business invariant into a distributed transaction. The distributed monolith — its symptom list, its causes, and the arithmetic by which a five-deep synchronous chain destroys the availability of every service in it. Then integration style, database-per-service, and how to split something that is already running without stopping it.


Microservices Are an Answer to an Organisational Question

The technical claims made for microservices — better scalability, better fault isolation, better performance — are all achievable in a monolith, and several are easier there. What a monolith genuinely cannot give you is this:

Forty engineers in six teams, each able to ship to production several times a day without coordinating with the other five.

That is the problem microservices solve. Independent deployability by independent teams. Everything else attributed to them is either a side effect, achievable more cheaply, or false.

Which produces the test to apply before anything else in this module: do you have that problem? Three teams sharing a deploy pipeline and stepping on each other in a merge queue have it. Eight engineers on one product do not, and adopting microservices to solve a problem they do not have means paying the whole bill below for none of the benefit.

Analogy: a restaurant splitting into separate businesses — the kitchen, the bar, the front of house — each with its own accounts, premises, and staff who can renovate without asking the others. If the kitchen and the bar are constantly blocking each other's refits, the split buys real freedom. If it is one chef and one bartender who work fine together, you have just created three sets of books, three leases, three insurance policies, and a requirement that every drink order travel between legal entities. The freedom was never the constraint.


What One Service Boundary Actually Costs

Every item on this list is a mechanism from earlier in the course, arriving as a bill:

What you hadWhat you getModule
A function call: nanoseconds, cannot half-succeedA network call: milliseconds, can time out ambiguouslyF-5, P-16
An exception you handle locallyPartial failure — the other service is up, slow, or lyingA-6, A-7
BEGIN … COMMIT across five tablesA saga, with compensation, pivot ordering, and an operator queueA-3
A JOINAPI composition, or a replicated read model you keep in syncP-20, P-23
Read-your-own-writes for freeEventual consistency, and a UI that must tolerate itP-11
A stack traceDistributed tracing, or you cannot diagnose anythingO-1
One deploy, one CI pipeline, one on-call rotationN of eachO-3
Refactoring a function signatureA versioned contract with unknown callers, migrated in expand/contractO-4
npm run devRunning eight services, or mocking seven of them
One connection poolN services × M instances × pool sizeP-4

That last row deserves its own paragraph, because it is the failure that surprises teams six months into a migration. A monolith with 10 instances × 20 connections is 200 connections against one Postgres. Split it into 8 services, each with 6 instances and a pool of 20, and you have 960 — against a database that starts degrading past a few hundred (Module P-4). Nothing about the split increased the work; it multiplied the connections, and the fix is a per-service pooler and much smaller per-service pools, decided before the split rather than during the incident.

Two costs that are not on the table because they are not technical. Cognitive load: a change spanning three services requires understanding three deployment states, three contracts and three failure modes. And cost in money: N services means N sets of idle headroom, N load balancers, N log streams and N sets of baseline overhead, which Module O-8 treats as a first-class design metric rather than an accounting detail.


The Monolith Is the Right First Answer, and the Modular Monolith Is the Right Second

Module F-7 argued that the monolith is the correct starting architecture. The refinement worth making here is that "monolith" and "unstructured" are different words.

A modular monolith enforces boundaries in-process:

  • Code organised by capability (billing/, catalogue/, fulfilment/), not by layer (controllers/, services/, models/).
  • One module never touches another module's tables. Access is through an explicit exported interface, and the rule is enforced mechanically — a lint rule, a dependency-cruiser check, a build-time boundary — not by convention.
  • Cross-module calls go through interfaces narrow enough that you could put a network between them later.
  • Schema separated by prefix or Postgres schema per module, so "who owns this table" has an answer.

What that buys is the boundary discipline without the distribution tax: you keep transactions, joins, one deploy, one trace, one pool, and refactoring a boundary is a rename rather than a migration.

And it is the honest prerequisite for the other thing. A team that cannot maintain module boundaries inside one codebase will not maintain service boundaries across eight. The forces that produce a reach into another module's tables — a deadline, an unclear owner, a missing interface — do not disappear when the reach becomes an HTTP call or, worse, a direct connection to someone else's database. The modular monolith is where you find out whether your boundaries are correct, at a cost of a few hours per mistake instead of a few weeks.


The Six Real Triggers, and the Four That Are Not

Split when you can point at one of these:

  1. Deployment contention between teams. Release trains, a merge queue that is always full, a change waiting three days for someone else's fix to land. This is the actual trigger, and it usually arrives somewhere around three teams sharing a deploy unit.
  2. A genuinely divergent scaling profile. One component needs forty instances while the rest need three — image processing, a public read API, a WebSocket gateway. Note the qualifier: divergent and independent. Scaling a monolith horizontally is cheap, so this only counts when scaling the whole thing wastes a lot.
  3. A divergent technology requirement. ML inference that must be Python, video transcoding that must be Rust or C++, a component needing a runtime the rest cannot use.
  4. A divergent compliance or availability requirement. Isolating PCI scope so that most of your code is out of it (Module P-24), keeping EU data in an EU-only deployment (Module O-9), or a component with a materially stricter SLO than the rest.
  5. Blast-radius isolation for a genuinely dangerous component. Something that OOMs, or runs untrusted code, or has an unstable dependency, and whose failure must not take checkout with it (Module A-7).
  6. Strangling something you are replacing. A boundary drawn around legacy code so it can be retired piece by piece.

And the four that masquerade as triggers:

  • "The codebase is too big." That is a modularity problem, and splitting it across a network makes navigating it harder, not easier. Fix the module boundaries first — you will need them for the split anyway.
  • "Microservices scale better." They scale independently. A monolith scales horizontally too, and usually with less overhead per unit of work, since a network hop is not involved.
  • "The tests are too slow." Fix the tests. Splitting the codebase to shorten CI produces a much worse problem: you can no longer test an interaction at all.
  • "It's cleaner." Clean is a property of boundaries, and boundaries are free in-process. Distribution is what costs.

Where Boundaries Belong: Contexts and Aggregates

A bounded context is a region where a word means one thing. In a shop, "order" means a basket to the checkout team, a fulfilment instruction to the warehouse, and a revenue event to finance. Those are three models, and trying to build one shared Order object that satisfies all three produces a class with forty fields where every team is afraid to change anything. Context boundaries are the natural service boundaries because they are where the language changes.

Then the technical rule that matters most, and the one most often violated:

A service boundary must not cut through an aggregate.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.