System Design (High-Level Design) Module 13 – Architecture styles at scale
Monolith, modular monolith or microservices?
Compare monoliths, modular monoliths and microservices on what each costs and buys, draw boundaries by data ownership and spot the signals for a split.
What you will learn
- Compare monoliths, modular monoliths and microservices on what each deploys together and who owns the data
- Estimate what a chain of synchronous services costs in latency and in failed minutes a month
- Draw service boundaries from business capabilities and data ownership
- Recognise the signals that justify moving one part out, and the reasons that do not
- Choose a structure for an organisation from its teams, its load and its data rules
Before you start
What you’ll learn · Before you start
What you will learn
- Compare monoliths, modular monoliths and microservices on what each deploys together and who owns the data
- Estimate what a chain of synchronous services costs in latency and in failed minutes a month
- Draw service boundaries from business capabilities and data ownership
- Recognise the signals that justify moving one part out, and the reasons that do not
- Choose a structure for an organisation from its teams, its load and its data rules
Before you start
On this page
A request that needs the work of eight components can run them as eight modules of one application or as a chain of eight services. With every deployable up 99.95 % of the time, the chain fails for about 172 minutes in 30 days and the single application for 22, and the chain is about 6 ms slower at the median. Every split pays such a price for independent deployment and scaling.
A monolith is one application deployed as one unit. A modular monolith: One application deployed as one unit, whose modules call each other only through public interfaces and each read and write only their own tables. is still one deployable, but its code is divided into modules that call each other only through public interfaces and that each own their tables. Microservices split the application into services that are deployed independently, each with its own data, and that talk over the network. Start with a modular monolith, and move a part out only when a specific need demands it: a team that must release on its own, a part that scales or fails very differently, or data under separate legal rules.
What each structure deploys together, and who owns the data
One food-delivery application can take all three shapes. Look at where each panel draws its boundaries and where the data lives:
The same application in three structures
Text description of the diagram
The figure has three panels, one under the other.
- Monolith. One app holds ordering, payments, delivery and the rest, and any part may call any other part's code. It uses one database in which every table is open to all code.
- Modular monolith. One app is divided into six modules, of which ordering and payments are shown. Ordering calls payments only through the functions payments makes public. The app still uses one database, but each module has its own schema and touches only its own tables.
- Services by capability. Ordering, catalogue and payments are separate services, each with its own database. To place an order, ordering calls catalogue synchronously to price the cart and payments synchronously to authorise the payment, then publishes an OrderPlaced event to an event stream. Delivery and notifications, also services with their own databases, read the event later.
A monolith keeps the code, the deployment and usually the database in one piece. That makes it the cheapest structure to build, test and run: a change that touches three features is one commit and one deploy, and one database transaction can cover everything a request writes. Its costs grow with size. James Lewis and Martin Fowler describe the pattern in their article on microservices: any change means rebuilding and redeploying the whole application, the release cycles of unrelated features are tied together, module boundaries erode over time, and the whole thing must be scaled together. A memory leak in one corner restarts all of it.
A modular monolith keeps the single deployable and adds boundaries inside it. Each module covers one business capability: Something the business does, such as ordering, payments or delivery, owned from its interface to its data by one team: the usual line along which modules and services are drawn., such as ordering or payments, publishes the functions other modules may call, and is the only code that reads or writes its tables. Alistair Cockburn’s hexagonal architecture gives each module a similar shape: the domain logic sits inside and reaches databases and other systems only through ports, with adapters that translate, so the logic can be tested without either. Shopify chose this structure for its core Ruby on Rails application, among the largest anywhere, with more than a thousand developers: about 6,000 classes were sorted into components named after business concepts such as orders, shipping and billing, each meant to have a public interface and sole ownership of its data, so the code gained modules without gaining deployables (Shopify Engineering).
A monolith with folders
Modules that read each other’s tables, or call each other’s private functions, are a monolith with folders: the boundaries exist only in the file tree, and nothing stops the next shortcut. Let only a module’s own code touch its tables, and let other modules call only the functions it makes public.
Microservices turn each capability into a separate service, running in its own process and deployed on its own, which Lewis and Fowler give as one main reason to use services at all. Each service manages its own database, so the services coordinate without shared transactions and accept that data elsewhere is eventually consistent. And every call to another service can fail, so each caller must cope with timeouts and missing answers. Fowler calls the total cost of running a distributed system the microservice premium: it slows a team down until the system is complex enough to need what services give.
| Criterion | Monolith | Modular monolith | Microservices |
|---|---|---|---|
| Deploys as | One unit | One unit | One unit per service |
| Parts call each other | Any function, freely | Public functions only | Over the network |
| Data | One shared schema | One database, a schema per module | A database per service |
| One transaction across parts | Yes | Yes | No: events, retries and compensation |
| A crash or leak takes down | Everything | Everything | One service, if callers cope |
| Cost to run | Lowest | Low | A pipeline, dashboards and on-call per service |
| When to choose | A small team still finding its product, for a while | Most systems, as the default | Many teams, or parts with very different needs |
The table reads as one rule: the two single-deployable structures share everything a process shares, transactions and crashes alike, and only microservices separate both.
The default
Start with a modular monolith. It costs almost what a monolith costs, and its boundaries are the seams along which a part can later leave.
Checkpoint
Check: Three structures and their data
What a network hop costs, in milliseconds and minutes
A function call inside one process takes nanoseconds and either returns or throws. A call to another service crosses the network, encodes and decodes its data, and may time out, fail halfway or arrive twice. Lewis and Fowler put it plainly: remote calls are more expensive than in-process calls, so service interfaces must be coarser than the function calls they replace. The model below puts numbers on one request whose work needs eight components, either as eight modules in one process or as a chain of synchronous services, under stated assumptions:
"""One request that needs the work of several components, built two ways:
in one process, where the components call each other as functions, and as a chain of synchronous services,
where every step after the first adds a network hop.
Every number below is an assumption of the model, chosen to be typical inside one data centre zone:
- each component's own work: median 3 ms, p99 about 12 ms (lognormal);
- each service-to-service hop (network, encoding, the receiving server): median 0.6 ms, p99 about 4 ms;
- each deployable is up 99.95 % of the time, and deployables fail independently."""
import math
import random
import statistics
random.seed(7)
REQUESTS = 50_000
AVAILABILITY = 0.9995
MINUTES_IN_30_DAYS = 30 * 24 * 60
def work_ms():
return random.lognormvariate(math.log(3.0), 0.6)
def hop_ms():
return random.lognormvariate(math.log(0.6), 0.8)
def percentiles(samples):
q = statistics.quantiles(samples, n=1000)
return statistics.median(samples), q[989]
def chain(services):
"""A request whose work passes through `services` synchronous services, one after another."""
return [sum(work_ms() for _ in range(services)) + sum(hop_ms() for _ in range(services - 1)) for _ in range(REQUESTS)]
def one_process(components):
"""The same work done by `components` modules inside one deployable: no hops between them."""
return [sum(work_ms() for _ in range(components)) for _ in range(REQUESTS)]
print("design p50 ms p99 ms availability failed minutes in 30 days")
rows = {}
for label, samples, deployables in [
("1 service", chain(1), 1),
("4 services in a chain", chain(4), 4),
("8 services in a chain", chain(8), 8),
("8 modules in one process", one_process(8), 1),
]:
p50, p99 = percentiles(samples)
up = AVAILABILITY**deployables
failed = (1 - up) * MINUTES_IN_30_DAYS
rows[label] = (p50, p99, failed)
print(f"{label:<26} {p50:>8.1f} {p99:>8.1f} {up:>11.3%} {failed:>10.0f}")
chain8, process8 = rows["8 services in a chain"], rows["8 modules in one process"]
print(f"\n8 services instead of 8 modules: +{chain8[0] - process8[0]:.1f} ms at p50, +{chain8[1] - process8[1]:.1f} ms at p99,")
print(f"and {chain8[2] / process8[2]:.1f} times the failed minutes, from 7 hops and 7 more deployables that can fail") Output
design p50 ms p99 ms availability failed minutes in 30 days 1 service 3.0 12.0 99.950% 22 4 services in a chain 16.1 31.9 99.800% 86 8 services in a chain 33.8 54.3 99.601% 172 8 modules in one process 27.9 48.2 99.950% 22 8 services instead of 8 modules: +5.9 ms at p50, +6.1 ms at p99, and 8.0 times the failed minutes, from 7 hops and 7 more deployables that can fail
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 latency_budget.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Two costs appear. The seven hops add about 6 ms at the median and at the 99th percentile: small for one request, but they add up in every request that takes this path. The larger cost is availability. When each deployable is up 99.95 % of the time and fails independently, a request that needs all eight is up only as often as all eight are at once, the product of their availabilities, as the availability maths lesson shows: 99.60 % instead of 99.95 %, or about 172 failed minutes in 30 days instead of 22. The Availability and Beyond whitepaper makes the same point about hard dependencies: each one multiplies into the availability of everything that needs it. Watch the product shrink as the chain grows:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- One deployable. The request needs one service, up 99.95 % of the time. In 30 days that leaves about 22 minutes in which the request fails.
- Four in a chain. Split the work over four services that call each other in turn. The request succeeds only while all four are up at once, 0.9995 to the fourth power, 99.80 %.
- The minutes add up. At 99.80 % the request fails for about 86 minutes in 30 days, and with three hops on the way, about 0.6 ms each at the median, the median request takes 16.1 ms.
- Eight in a chain. With eight services the request is up 99.60 % of the time, 0.9995 to the eighth power, which is about 172 failed minutes in 30 days, eight times those of one deployable.
- The same work in one process. The same eight components as modules of one deployable are up 99.95 % of the time, with 22 failed minutes, and 5.9 ms faster at the median, because no call crosses the network.
- Keep the chain short. Every service the request waits on multiplies its availability in. Call only what the request cannot do without, and move the rest after the commit.
So a service design keeps synchronous chains short. Call only what the request cannot do without, move everything else after the commit (a confirmation email can be sent a second later from a queue), give every call a timeout and a retry budget, and stop calling a dependency that is failing; the lessons on timeouts, retries and backoff and on circuit breakers and bulkheads cover each tool.
Key idea
Every service a request waits on adds its latency and multiplies its availability into the request’s, so a chain of eight services at 99.95 % each is up only 99.60 % of the time.
Checkpoint
Check: What a network hop costs
Boundaries follow business capabilities, and every table has one owner
Where you draw the lines matters more than how many there are. Lewis and Fowler recommend organising services, and the teams that own them, around business capabilities: ordering, payments, delivery, each owned from its interface to its database by one cross-functional team. The alternative that tends to happen without a plan is a split by technical layer or by noun, which gives you an order service, a customer service and a price service that must all talk to each other for any request to finish. Melvin Conway’s paper How Do Committees Invent? explains why the organisation matters too: organisations that design systems are constrained to produce designs that copy their own communication structures, which became known as Conway's law: Melvin Conway's observation that organisations which design systems produce designs that copy their own communication structures, so teams split by layer build systems split by layer.. Teams split by layer produce systems split by layer.
The model below places one food-delivery order four ways: in a monolith, in a modular monolith, in six services split by business capability, and in seven services split by noun. It counts what the request waits on:
"""Placing a food-delivery order, in four structures of the same application.
Each structure lists the calls the "place order" request makes. A sync call waits on the network; an event is
published after the order is saved and handled later. Each deployable is assumed to be up 99.95 % of the time,
independently, so the request's availability is the product over the deployables it waits on."""
AVAILABILITY = 0.9995
DESIGNS = {
"monolith": {
"deployables": ["app"],
"databases": 1,
"calls": [], # every step is a function call inside the app
"writes": ["app"],
},
"modular monolith": {
"deployables": ["app (accounts, catalogue, ordering, payments, delivery, notifications)"],
"databases": 1, # one database, one schema per module, each module touches only its own tables
"calls": [],
"writes": ["app"],
},
"6 services by capability": {
"deployables": ["ordering", "catalogue", "payments", "accounts", "delivery", "notifications"],
"databases": 6,
"calls": [
("ordering", "catalogue", "sync", "price the cart"),
("ordering", "payments", "sync", "authorise the payment"),
("ordering", "delivery", "event", "OrderPlaced: find a courier"),
("ordering", "notifications", "event", "OrderPlaced: tell customer and restaurant"),
],
"writes": ["ordering", "payments"], # the address comes from ordering's own copy, kept by accounts' events
},
"7 services by noun": {
"deployables": ["order", "customer", "restaurant", "menu", "price", "payment", "courier"],
"databases": 7,
"calls": [
("order", "customer", "sync", "read the address"),
("order", "restaurant", "sync", "is it open?"),
("order", "menu", "sync", "are the items on the menu?"),
("menu", "price", "sync", "price each item"),
("order", "payment", "sync", "authorise the payment"),
("payment", "customer", "sync", "read the saved card"),
("order", "courier", "sync", "assign a courier"),
],
"writes": ["order", "payment", "courier"],
},
}
def waited_on(design):
"""The deployables the request waits for: the entry point and every target of a sync call."""
entry = design["deployables"][0]
return {entry} | {callee for _, callee, mode, _ in design["calls"] if mode == "sync"}
def depth(design, start):
"""How many deployables the longest chain of sync calls from `start` waits on in a row."""
nexts = [callee for caller, callee, mode, _ in design["calls"] if caller == start and mode == "sync"]
return 1 + max((depth(design, n) for n in nexts), default=0)
print(f"{'design':<26}{'deployables':>12}{'databases':>10}{'sync calls':>11}{'waits on':>9}{'in a row':>9}{'availability':>14} one transaction?")
for name, d in DESIGNS.items():
sync = [c for c in d["calls"] if c[2] == "sync"]
waits = waited_on(d)
up = AVAILABILITY ** len(waits)
one_tx = "yes" if len(d["writes"]) == 1 else f"no: {len(d['writes'])} databases written"
print(f"{name:<26}{len(d['deployables']):>12}{d['databases']:>10}{len(sync):>11}{len(waits):>9}{depth(d, d['deployables'][0]):>9}{up:>13.3%} {one_tx}")
for name in ("6 services by capability", "7 services by noun"):
print(f"\n{name}, place order:")
for caller, callee, mode, why in DESIGNS[name]["calls"]:
print(f" {caller:>8} -> {callee:<13} {mode:<5} {why}") Output
design deployables databases sync calls waits on in a row availability one transaction?
monolith 1 1 0 1 1 99.950% yes
modular monolith 1 1 0 1 1 99.950% yes
6 services by capability 6 6 2 3 2 99.850% no: 2 databases written
7 services by noun 7 7 7 7 3 99.651% no: 3 databases written
6 services by capability, place order:
ordering -> catalogue sync price the cart
ordering -> payments sync authorise the payment
ordering -> delivery event OrderPlaced: find a courier
ordering -> notifications event OrderPlaced: tell customer and restaurant
7 services by noun, place order:
order -> customer sync read the address
order -> restaurant sync is it open?
order -> menu sync are the items on the menu?
menu -> price sync price each item
order -> payment sync authorise the payment
payment -> customer sync read the saved card
order -> courier sync assign a courier
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 boundaries.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The capability split makes two synchronous calls and waits on three deployables. It needs the customer’s address, but it does not call for it: the ordering service keeps its own copy, updated by the events the accounts service publishes whenever an address changes. Courier assignment and notifications happen after the order is saved, from an event. The split by noun is the opposite. Step through its calls one at a time:
The drawing shows the whole scene; its 9 steps are listed below.
Every step as text
- The request arrives. Placing one order starts at the order service, which owns only orders. Everything else it needs belongs to six other services.
- The address. First it reads the delivery address from the customer service and waits for the answer.
- Is it open?. Then it asks the restaurant service whether the restaurant is open, and waits again.
- The menu. Next it asks the menu service whether the items are on the menu.
- Three in a row. The menu service cannot answer without the price service, so the order waits on menu, which waits on price, three services in a row.
- The payment. The order service asks the payment service to authorise the payment.
- The card. The payment service reads the saved card from the customer service before it answers. That is a second chain of three, order, payment and customer.
- The courier. Even the courier is assigned synchronously, so the order waits for a courier before it can answer.
- What it adds up to. Seven synchronous calls, seven services waited on, chains three deep, and three databases written, by order, payment and courier. The request is up 99.651 % of the time, and no single transaction covers its writes.
The split by noun makes seven synchronous calls, waits on seven deployables, three of them in a row, and writes three databases, so no single transaction can keep them consistent. It has every cost of microservices and none of the independence: the order service cannot change anything without the customer, menu, price and payment services.
The rule that keeps boundaries honest is data ownership: every table has exactly one owner, and only the owner’s code writes it. Other parts ask the owner through its interface or keep a read-only copy fed by its events. Copies are fine; shared writes are not. Several services that read and write one database are coupled as tightly as a monolith, without its benefits: a schema change needs every team at once, a slow query from one service stalls the others, and none of them can be deployed or scaled on its own terms. The same rule makes a modular monolith ready for a later split, because a module that already owns its schema can take it to its own database.
Checkpoint
Check: Boundaries and data ownership
Five signals justify a split, and three popular reasons do not
Each of these is a reason to move one part out of the modular monolith. None of them is a reason to move all of them.
- Team autonomy. A team that must release several times a day keeps waiting for a shared deploy slot, or keeps being rolled back with everyone else. Independent deployment is what services are for.
- A very different load. One part needs ten times the machines of the rest at peak, or a different kind of machine, such as image resizing that burns CPU in bursts after uploads. It becomes a worker service behind a queue, scaled on its own.
- Failure isolation. A part that crashes, leaks memory or runs untrusted work must not take the rest down with it; or one part needs a stricter objective than the others and its own deployment discipline.
- A compliance boundary. Card data, health records or data that must stay in one country brings audits and change controls. Keeping that data and the code that touches it in one small service keeps the rules off everything else.
- A different technology. A model served from Python next to an application written in Java is easier as its own service than as an embedded runtime.
Payment data stored in India
A Reserve Bank of India notification requires every provider of a payment system to keep all of that system’s data, end-to-end transaction records included, on systems in India only (RBI notification). A payments service with its own database in an Indian region keeps that requirement to one small part, while the rest of the application can run wherever its users are.
Three reasons come up often and are not signals. “To scale”: a stateless monolith already scales out by running more copies behind a load balancer, as the lesson on scaling up, out and statelessness shows; what services add is scaling one part differently from the rest. “To clean up the code”: module boundaries do that inside one deployable, at a fraction of the price. “We will need it later”: Fowler’s MonolithFirst observes that nearly every microservice success he has heard of began as a monolith that outgrew itself and was then split, while nearly every system designed as microservices from the start ran into serious trouble; one reason he gives is that even experienced architects find good boundaries hard to draw on day one.
When a signal does appear, split one module at a time, starting with its data: the module’s schema moves to its own database first, its callers switch to its interface, and only then does its code move to its own deployable. Step through the order of the moves:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Before the move. Inside the modular monolith the module already owns its tables. Only its own code reads and writes them, so the seam exists before anything moves.
- The data moves first. The module's schema moves to a database of its own. The application is still one deployable, so a mistake here is undone without touching the network.
- The callers switch. Every other module that needed the data now calls the module's public functions. Nothing reads its tables directly any more.
- The code moves last. Only then does the module's code move to a deployable of its own, and its public functions become calls over the network. Every earlier step already works without it.
The last lesson of this module shows how to move a live system piece by piece, with a way back at every step.
Checkpoint
Check: Signals for a split
Four questions choose the structure
Four questions about your organisation decide most of the choice: how many teams wait on each other, whether the product has settled, whether one part behaves very differently, and whether some data is under its own rules. Give your answers in the table: each answer rules a structure out or adds a caveat, and the cells carry the numbers of the two models above:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Structure | Cost to build and run | Teams release on their own | Contains a crash or an audit | One transaction across parts | Latency and failed minutes |
|---|---|---|---|---|---|
| Monolith Deploys as: one unit with no boundaries inside | Good one pipeline, one deploy, one on-call rota | Poor every change waits for the shared deploy | Poor a leak in one corner restarts all of it; audits cover all code | Good one database transaction covers every write | Good function calls only: 22 failed minutes in 30 days |
| Modular monolith Deploys as: one unit of modules that own their tables | Good one deployable; the boundaries cost discipline, not machines | Mixed still one deploy, but clean seams for a later split | Poor still one process, so a leak still takes everything down | Good one database, a schema per module | Good no network hops between modules |
| Modular monolith and one service Deploys as: one unit plus the one part that needs to be apart | Mixed one more pipeline, dashboard and on-call rota | Mixed the part apart ships alone; the rest still share a deploy | Good the part apart fails, scales and is audited on its own | Mixed writes that cross into the part need events or retries | Mixed a hop and one more deployable on the paths that call it |
| Microservices Deploys as: one unit per business capability | Poor a pipeline, dashboards and on-call for every service | Good each team deploys its own service | Good one service fails, if its callers cope | Poor no shared transaction: events, retries, compensation | Poor 8 in a chain: 99.60 % up, 172 failed minutes in 30 days |
How the answers decide
- “Has the product settled, so that its business capabilities are clear?” Not yet: we are still finding the product: rules out Microservices. Good boundaries are hard to see before the product has settled, and services would freeze today's guesses into network contracts.
- “How many teams change the code, and do they keep waiting for each other's releases?” One or two teams: rules out Microservices. One or two teams gain nothing from independent deployment and would pay a pipeline, dashboards and on-call for every service.
- “How many teams change the code, and do they keep waiting for each other's releases?” Many teams, blocked by a shared deploy: rules out Monolith. Without boundaries inside, no part can ever leave, so the shared deploy stays the bottleneck.
- “How many teams change the code, and do they keep waiting for each other's releases?” Many teams, blocked by a shared deploy: adds a caveat to Modular monolith. The seams are there, but releases still wait for one deploy; move the modules whose teams wait most, one at a time.
- “Does one part scale, fail or run very differently from the rest?” Yes, one part: adds a caveat to Monolith and Modular monolith. Scaling or protecting the whole deployable for one part wastes machines or leaves the rest exposed to it.
- “Is some data under separate legal or audit rules, such as card data or data that must stay in one country?” Yes, some of it: adds a caveat to Monolith and Modular monolith. Audits and change controls then apply to the whole codebase, where one small service would keep them to the code that touches the data.
- “Does one part scale, fail or run very differently from the rest?” No and “Is some data under separate legal or audit rules, such as card data or data that must stay in one country?” No: adds a caveat to Modular monolith and one service. No part needs to scale, fail or be audited on its own, so the extra pipeline buys nothing yet.
In short: a modular monolith while the product is still moving or the teams are few; the same monolith with one part moved out when that part scales, fails or is audited differently; and services, one module at a time, when many teams keep waiting for each other’s releases.
Checkpoint
Check: Choosing a structure
Practise: find the calls and table reads that cross a boundary
Write the check that a modular monolith runs on its own code, and run the sample tests in your browser:
Exercise · Easy · Python
Find the calls and table reads that cross module boundaries
A modular monolith keeps one deployable but lets each module call another module only through the functions that module makes public, and lets each module read and write only the tables it owns. Finish violations in boundary_check.py so that it reports every place where the code breaks those two rules.
ownersmaps each table to the module that owns it, such as{"orders": "ordering"}.publicmaps each module to the set of function names other modules may call. A module that is missing frompublichas no public functions.callslists the calls the code makes, as(caller_module, callee_module, function)tuples.readslists the tables each module's code touches, as(module, table)tuples.
Return a sorted list of strings, each violation once:
"call ordering -> payments.charge_card"for a call from one module into a function of another module that is not public;"table delivery -> orders"for a module that touches a table another module owns.
A module may always call its own functions and use its own tables. A table that is missing from owners belongs to no module yet: report it as "table <module> -> <table> (no owner)".
Starter code · boundary_check.py
"""Report the calls and table reads that break the boundaries of a modular monolith."""
def violations(owners, public, calls, reads):
"""owners: {table: module}; public: {module: set of public function names};
calls: [(caller, callee, function)]; reads: [(module, table)]. Return the sorted violations."""
# Replace this line with your code.
return [] The sample tests · test_boundary_check.py
from boundary_check import violations
OWNERS = {"orders": "ordering", "payments": "payments", "couriers": "delivery"}
PUBLIC = {"payments": {"authorise"}, "ordering": {"place_order", "order_status"}}
def test_public_calls_and_own_tables_are_fine():
"""calling a public function and using your own tables breaks nothing"""
calls = [("ordering", "payments", "authorise"), ("delivery", "ordering", "order_status")]
reads = [("ordering", "orders"), ("delivery", "couriers")]
assert violations(OWNERS, PUBLIC, calls, reads) == []
def test_private_function_of_another_module():
"""a call into a function another module did not make public is a violation"""
calls = [("ordering", "payments", "charge_card")]
assert violations(OWNERS, PUBLIC, calls, []) == ["call ordering -> payments.charge_card"]
def test_own_private_function_is_fine():
"""a module may call its own functions, public or not"""
calls = [("payments", "payments", "charge_card")]
assert violations(OWNERS, PUBLIC, calls, []) == []
def test_module_without_public_functions():
"""a module missing from `public` has no public functions"""
calls = [("ordering", "delivery", "assign_courier")]
assert violations(OWNERS, PUBLIC, calls, []) == ["call ordering -> delivery.assign_courier"]
def test_reading_another_modules_table():
"""touching a table that another module owns is a violation"""
reads = [("delivery", "orders"), ("ordering", "orders")]
assert violations(OWNERS, PUBLIC, [], reads) == ["table delivery -> orders"]
def test_table_without_owner():
"""a table nobody owns yet is reported as having no owner"""
reads = [("ordering", "coupons")]
assert violations(OWNERS, PUBLIC, [], reads) == ["table ordering -> coupons (no owner)"]
def test_each_violation_once_and_sorted():
"""repeated violations appear once, and the list is sorted"""
calls = [("ordering", "payments", "refund"), ("ordering", "payments", "refund")]
reads = [("payments", "orders"), ("delivery", "payments"), ("payments", "orders")]
assert violations(OWNERS, PUBLIC, calls, reads) == [
"call ordering -> payments.refund",
"table delivery -> payments",
"table payments -> orders",
] A hint
Collect the violations in a set, so that a call made twice is reported once, and return sorted(...) of it at the end. For a call, skip it when the caller and the callee are the same module, and otherwise look the function up in public.get(callee, set()). For a table, compare owners.get(table) with the module that touched it.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then test yourself on the five ideas of this lesson, starting with the one you know least:
Practice that adapts to you
Practise this lesson
12 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Design practice: structures to recommend
A design round asks for exactly these decisions about structure. Decide each brief first, then test your choice against the table above, whose four questions are the ones an interviewer tends to ask next:
Checkpoint
Your first decision
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A 40-engineer company runs one monolith. Deploys wait days for a slot, and one module's memory leak restarts the whole application every few hours. The CTO proposes splitting it into 30 microservices this quarter. What do you recommend, and which parts would you split first?
Interview questions
What is a modular monolith, and how is it different from microservices?
A modular monolith is one application, deployed as one unit, whose code is divided into modules along business capabilities. Each module calls the others only through the functions they make public and reads and writes only its own tables, so the boundaries are real even though everything runs in one process. Microservices take the same boundaries further: each part becomes its own deployable service with its own database, and the parts talk over the network. Both give teams clear ownership. Only microservices give independent deployment and scaling, and they pay for it with network latency and partial failures, data spread across databases with no shared transaction, and a pipeline, monitoring and on-call rota for every service. A good modular monolith is also the easiest system to split later, because its seams already exist.
Key takeaways
- A monolith is one deployable; a modular monolith is one deployable with enforced module boundaries and owned tables; microservices are independently deployed services, each with its own data, talking over the network.
- Every synchronous hop adds latency and multiplies availability: eight services in a chain at 99.95 % each are up 99.60 % of the time, eight times the failed minutes of one deployable.
- Draw boundaries around business capabilities and give every table exactly one owner; copies fed by events are fine, shared writes are not.
- Split one part at a time, data first, and only for a signal: a team that must release on its own, a very different load, failure isolation, a compliance boundary or a different technology.
- Start with a modular monolith; its boundaries make a later split possible, and its single deployable keeps changes cheap while the product is still moving.
Terms in this lesson
- business capability
- Something the business does, such as ordering, payments or delivery, owned from its interface to its data by one team: the usual line along which modules and services are drawn.
- Conway's law
- Melvin Conway's observation that organisations which design systems produce designs that copy their own communication structures, so teams split by layer build systems split by layer.
- modular monolith
- One application deployed as one unit, whose modules call each other only through public interfaces and each read and write only their own tables.
References
- Microservices (James Lewis and Martin Fowler) (martinfowler.com)
- MonolithFirst (martinfowler.com)
- MicroservicePremium (martinfowler.com)
- Hexagonal architecture (ports and adapters), HaT Technical Report 2005.02 (Alistair Cockburn)
- How Do Committees Invent? (Melvin E. Conway, Datamation) (Melvin E. Conway)
- Deconstructing the Monolith: Designing Software that Maximizes Developer Productivity (Shopify Engineering)
- Availability and Beyond: Availability with dependencies (Amazon Web Services)
- Storage of Payment System Data (RBI notification to payment system operators) (Reserve Bank of India)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress