01 / The prompt
“Should the shop move to microservices?”
The shop from section 6 sells mugs and totes. Inventory already runs as a service, and the warehouse team now owns it. One team works on everything else and releases weekly. A flash sale in November will send twenty times the usual traffic to the product pages. Card payments and email both go to outside providers that have had outages. Ask an agent the question, and three answers are plausible:
- Split it. Catalog, orders, payments, and mailer each become a service, like Inventory, so each can scale and fail on its own.
- Keep one deployment. Bring Inventory back; the modules already have front doors and their own tables.
- Decide per module. A module moves when something one deployment cannot give presses on it, and not before it is ready.
Each can be argued well. Martin Fowler’s default is the second: “The majority of software systems should be built as a single monolithic application. Do pay attention to good modularity within that monolith, but don’t try to separate it into separate services.” The question the prompt never asks is what, exactly, is pressing on each module, and is that module ready to leave?
M. Fowler, “Microservice Premium”, 13 May 2015, fetched 23 September 2026.
02 / Name the choice
A pressure, and readiness.
A modular monolith is one deployment made of modules that each own their data and expose a front door; everything ships together and a call between modules is a function call. Services are deployed separately, each with its own data; a call between them crosses a network, and each ships when its owners decide.
A module becomes a service when something presses on it that one deployment cannot meet: another team owns it, it must release on its own schedule, or its failures must not reach the rest. And only once it is ready: nothing reaches past its front door, nothing else names its tables, and its callers have a tested answer for when it is gone. More traffic alone is met by more copies of the whole shop, once the shop is safe to run in copies.
| Modular monolith | Services | |
|---|---|---|
| Releasing a change | One release carries every module’s changes | Each service releases when its owners are ready |
| A call between modules | A function call; it cannot time out | A network call that can be slow, fail, or half-succeed |
| Twenty times the traffic | Run more copies of the shop | Run more copies of the busy service |
| One part fails | A crash takes the process down | The rest keeps running, if its callers know what to do without it |
| Two writes in one checkout | Still needs a plan once a provider is involved | Needs a plan for every write that crosses |
| What you operate | One process, one set of dashboards | A process, a deploy, and an on-call rota per service |
Words to put in a prompt or a review
- Modular monolith
- One deployment of modules that each own their data behind a front door.
- Pressure
- A need one deployment cannot meet: another team, its own releases, isolated failure.
- Readiness
- A clean front door, tables nobody else names, and callers that cope without it.
- Front door
- The one file other modules may import: here, each module’s
index.ts. - Distributed monolith
- Services that must release together and fail together: the costs of both shapes.
- Bounded answer
- What a caller says, within a time limit, when the thing it called is gone.
Where the relay’s lessons feed the deskFour lessons, one shop
The desk reads the shop the section 6 relay left behind: the Extracting a service lesson’s recorded build, copied here byte for byte. Each earlier lesson left something the desk can check in that code:
- Module contracts: every module
behind its
index.ts. The scan finds no import past one. - Communication between modules: mail on events. The scan finds no module that calls the mailer.
- Module-owned data in one database: every table prefixed with its owner. The scan finds one file outside a module that
names them:
scripts/inventory-move.ts. - Consistency without a shared transaction: the checkout’s recovery, which keeps working in the recorded build.
Extracting a service adds the one measured fact the desk uses: when its checker made the Inventory service hang, the shop gave no answer before the checker gave up at 60 seconds.
03 / Follow the desk
Rule on each module, then change one condition.
The shop as recorded, the ticket’s pressures, and the desk’s verdict on every module. Then marketing asks to release the catalog every day. In Try it, change the pressures, say what callers do when a module is down, or plant a shortcut in the code.
Which modules should be services?
As recorded
- catalog: keep
- inventory: fix first
- orders: keep
- payments: keep
- mailer: keep
The shop the relay built.
Five modules. Inventory already runs as a service; catalog, orders, payments, and mailer run in the shop. Orders calls three of them during checkout, and mailer listens for events.
Reduced motion: choose a scene to see its completed state.
Read this scene
Five modules. Inventory already runs as a service; catalog, orders, payments, and mailer run in the shop. Orders calls three of them during checkout, and mailer listens for events.
As recorded. The shop the relay built. Five modules. Inventory already runs as a service; catalog, orders, payments, and mailer run in the shop. Orders calls three of them during checkout, and mailer listens for events.
Watch restarts the story when you come back. Step through keeps your step. Try it starts from the ticket’s pressures each time you open it.
04 / Read the shape
A verdict is a pressure plus readiness.
Basic form reads the code. In the wild decides one module. At the call site the desk rules on the whole shop and prints reasons a reviewer can check.
The scan: for each module, the imports that reach past its index.ts, the files outside it that name its tables, the modules that call it, whether it keeps data, and whether it already runs apart.
export type SourceFile = { path: string; text: string };
export type Facts = {
module: string;
deepImports: string[];
foreignTables: string[];
readsTablesOf: string[];
callers: string[];
keepsData: boolean;
remote: boolean;
};
const importPattern = /from\s+["'](\.{1,2}\/[^"']+)["']/g;
const tableName = /\b([a-z]+)_[a-z_]+\b/g;
const tableConstant = /\b([A-Z]+)_TABLES\b/g;
/** Modules whose tables a file names: a lowercase `<module>_<table>`, or a `<MODULE>_TABLES` constant. */
function tableOwners(text: string): string[] {
const owners = [...text.matchAll(tableName)].map((m) => m[1]);
for (const name of text.matchAll(tableConstant)) owners.push(name[1].toLowerCase());
return owners;
}
/** Resolve `../x/y.ts` against the importing file's folder. */
function resolvePath(from: string, target: string): string {
const parts = from.split('/').slice(0, -1);
for (const piece of target.split('/')) {
if (piece === '..') parts.pop();
else if (piece !== '.') parts.push(piece);
}
return parts.join('/');
}
/** What the code says about each module. Tests are not read: they may reach anywhere. */
export function scan(files: SourceFile[], modules: string[]): Facts[] {
const moduleOf = (path: string) => {
const top = path.split('/')[0];
return path.includes('/') && modules.includes(top) ? top : null;
};
const facts = new Map<string, Facts>(
modules.map((m) => [
m,
{
module: m,
deepImports: [],
foreignTables: [],
readsTablesOf: [],
callers: [],
keepsData: false,
remote: false
}
])
);
for (const file of files) {
if (file.path.startsWith('tests/')) continue;
const own = moduleOf(file.path);
for (const match of file.text.matchAll(importPattern)) {
const target = resolvePath(file.path, match[1]);
const owner = moduleOf(target);
if (!owner || owner === own) continue;
const f = facts.get(owner)!;
if (!target.endsWith('/index.ts')) f.deepImports.push(`${file.path} → ${target}`);
else if (own && !f.callers.includes(own)) f.callers.push(own);
}
for (const owner of tableOwners(file.text)) {
if (!facts.has(owner) || file.path.startsWith('db/')) continue;
if (owner === own) facts.get(owner)!.keepsData = true;
else {
if (!facts.get(owner)!.foreignTables.includes(file.path))
facts.get(owner)!.foreignTables.push(file.path);
const reader = own && facts.get(own)!;
if (reader && !reader.readsTablesOf.includes(owner)) reader.readsTablesOf.push(owner);
}
}
const service = /^([a-z]+)-service\.ts$/.exec(file.path);
if (service && facts.has(service[1])) facts.get(service[1])!.remote = true;
}
return modules.map((m) => {
const f = facts.get(m)!;
return { ...f, readsTablesOf: [...f.readsTablesOf].sort(), callers: [...f.callers].sort() };
});
} type SourceFile struct {
Path string `json:"path"`
Text string `json:"text"`
}
type Facts struct {
Module string `json:"module"`
DeepImports []string `json:"deepImports"`
ForeignTables []string `json:"foreignTables"`
ReadsTablesOf []string `json:"readsTablesOf"`
Callers []string `json:"callers"`
KeepsData bool `json:"keepsData"`
Remote bool `json:"remote"`
}
var (
importPattern = regexp.MustCompile(`from\s+["'](\.{1,2}/[^"']+)["']`)
tableName = regexp.MustCompile(`\b([a-z]+)_[a-z_]+\b`)
tableConstant = regexp.MustCompile(`\b([A-Z]+)_TABLES\b`)
servicePath = regexp.MustCompile(`^([a-z]+)-service\.ts$`)
)
// Scan reports what the code says about each module. Tests are not read: they may reach anywhere.
func Scan(files []SourceFile, modules []string) []Facts {
moduleOf := func(p string) string {
top, _, nested := strings.Cut(p, "/")
if nested && slices.Contains(modules, top) {
return top
}
return ""
}
facts := map[string]*Facts{}
for _, m := range modules {
facts[m] = &Facts{Module: m, DeepImports: []string{}, ForeignTables: []string{}, ReadsTablesOf: []string{}, Callers: []string{}}
}
for _, file := range files {
if strings.HasPrefix(file.Path, "tests/") {
continue
}
own := moduleOf(file.Path)
for _, m := range importPattern.FindAllStringSubmatch(file.Text, -1) {
target := path.Clean(path.Join(path.Dir(file.Path), m[1]))
owner := moduleOf(target)
if owner == "" || owner == own {
continue
}
f := facts[owner]
if !strings.HasSuffix(target, "/index.ts") {
f.DeepImports = append(f.DeepImports, file.Path+" → "+target)
} else if own != "" && !slices.Contains(f.Callers, own) {
f.Callers = append(f.Callers, own)
}
}
var owners []string
for _, m := range tableName.FindAllStringSubmatch(file.Text, -1) {
owners = append(owners, m[1])
}
for _, m := range tableConstant.FindAllStringSubmatch(file.Text, -1) {
owners = append(owners, strings.ToLower(m[1]))
}
for _, owner := range owners {
f, known := facts[owner]
if !known || strings.HasPrefix(file.Path, "db/") {
continue
}
if owner == own {
f.KeepsData = true
continue
}
if !slices.Contains(f.ForeignTables, file.Path) {
f.ForeignTables = append(f.ForeignTables, file.Path)
}
if own != "" && !slices.Contains(facts[own].ReadsTablesOf, owner) {
facts[own].ReadsTablesOf = append(facts[own].ReadsTablesOf, owner)
}
}
if s := servicePath.FindStringSubmatch(file.Path); s != nil && facts[s[1]] != nil {
facts[s[1]].Remote = true
}
}
out := make([]Facts, 0, len(modules))
for _, m := range modules {
f := *facts[m]
slices.Sort(f.ReadsTablesOf)
slices.Sort(f.Callers)
out = append(out, f)
}
return out
} The behavior these examples promiseChecked by shared cases from a separate model
- A file belongs to a module when its path starts with the module’s folder. Tests are not read; they may reach anywhere.
- An import into another module that does not end in
/index.tsis a deep import; one that does, from inside a module, makes that module a caller. - A file names a module’s table with a lowercase
module_nameword or aMODULE_TABLESconstant; the table registry indb/is skipped. - Only another team, its own releases, and isolated failures count as pressure. More traffic adds a reason and nothing else.
- A module with no data and exactly one caller stays in the shop.
- With a pressure, any blocker means fix first; none means extract, or stay a service if it already runs apart. With no pressure: keep, or bring it back.
Every expectation in cases.json was produced by a Python model written from
these rules, over the same recorded files; it lives in model/cases.py.
Reading the TypeScriptRegular expressions and a path resolver
Imports are found with one regular expression and resolved by walking .. segments, so ../catalog/store.ts from orders/index.ts becomes catalog/store.ts. The verdict is a small decision table written as two
ternaries, one for a module in the shop and one for a module that runs apart.
Reading the GoRE2, and a pointer for “not measured”
Go’s regexp is RE2: no back-references or look-ahead, which is why the
table rule reads whole files rather than string literals. “Not measured” is a nil *bool, so it marshals to the same null the shared cases hold.
Run it yourselfNo dependencies
Copy the complete TypeScript file and run node --experimental-strip-types desk.ts with Node 22.18 or later. For Go, save main.go next to this go.mod and run go run .. Both print:
module heyrian.dev/lessons/modular-monolith-vs-services
go 1.23
orders: keep (more traffic is met by running more copies of the shop; no pressure one deployment cannot meet) pricing: keep (no data of its own and one caller (orders): a service would only forward) stock: fix first (pressure: team; 1 import reaches past its index.ts; callers waited without limit when it was down (recorded))
05 / Review the agent’s diff
“Extracted the catalog ahead of the flash sale.”
Twenty times the product-page traffic is the first thing anyone reads in the ticket, and a catalog service is the first answer it invites. Neither recorded agent made this change; it is the one the question makes easy to ask for. Before you merge, ask what presses on the catalog that one deployment cannot meet.
06 / How it fails
Each shape fails in its own place.
The modular monolith fails together and releases together. Services fail apart, and every boundary is somewhere a checkout can wait, half-finish, or answer wrongly.
| What goes wrong | What people see | Where it comes from |
|---|---|---|
| Slow: a service hangs | Checkout spins, then nothing. The customer does not know whether they bought a mug. | Recorded: the relay’s Inventory service, 60 seconds without an answer. |
| Down: the shop crashes | Everything is down at once, catalog included. | Authored; the price of one process. |
| Wrong: two owners of one table | Stock in the service and stock in the shop disagree after a move back. | Shared case: scripts/inventory-move.ts names Inventory’s tables from outside it. |
| Stuck: a release waits for another team | The warehouse team’s fix waits for the shop’s weekly release. | Shared case: Inventory’s pressure; the reason it is a service. |
| Wasted: a service that forwards | A payment service that only calls the card provider adds a hop and a way to fail. | Shared case: payments, one caller and no data. |
| Half-done: a charge across a boundary | The card is charged and the order is not recorded. | Covered in Consistency without a shared transaction; the recovery survives in the recorded build. |
07 / Is it worth it?
For Inventory, yes. For the rest, not yet.
Run the shop as it stands (one deployment and Inventory apart) against every module as a service, over the same four kinds of change.
| Change | The shop and one service | Every module a service |
|---|---|---|
| A second client: a mobile app | It calls the shop’s endpoints. | It calls the same endpoints, through a gateway. No real difference. |
| Replace a dependency: a new card provider | A change in payments, released weekly. | A change in the payments service. No difference in what changes. |
| Change a rule: a price that varies by warehouse | Catalog and orders change together, in one release. | Two services and a contract change, released in order. |
| A second team: the warehouse | They release Inventory on their own schedule. | The same, for Inventory. Nobody else asked for it. |
Before you move a module either way, decide what you will measure:
- Time from a finished change to production, per team: the pressure a service is meant to relieve. If the warehouse team’s changes do not ship sooner, the service is not paying for itself.
- Checkout’s answer when each dependency is slow or down: the time to an answer, and which answer. The recorded Inventory service had none within a minute.
- Changes that needed two releases, from merged pull requests: the cost of a boundary in the wrong place, as in How big should a module or service be?
- Product-page latency at the flash sale’s load, with more copies of the shop: the evidence that traffic did not need a service. Before that, check that two copies can run at all; section 08 shows why.
The shop runs on a laptop in these lessons. There are no production release times or latencies here, and the lesson gives none. Baseline before you change is how to take them.
08 / Ask for it
Two agents, one shop, the same verdict, and the same gap.
We copied the relay’s latest build into two folders and sent two agents running Claude
Sonnet the question from section 01, with its four facts, at the same time: decide module by
module, write DECISION.md, make the changes the decision needs, and keep every
endpoint working. The topology prompt added this lesson’s rule, including “every caller gets
an answer in bounded time when it is slow or down, and that answer is tested”. A script then
ran each result with its own card and mail providers and a proxy in front of Inventory, made
Inventory and the card provider hang, started two copies of the shop on one crashed
checkout, and ran this lesson’s desk over the code.
| What the checker did | Plain prompt | Topology prompt |
|---|---|---|
| Files it changed | DECISION.md, tests/decision.test.ts | DECISION.md, tests/inventory-outage.test.ts |
| Modules that run apart | inventory-service.ts | inventory-service.ts |
| A checkout, Inventory as a service | 201 in 0.1 s | 201 in 0.0 s |
| A checkout while Inventory hangs | no answer in 60 s | no answer in 60 s |
| Product pages while Inventory hangs | 503 in 5.0 s | 503 in 5.0 s |
| A checkout while the card provider hangs | no answer in 60 s | no answer in 60 s |
| Two copies recover one crashed checkout: orders per trial | 1, 1, 1, 1, 1 | 1, 1, 1, 1, 1 |
| The desk over the result | catalog: keep; inventory: fix first; orders: keep; payments: keep; mailer: keep | catalog: keep; inventory: fix first; orders: keep; payments: keep; mailer: keep |
| Its own tests | 104 of 104 pass | 105 of 105 pass |
Both agents reached the desk’s topology on their own: Inventory stays the warehouse team’s service, and catalog, orders, payments, and mailer stay in the shop. Neither split anything for the flash sale. Neither changed a line of the shop’s code; each wrote its decision and a test that pins it.
Neither fixed what the desk says to fix first. When Inventory hangs, a checkout still gets no answer within a minute in both builds. The plain agent never looked. The topology agent looked, tested that product pages answer 503 in five seconds, and then decided checkout should stay unbounded, citing the relay’s own consistency rule that an unknown outcome must never be guessed:
- **Writes** (`reserveStock`/`releaseStock`, the checkout path):
deliberately **not** bounded — `orders/index.ts`'s
`resolveReservation`/`resolveRelease` retry until the service
actually answers, because guessing "declined" or "reserved" on an
unknown outcome risks releasing stock that was actually held, or
the reverse (`CONSISTENCY.md`). This is not a gap in the bounded-
time rule; it's the one case where a wrong fast answer is worse than
a slow correct one, and it was already tested before this change
(`tests/service-move.test.ts`'s "service going down mid-checkout"
case). Both instructions were right, and together they meant a customer staring at a spinner. A
bounded answer does not have to be a guess: “we are confirming your order, and we will email
you” is an answer in two seconds, while the recovery the consistency lesson built finishes
the checkout in the background. Both DECISION.md files also call the payment client
timeout-bounded; with the card provider holding the request, checkout gave no answer in a minute
either.
The plain agent found something the desk does not ask. Its answer to the flash sale is the desk’s answer, more copies of the shop, and it warned that the checkout recovery is not safe to run in two copies at once:
Handling the flash sale's 20x product-page traffic in practice will mean
running more than one instance of the shop process for that day. That is
a deployment/replica-count question, not a module-boundary one, so it is
not decided here — but it is worth flagging: `orders/store.ts`'s
`_completeCheckout` checks a checkout's status and then writes its
completion in a second step; within one process this is safe (the check
and the write happen in the same synchronous call, so nothing else in
that process can interleave), but if two *separate* shop processes ever
raced to resume the exact same crash-orphaned checkout at once (e.g. two
replicas both running `recoverCheckouts()` against the same `SHOP_DB` at
startup), nothing today stops both from committing an order for it. This
was already true before this decision and isn't introduced by it; it
would need its own fix (e.g. a uniqueness constraint on
`orders_orders.checkout_key`) the day multiple shop replicas against one
`SHOP_DB` actually becomes the plan.
In the completed run, each of five recovery trials produced one order in both builds. That did not make recovery safe to run in parallel: in the plain build, one copy failed to start in one trial, and another trial logged a database-lock error during recovery. An earlier topology run also stopped this check when a copy could not start because the database was locked. More copies require coordination as well as the same code.
The line both prompts lacked: when a caller cannot know an outcome in time, it answers within N seconds with “pending” and a way to check, and finishing the work is someone else’s job. The desk can say a module is not ready. Only a prompt can say what “ready” answers.
How the runs were made and checkedTwo decisions, one checker
- Both folders started as the Extracting a service lesson’s recorded
extractionbuild, restored from its evidence and checked against its checksums (40 of 40 files). The copy is kept inevidence/base. - Both agents received the prompts word for word, in fresh contexts, at the same time; the only differences were the topology block, the folder, and the ports. The files each wrote are kept byte for byte with checksums, and each result’s diff against the base.
- The run notes were not written: the authoring session was cut off by an API limit after
the checker finished, and the run transcripts were cleared before the notes could be
taken, so this page cannot show a transcript audit. What follows is the authoring
session’s account, and only its first part is written down in the tree (
evidence/prompts.md): the prompt files lived two folders above the run folders, and no transcript mentions them. It also reported that both agents stopped their processes by process id, and that the plain agent’s checks read spilled output of its own tools, outside its folder, which the harness wrote there; no notes back those two. - The first checker run was interrupted. The second recorded a database-lock failure during the two-copy check; the final checker records failed starts and recovery errors per trial so the remaining trials can finish. Earlier outputs are kept alongside the completed report. The final run reports 104 of 104 tests passing for the plain build and 105 of 105 for the topology build. Those passing tests do not remove the outage and recovery findings above.
- One run of each prompt is a sample, not a measurement of a model.
09 / Hold it there
Keep the modules ready, and the decision written down.
A modular monolith stays a choice only while each module could still leave. Three checks keep it that way.
The platform’s door: a package’s exports
Node’s package
exportsfield allows “multiple entry points to be defined, conditional entry resolution support between environments, and preventing any other entry points besides those defined in "exports". This encapsulation allows module authors to clearly define the public interface for their package” (Node.js packages, v26.10.0, fetched 23 September 2026). Make each module a workspace package that exports only itsindex.ts, and a deep import stops resolving. On a scratch fixture with a workspace link and Node 22.21.1, the front-door import ran and the deep one failed withERR_PACKAGE_PATH_NOT_EXPORTED.catalog/package.json and orders/index.ts // catalog/package.json { "name": "@shop/catalog", "type": "module", "exports": { ".": "./index.ts" } } // orders/index.ts import { getProduct } from '@shop/catalog'; // resolves import { getRow } from '@shop/catalog/store.ts'; // ERR_PACKAGE_PATH_NOT_EXPORTEDA rule a check enforces
Run the desk in CI over the repository and a pressures file the team keeps next to the code. A deep import, a table named outside its owner, or a module that runs apart with no pressure fails the build, and the pressures file is where a new reason to split has to be written down first. Architecture as rules covers turning sentences into checks like this one.
decision.spec.ts // decision.spec.ts, run on every pull request import { readFileSync } from 'node:fs'; import { expect, it } from 'vitest'; import { desk } from './desk'; import { sourceFiles } from './source-files'; const situation = JSON.parse(readFileSync('PRESSURES.json', 'utf8')); it('runs apart only what a written pressure needs apart', () => { const verdicts = desk(sourceFiles('.'), situation.modules, situation); expect(verdicts.filter((v) => v.verdict !== 'keep' && v.verdict !== 'stay a service')).toEqual([]); });A check on what actually happens
For every module that runs apart, make it hang behind a proxy and time the caller’s answer, as the checker in section 08 does to Inventory. Readiness is not a document; it is an answer measured in seconds.
The frontend version of this choice is whether two teams ship parts of one page separately, which Micro-frontends covers. Nothing here runs in a browser, so this lesson has no frontend row.
10 / Make the call
One deployment, one service, and a written reason for each.
For this shop I would keep catalog, orders, payments, and mailer in one deployment, and keep Inventory as the warehouse team’s service, after two fixes: checkout answers within a few seconds when Inventory is slow or down, and the move scripts move to the warehouse team’s repository with the tables they touch. I accept one network hop in every checkout and a weekly release for everything else. For the flash sale, the shop needs a database that several copies can share before it runs as several copies. I would reconsider when a second team takes a module, when marketing must change the catalog faster than weekly, or when a module’s failures start taking checkout down with them.
Keep a note of the decision
- Why
- The warehouse team owns Inventory and releases on its own schedule; one team and a weekly release cover the rest; a flash sale brings twenty times the traffic.
- What
- A modular monolith for catalog, orders, payments, and mailer; Inventory as its own service, with a bounded answer at checkout.
- Constraint
- Only another team, its own releases, or isolated failures justify a service; a module leaves only with a clean front door, its own tables, and a tested answer when it is down.
- Fallback
- The flash sale runs on more copies of the shop, after its data moves to a database they can share; checkout answers “pending” in seconds when Inventory is down; everything else still ships weekly.
- Reconsider when
- A second team takes a module, the catalog must release more often than weekly, or one module’s failures keep reaching checkout.
Take it with you
Explain it without saying “microservices”: “A part runs on its own when someone needs it to, another team, another release schedule, a failure we must contain, and when the rest can live without it for a minute. Until then it is a folder with a front door.” Then list the services you run, and write one pressure next to each.
Paste into your next prompt, and fill in the blanks
Decide module by module. A module becomes, or stays, a service only for a pressure one deployment cannot meet: <another team owns it | it releases on its own schedule | its failures must not reach <the critical path>>. More traffic alone is met by running more copies. A module with no data that one other module calls is never a service. Before a module runs apart, every caller has a tested answer in <N> seconds when it is slow or down, nothing outside it names its tables, and nothing imports past its front door. Write each verdict in DECISION.md with what would change it.
Connections to follow nextRelated lessons
- Modular monolith is where this shop started, and the shape the verdict keeps.
- Extracting a service is how a module with a verdict of extract actually moves.
- How big should a module or service be? sizes the units this lesson decides to deploy.
- Conway’s law and team boundaries is why another team is the strongest pressure.
- Containing failure is the bounded answer the desk asks every caller for.