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 had
What you get
Module
A function call: nanoseconds, cannot half-succeed
A network call: milliseconds, can time out ambiguously
F-5, P-16
An exception you handle locally
Partial failure — the other service is up, slow, or lying
A-6, A-7
BEGIN … COMMIT across five tables
A saga, with compensation, pivot ordering, and an operator queue
A-3
A JOIN
API composition, or a replicated read model you keep in sync
P-20, P-23
Read-your-own-writes for free
Eventual consistency, and a UI that must tolerate it
P-11
A stack trace
Distributed tracing, or you cannot diagnose anything
O-1
One deploy, one CI pipeline, one on-call rotation
N of each
O-3
Refactoring a function signature
A versioned contract with unknown callers, migrated in expand/contract
O-4
npm run dev
Running eight services, or mocking seven of them
—
One connection pool
N services × M instances × pool size
P-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:
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.
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.
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.
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.
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).
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.
An aggregate is a cluster of data that must change together to preserve an invariant — an order and its line items, a ledger transaction and its entries (Module P-24). If the invariant "the entries of a transaction sum to zero" spans two services, you have converted a CHECK constraint into a saga (Module A-3), with compensation, an operator queue, and a low background rate of unrecoverable states. That is an enormous price for a boundary that could have been drawn ten centimetres to the left.
So the design procedure is: find the invariants first, keep each one inside one service, and draw boundaries in the gaps. Where an invariant genuinely must span services, that is strong evidence the boundary is wrong.
Two more heuristics:
Split along change boundaries, not noun boundaries. Things that change together belong together. The classic failure is a service per entity — an "Address service," a "User service," a "Notification service" — which guarantees that every feature touches four services and every deploy is coordinated. Ask "what changes together when we build a feature?" rather than "what are the nouns?"
Conway's law is a constraint, not an observation. Your architecture will come to mirror your communication structure, because boundaries that cut across a team get eroded by the shortest path to shipping. Choose boundaries that match team ownership, or the org will reshape them for you — and a service owned by nobody in particular becomes the place where everyone puts things they cannot find a home for.
The Distributed Monolith: All the Costs, None of the Benefits
If you guess the boundaries wrong you get the worst available architecture, and it has a recognisable symptom list:
Symptom
What it means
Services must be deployed together, in an order
there is no independent deployability — the only benefit is gone
One feature requires PRs in five repositories
boundaries cut across the direction of change
Synchronous call chains four or five deep
availability and latency multiply (see below)
Multiple services write the same database tables
you have one service with several deployables
A shared library everyone must upgrade in lockstep
compile-time coupling recreated across the network
A single user request fans out to eight services
your p99 is the worst of eight p99s
You cannot test one service without running six
boundaries are not real
A failure in a peripheral service breaks checkout
no isolation was actually achieved
The arithmetic behind the third and sixth rows is worth doing once, because it is unintuitive and decisive.
Availability multiplies in series. Five services, each 99.9% available, in a synchronous chain: 0.999⁵ ≈ 99.5%. You have turned four nines of component reliability into two and a half nines of user-visible reliability, and every service in the chain is individually blameless.
Tail latency amplifies on fan-out. If one service's p99 is 100 ms, and a request calls ten such services in parallel and waits for all of them, the chance that at least one is in its slow tail is roughly 1 − 0.99¹⁰ ≈ 10%. So the request's p90 is now what used to be a p99 event. This is Module F-4's point in its most expensive form: fan-out converts rare slowness into common slowness, and it is why a request touching eight services feels sluggish even when every service's dashboard looks healthy.
Both are arguments for the same two things: shallow synchronous call graphs, and asynchronous integration wherever the operation permits it.
Synchronous request/response (HTTP, gRPC). Simple, immediately consistent, easy to debug — and it couples availability: if B is down, A cannot serve. Every synchronous dependency is a multiplication in the arithmetic above.
Asynchronous events (Kafka, queues). Decouples availability — B can be down for an hour and process the backlog later — at the cost of eventual consistency and the need for idempotent consumers (Module P-16).
The heuristic that resolves most cases: query synchronously, mutate asynchronously. A user needs an answer now, so reading through a synchronous call is often fine. But "when an order is placed, the warehouse must know" does not need to happen inside the request — publish an event (via an outbox, Module A-3) and let the warehouse consume it. That single reframing removes most synchronous chains, and with them most of the availability multiplication.
Where synchronous reads themselves become the problem — because service A needs B's data on every request — the answer is not a faster call but a local read model: A subscribes to B's events and keeps its own copy of the small slice it needs. This is CQRS in its most useful and least ceremonial form. The cost is duplicated data and a staleness window, and the benefit is that A can serve while B is down, with no network hop on the read path. Keep it small and deliberately scoped: a read model that grows into a full replica of B's data is a boundary violation with extra steps.
Database-Per-Service Is the Boundary; Everything Else Is Decoration
If two services write the same tables, they are one service. Full stop — the deployment independence you were buying does not exist, because a schema change requires coordinating both, and a bug in either can corrupt the other's invariants.
So each service owns its data exclusively. The consequences, which are the real cost of the split:
No cross-service joins. Composition happens in the calling service (fetch, then combine), or against a replicated read model. Neither is as good as a JOIN, and pretending otherwise is how a five-service fan-out gets built.
No cross-service referential integrity. There is no foreign key from orders to customers when they are in different databases. Deletion, in particular, becomes a coordination problem (Module O-9).
Reporting needs somewhere else to live. Cross-service questions — revenue by region by product — cannot be answered by any single service, which is exactly the case for a warehouse fed by CDC (Modules P-22, P-23).
Migrations become per-service and versioned contracts become mandatory (Module O-4).
A useful intermediate step, when splitting an existing system: schema-per-service inside one Postgres instance, with a role per service granted access only to its own schema. You get enforced ownership, one operational database, and joins are prevented by permissions rather than by convention — while leaving the option to move a schema to its own instance later. It is the most under-used step in the migration path.
Splitting Something That Is Already Running
Strangler fig. Put a routing layer (a proxy or gateway, Module F-15) in front of the monolith. Extract one capability into a new service, route just those paths to it, and leave everything else alone. Repeat. The monolith shrinks; at no point is there a big-bang cutover.
Branch by abstraction for the code path: introduce an interface, implement it twice (in-process and remote), switch with a feature flag, remove the old implementation. Combined with a flag you can flip per tenant, the rollback path is a config change rather than a deploy.
Practical rules, learned expensively by others:
Start with the most divergent, least coupled piece — the thing that scales differently or needs a different runtime — not the piece at the centre of the domain.
Do not split and rewrite simultaneously. Move the code as-is, then improve it. Doing both means a failure has two possible causes and no clean rollback.
Decide when the data moves, and treat it as the hard part. Extracting code while both services share a database gives you a distributed monolith immediately; extracting data first requires a dual-write or CDC period (Module P-23) with a reconciliation check. Either way, the data migration is the project, and the code extraction is the easy half.
Keep the boundary reversible for a while. Merging two services back is a legitimate outcome, and a team that treats a split as irreversible will defend a wrong boundary for years.
Instrument before you split. You cannot draw a boundary well without knowing which code paths actually call which, and you cannot debug the result without tracing (Module O-1) that must exist beforehand.
Why this matters in production: the failure mode of a premature split is not a dramatic outage, it is a permanent tax. Feature delivery slows because every change spans repositories; incidents take longer because the cause is one hop away from the symptom; the database is saturated by connections rather than by work; and the eventual consistency nobody planned for surfaces as a slow trickle of support tickets about data that "looks wrong for a few seconds." None of it appears on a dashboard, none of it is attributable to a single decision, and the remediation — merging services back together — is politically harder than the split was.
Where This Shows Up in Your Stack
PostgreSQL: do the connection arithmetic before splitting — services × instances × pool is the number that hits the database (Module P-4), and a split that multiplies it by five is a capacity incident with no extra work being done. Use a pooler per service and much smaller pools. Schema-per-service with a role per service is the best intermediate boundary available, because permissions enforce ownership that convention cannot. And accept that cross-service reporting belongs in a warehouse (Module P-22), not in a clever dblink.
Node.js: a monorepo with workspaces is what makes a modular monolith practical — separate packages with explicit dependencies, enforced by the build rather than by review. Beware the shared internal library that every service must upgrade together: it is compile-time coupling reintroduced after you paid to remove it. Keep shared code to genuinely stable primitives (types, logging setup), and duplicate small things rather than coupling on them.
Kafka: the async spine that makes "mutate asynchronously" possible, and the transport for the event-driven read models that remove synchronous read chains. Publish via an outbox (Module A-3), consume idempotently (Module P-16), and treat your published events as the versioned contract they are (Module P-23).
Observability: distributed tracing changes from useful to mandatory the moment a request spans services, and it must be in place before the split (Module O-1). Correlation IDs propagated through every hop are the minimum; without them, "why was this request slow" has no answer.
Serverless / function-per-endpoint: the extreme of this module's failure mode. Module F-10's constraints plus one boundary per function gives maximum distribution with minimum boundary thought, which is how you get a distributed monolith of ninety Lambdas that must be deployed together.
Summary
Microservices solve an organisational problem: independent deployability by independent teams. Every technical benefit claimed for them is either achievable more cheaply in a monolith, a side effect, or false. If you do not have deployment contention between teams, you are paying the whole bill for none of the benefit.
One service boundary costs: a network call where a function call was, ambiguous partial failure, a saga instead of a transaction, API composition or a replicated read model instead of a JOIN, eventual consistency in the UI, mandatory distributed tracing, N pipelines and rotations, versioned contracts with unknown callers, and a local dev story that needs eight processes.
The cost that surprises people is connections:services × instances × pool can take you from 200 to nearly 1,000 connections against the same database, with no increase in actual work (Module P-4). Do that arithmetic before the split, not during the incident.
The modular monolith is the right second answer: organise by capability rather than layer, forbid cross-module table access, enforce it with a build-time rule rather than convention, and separate schemas per module. You get boundary discipline while keeping transactions, joins, one deploy and one trace — and a wrong boundary costs hours instead of weeks.
It is also the prerequisite. A team that cannot hold module boundaries in one codebase will not hold service boundaries across eight, because the pressures that cause the violation do not change when the reach becomes an HTTP call.
The six real triggers: deployment contention between teams (the actual one), a genuinely divergent scaling profile, a divergent technology requirement, a divergent compliance or availability requirement, blast-radius isolation for a dangerous component, and strangling something you are replacing.
The four false ones: "the codebase is too big" (a modularity problem, and distribution makes navigation worse), "microservices scale better" (they scale independently; monoliths scale horizontally too), "the tests are slow" (fix the tests; splitting means you can no longer test the interaction at all), and "it's cleaner" (clean is a property of boundaries, and boundaries are free in-process).
A bounded context is where a word stops meaning one thing — "order" to checkout, fulfilment and finance are three models, and forcing one shared object produces a forty-field class nobody dares change.
A service boundary must never cut through an aggregate. Splitting an invariant across services converts a CHECK constraint into a saga with compensation, an operator queue and a background rate of unrecoverable states. Find the invariants first, keep each inside one service, and draw boundaries in the gaps.
Split along change boundaries, not noun boundaries. A service per entity — Address, User, Notification — guarantees every feature touches four services. And Conway's law is a constraint to design with: boundaries that cut across a team get eroded, and a service owned by nobody becomes a dumping ground.
The distributed monolith has recognisable symptoms — lockstep deploys, five repos per feature, deep synchronous chains, shared tables, a shared library upgraded in lockstep, eight-service fan-outs, untestable services, and peripheral failures breaking checkout.
Do the two pieces of arithmetic. Five services at 99.9% in series is 99.5% user-visible availability. And a fan-out to ten services each with a 100 ms p99 gives roughly a 10% chance that at least one is in its tail — so fan-out converts rare slowness into common slowness, which is why an eight-service request feels slow while every dashboard is green.
Query synchronously, mutate asynchronously. Publish an event via an outbox instead of calling the next service in the request path, and where a synchronous read is on every request, keep a small local read model fed by events — accepting duplicated data and a staleness window in exchange for serving while the other service is down. Keep the read model narrow, or it becomes a boundary violation with extra steps.
Database-per-service is the boundary. Two services writing the same tables are one service with two deployables. The real costs are no cross-service joins, no referential integrity, reporting that must live in a warehouse fed by CDC, and mandatory contract versioning. Schema-per-service with a role per service is the most under-used intermediate step, because permissions enforce what convention cannot.
Split with a strangler fig and branch-by-abstraction: route one capability at a time behind a gateway, start with the most divergent and least coupled piece, never split and rewrite at once, treat the data migration as the actual project, keep the boundary reversible, and get tracing in place beforehand.
A premature split fails as a permanent tax rather than an outage — slower delivery, longer incidents, a saturated database, and a trickle of "the data looks wrong for a few seconds" tickets — none of which appears on a dashboard, and whose remediation is politically harder than the split was.
The next module, Service Discovery and Service Mesh, addresses the question this one creates. Once there are N services, something has to know where they are, which instances are healthy, and how to retry, encrypt and observe every hop between them — and the answer has evolved from a config file to a client library to a sidecar proxy beside every process. A-5 covers each, and gives a frank accounting of what a mesh costs in latency, resource overhead and operational surface, because that bill is routinely understated.
Knowledge Check
A team of nine engineers on one product splits their Rails-style monolith into eleven services, citing scalability and code cleanliness. Six months later: feature work is slower, most features require changes in three or four repositories, deploys happen in a fixed order, and the shared Postgres instance is intermittently refusing connections despite unchanged traffic. What does the module identify as the causes?
During a split, an architect proposes a Ledger Entries service separate from a Ledger Transactions service, on the grounds that entries are high-volume and transactions are low-volume, so they scale differently. What objection does the module raise?
A product page assembles data by calling ten services in parallel and waiting for all of them. Each service reports a p99 latency of 100 ms and 99.9% availability, and every dashboard looks healthy — yet users describe the page as frequently slow. What arithmetic explains this, and what does the module prescribe?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.