Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

One food-delivery app as a monolith, as a modular monolith with owned schemas, and as services that each own a database.MonolithModular monolithServices by capabilityOne app: ordering, payments,delivery and the rest calleach other's code freelyOne database: every tableopen to all codeOne app, six modulesOne database, one schemaper moduleOrdering+ its databaseCatalogue+ its databasePayments+ its databaseEvent streamDelivery, notifications+ their databasesOrderingPaymentsany tablepublicfunctionsonlyown tables onlysync: pricesync: authoriseOrderPlacedlater

The same application in three structures

Text description of the diagram

The figure has three panels, one under the other.

  1. 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.
  2. 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.
  3. 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.

One application, three structures
Criterion MonolithModular monolithMicroservices
Deploys as One unitOne unitOne unit per service
Parts call each other Any function, freelyPublic functions onlyOver the network
Data One shared schemaOne database, a schema per moduleA database per service
One transaction across parts YesYesNo: events, retries and compensation
A crash or leak takes down EverythingEverythingOne service, if callers cope
Cost to run LowestLowA pipeline, dashboards and on-call per service
When to choose A small team still finding its product, for a whileMost systems, as the defaultMany 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

  1. Question 1 of 2 In a modular monolith, which code may read and write the tables of the payments module?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Only the payments module's own code

    A modular monolith keeps one deployable but enforces two rules: a module calls another only through its public functions, and only a module's own code reads or writes its tables. That is what makes a later split possible.

  2. Question 2 of 2 The payments module of a modular monolith leaks memory until the process restarts. What goes down while it restarts?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The whole application, because every module runs in the same process

    A crash or a leak takes down everything that shares the process, in a monolith and in a modular monolith alike. Failure isolation is one of the signals for moving a part into its own service.

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:

Eight modules in one process against a chain of services Python · latency_budget.py
"""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

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:

Interactive · stage by stage Availability multiplies down a chain of services Step through the chain as it grows from one service to eight. The line under the last box is what the whole request gets.
Availability multiplies down a chain of services, step 6 of 6. Showing Service 1, Service 2, Service 3, Service 4 (request up 99.80 %), Service 5, Service 6, Service 7, Service 8 (request up 99.60 %). In focus: Service 1, Service 2, Service 3, Service 4, Service 5, Service 6, Service 7, Service 8.Service 1up 99.95 % of the timeService 2Service 3Service 4request up 99.80 %Service 5Service 6Service 7Service 8request up 99.60 %

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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 %.
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. Question 1 of 2 A request waits on five services in a chain, each up 99.95 % of the time and failing independently. What share of the time is the request up? Answer in percent, to two decimals.

    Type a number.

    A hint

    The request is up only while all five are up at once.

    Show the answer: Question 1 of 2

    Answer: 99.75 % (anything from 99.74 to 99.76 counts)

    The availabilities multiply: 0.9995⁵ ≈ 0.99750, so the request is up 99.75 % of the time, against 99.95 % for one deployable. That is about 108 failed minutes in 30 days instead of 22.

  2. Question 2 of 2 "Place order" calls six services in a row, and the last one sends the confirmation email. Which change shortens the chain without losing anything the customer needs?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Send the email a moment later from a queue, after the order is saved

    Call only what the request cannot do without, and move everything else after the commit. A confirmation email can be sent a second later from a queue, so it costs the order neither latency nor availability.

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 an order in four structures Python · boundaries.py
"""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

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:

Interactive · step by step One order placed through seven services split by noun Step through the calls that placing one order makes. Count the services the order waits on, and the longest chain of them.
One order placed through seven services split by noun, step 9 of 9. Showing Courier, Order (waits on: 7 services), Payment, Customer, Restaurant, Menu, Price.CourierOrderwaits on: 7 servicesPaymentCustomerRestaurantMenuPrice

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. The request arrives. Placing one order starts at the order service, which owns only orders. Everything else it needs belongs to six other services.
  2. The address. First it reads the delivery address from the customer service and waits for the answer.
  3. Is it open?. Then it asks the restaurant service whether the restaurant is open, and waits again.
  4. The menu. Next it asks the menu service whether the items are on the menu.
  5. 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.
  6. The payment. The order service asks the payment service to authorise the payment.
  7. 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.
  8. The courier. Even the courier is assigned synchronously, so the order waits for a courier before it can answer.
  9. 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

  1. Question 1 of 2 In the split by noun, placing one order waits on seven services. Which chains of three services, each waiting on the next, does it hold? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • Order, then payment, then customer
    • Order, then menu, then price

    The noun split makes seven synchronous calls and waits on seven services, and two of its chains are three deep: order, menu and price, and order, payment and customer. The capability split's longest chain is two.

  2. Question 2 of 2 The ordering service needs the customer's address for every order. How does the split by capability get it without a synchronous call to the accounts service?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It keeps a read-only copy, updated by the events the accounts service publishes when an address changes

    Every table has exactly one owner, and only the owner writes it. Others ask the owner through its interface or keep a read-only copy fed by its events. Copies are fine; shared writes are 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:

Interactive · stage by stage One module leaves the monolith, data first Step through the order of the moves. Each one is small, and each can be undone before the next begins.
One module leaves the monolith, data first, step 4 of 4. Showing Owns its tables, Its own database, Callers use its interface, Its own deployable. In focus: Its own deployable.Owns its tablesone schema, shared databaseIts own databasethe same single deployableCallers use its interfacenobody reads its tablesIts own deployablecalls cross the network

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. The callers switch. Every other module that needed the data now calls the module's public functions. Nothing reads its tables directly any more.
  4. 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

  1. Question 1 of 2 Which of these are NOT good reasons to move a part out of a modular monolith? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • "We will need it later"
    • "To scale": the application must handle more traffic
    • "To clean up the code"

    Each signal buys something a module cannot give: independent releases, a very different load, failure isolation, a compliance boundary or a different technology. Scaling as such, cleaner code and a guess about the future do not.

  2. Question 2 of 2 A module has to leave the monolith. What moves first?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Its schema, to a database of its own, while the code is still one deployable

    Split one module at a time, starting with its data: the schema moves to its own database first, the callers switch to its interface, and only then does its code move to its own deployable.

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:

Interactive · answer and compare Which structure fits your organisation? Answer four questions about your teams, your product and your data. Every option your answers rule out says why.
How many teams change the code, and do they keep waiting for each other's releases?
Has the product settled, so that its business capabilities are clear?
Does one part scale, fail or run very differently from the rest?
Is some data under separate legal or audit rules, such as card data or data that must stay in one country?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four structures for one application against the five things that decide between them
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

  1. One question Ten engineers in two teams, a product that has settled, and one part, video transcoding, that needs fifty times the CPU of the rest in bursts. Which structure does the table point to?

    Choose one answer.

    Show the answer: One question

    Answer: A modular monolith, with transcoding moved out into a worker service behind a queue

    A very different load is a signal for moving that one part out. A worker pool behind a queue scales on its own and absorbs the bursts, while the rest stays one simple deployable.

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.

  • owners maps each table to the module that owns it, such as {"orders": "ordering"}.
  • public maps each module to the set of function names other modules may call. A module that is missing from public has no public functions.
  • calls lists the calls the code makes, as (caller_module, callee_module, function) tuples.
  • reads lists 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.

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.

  1. Question 1 of 12 What does Fowler's "microservice premium" name?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: The cost of running a distributed system, which slows a team down until its system is complex enough to need services

    Fowler uses the term for the total cost of running a distributed system. It slows a team down until the system is complex enough to need what services give, which is why most systems should start as one deployable.

  2. Question 2 of 12 An order and its payment must be saved together or not at all. In which structures can one database transaction cover both writes? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 12

    Answer:

    • A modular monolith
    • A monolith

    Both single-deployable structures keep one database, so a transaction can span modules. Services each own their data, so a write that spans two of them has no shared transaction and needs another way to stay consistent.

  3. Question 3 of 12 Why must a service's interface be coarser than the function calls it replaces?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: A remote call costs far more than an in-process call and can fail, so each call should carry more work

    A function call inside one process takes nanoseconds and either returns or throws. A remote call crosses the network and can time out, fail halfway or arrive twice, so a service design makes fewer, larger calls.

  4. Question 4 of 12 A request waits on four deployables, each up 99.95 % of the time and failing independently. About how many minutes in 30 days does it fail?

    Type a number.

    A hint

    30 days have 43,200 minutes, and the request is up only while all four are.

    Show the answer: Question 4 of 12

    Answer: 86 minutes (anything from 85 to 87 counts)

    The request is up 0.9995⁴ ≈ 99.80 % of the time, so it fails 0.2 % of 43,200 minutes, about 86 minutes, against 22 for one deployable. The model printed the same 86 for four services in a chain.

  5. Question 5 of 12 Conway's paper says that organisations which design systems copy their own communication structures. What does that predict for teams split by technical layer?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: They build systems split by layer, whose parts must all talk to each other for any request

    Teams split by layer produce systems split by layer. Organising services and the teams that own them around business capabilities is how the seams fall where requests can be served by one or two parts.

  6. Question 6 of 12 Two services both need to update a customer's loyalty points. Which design keeps the boundaries honest?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: One service owns the points table, and the other asks it to change the points through its interface

    Give every table exactly one owner. 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.

  7. Question 7 of 12 Which of these are good reasons to move a part of a modular monolith into its own service? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 7 of 12

    Answer:

    • It handles data that must stay under separate legal or audit rules
    • It needs ten times the instances of everything else at peak
    • Its team must release several times a day without waiting for the others

    Independent release cadence, a very different scaling profile and a compliance boundary each buy something a module cannot give. Cleaner code comes from module boundaries inside the monolith, and fashion is not a reason: every service adds network calls, its own data and its own operations.

  8. Question 8 of 12 A retailer's modular monolith of 60 engineers works well, but the part that stores card numbers forces security audits and strict change controls on the whole codebase. What do you recommend?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: Move the card-handling module into its own service with its own data, and keep the rest as one modular monolith

    This is a compliance boundary, one of the signals that justify a split: taking the card data and the code that touches it out of the shared deployable shrinks what the strict rules apply to. Nothing else in the facts calls for more services, so the rest stays together.

  9. Question 9 of 12 A photo-sharing app runs as one deployable. Image resizing uses 90 % of the CPU in short bursts after uploads, while the rest of the app needs little. Ten engineers work on it. What fits best?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: Move resizing into a separate worker service behind a queue, and keep the app as one deployable

    A very different scaling profile is a signal for one split, not for many. A worker pool behind a queue scales on its own and absorbs bursts, while the app stays simple. Scaling everything for one hungry feature wastes capacity, and splitting by feature adds hops with no benefit.

  10. Question 10 of 12 A team of four is building a new appointment-booking product. Nobody knows yet which features customers will pay for, and traffic is a few requests a second. Which structure should they start with?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: A modular monolith, with one module per business capability

    Four people gain nothing from independent deployment, and the boundaries are not known yet, so services would freeze guesses into network contracts. One deployable keeps changes cheap; modules with their own tables keep a later split possible. Skipping the boundaries entirely is how a monolith becomes hard to split.

  11. Question 11 of 12 Twelve teams, about 150 engineers, share one monolith. Releases wait days for a shared deploy slot, a bug in one team's code rolls back everyone's changes, and the modules already have clean interfaces. What is the next step?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: Extract the modules whose teams are blocked most into services, one at a time, each with its own data

    Teams blocked by a shared deploy are the signal services answer best: independent deployment lets each team ship without waiting for the others. Clean module interfaces make the extraction realistic, and doing it one module at a time keeps each step reversible. A big-bang rewrite has no way back.

  12. Question 12 of 12 Twenty-five engineers in four teams run a modular monolith. The product has settled, releases rarely wait, no part scales or fails differently, and no data is under rules of its own. Someone proposes services "to be ready". What does the table point to?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: Keep the modular monolith, and keep its boundaries clean

    Each signal for a split is missing here: no team waits on a shared deploy, no part behaves differently and no data has its own rules. The modular monolith stays, with its boundaries kept clean, so a part can still leave when a signal appears.

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

  1. Question 1 of 2 Design brief: three engineers start a tutoring marketplace, and nobody knows yet which features learners will pay for. What do you build first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A modular monolith, one module per business capability, each owning its tables

    A small team still finding its product gains nothing from independent deployment. Modules along business capabilities, each owning its tables, give the seams for later without the cost of services now.

  2. Question 2 of 2 Design brief: a food-delivery app runs worldwide as one modular monolith, and its payment data for India must be stored only in India. What do you change?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Move payments into its own service with its own database in an Indian region, and keep the rest together

    The Reserve Bank of India requires payment system data to be stored only in India. A payments service with its own database in an Indian region meets the rule, and the rest of the application can run wherever its users are.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    Three services read and write the same orders table. Every schema change needs three teams and a joint release, and a slow report query once stalled checkout. What is wrong with this design, and how do you untangle it without downtime?

    Model answer, Pro. First step and follow-ups

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.