Monolith, Modular Monolith and Microservices
The main ways to divide a system into deployable parts, what each costs and gives, when microservices pay off, and why a modular monolith is often the better start.
Every system that grows beyond a script faces the question of how to
cut it into parts. Some cuts are inside the code: modules, packages,
layers. Others decide what
is built, released and run separately. This page is about the second
kind, which is among the hardest decisions to reverse. The running
example is Kestrel Energy's billing system, KBS (Kestrel Billing
System): one application on the servers bill-app-01 and bill-app-02
in Kestrel's colocation data center, which calculates tariffs,
produces invoices, collects payments and keeps customer accounts.
Its HTTP interface is the billing-api service.
Deployment units
A deployment unit is the piece of software that is built, versioned and released as a whole: one application package, one container image or one function. Everything inside a deployment unit ships together, so a change to any part of it means releasing all of it. Counting deployment units is the clearest way to tell the three styles on this page apart:
Deployment units matter to operations as much as to developers. They decide what a release contains, what can be rolled back on its own, what can be scaled separately, and how many things the on-call engineer has to understand at 3 a.m. A web shop with one application has one thing to deploy, monitor and patch; the same shop as 30 services has 30.
The monolith
A monolith is a system whose functionality is built, tested and deployed as a single unit, usually one codebase running as one process (possibly with several identical copies behind a load balancer) and one database. The word is neutral: it describes how the system is deployed, not whether its code is any good. (The monolithic kernel of an operating system is the same idea in a different place: one large program instead of many cooperating ones.)
Why monoliths work well
- Simple to run. One build, one deployment, one set of logs. KBS is released with one change and runs on two servers that Kestrel's operations team knows well.
- Fast, reliable calls. A call from invoicing code to tariff code is a function call: nanoseconds, no timeout, no network that can fail halfway.
- Real transactions. Creating an invoice and recording the payment schedule can happen in one database transaction: either both happen or neither does.
- Easy refactoring. Moving code between parts is an editor operation, checked by the compiler or the tests, not a negotiation between teams.
When a monolith hurts
The trouble starts when a monolith has no internal structure. KBS grew piece by piece since the 2010s: a quick fix in the payment code reads tariff tables directly, the invoice code calls into customer account internals, and nobody can say any more which part owns which table. Such a system is called a big ball of mud (see Architecture Anti-Patterns). The symptoms:
- A small change in one area breaks something in another, so every release needs a full regression test and feels risky.
- Many developers work in the same code, so changes wait for each other and merge conflicts are common.
- The whole application scales as one: if only the PDF generation needs more CPU in January, you still run more copies of everything.
- One memory leak or one runaway query can take down all functions at once.
Note that most of these are caused by missing boundaries, not by having one deployment unit. That is the point of the next style.
The modular monolith
A modular monolith is a system that is deployed as one unit but is divided internally into modules with strictly enforced boundaries: each module owns its data and logic, and other modules may use it only through its published interface. Shopify, which runs one of the largest Ruby on Rails monoliths, described its own move in 2019 (Kirsten Westeinde, "Deconstructing the Monolith"): "A modular monolith is a system where all of the code powers a single application and there are strictly enforced boundaries between different domains."
The key word is enforced. Folders named after modules do
not stop anyone from importing another module's internals. A check
in the build pipeline does. Joost Bakker, who owns KBS, splits it
into four modules (Tariffs, Invoicing, Payments and Customer
accounts), each with a small api part that other
modules may use. A rule table states which module may depend on
which, and a pipeline step compares it with the imports it finds in
the code:
# KBS as a modular monolith: every module exposes a small public API
# (<module>.api) and keeps everything else internal.
ALLOWED = { # which other modules each module may call
"accounts": set(),
"tariffs": set(),
"invoicing": {"tariffs", "accounts"},
"payments": {"invoicing", "accounts"},
}
# (file, imported name) pairs, as a build step would collect them from the code
imports = [
("invoicing/run.py", "tariffs.api"),
("invoicing/run.py", "accounts.api"),
("payments/collect.py", "invoicing.api"),
("payments/collect.py", "invoicing.internal.pdf"),
("tariffs/rules.py", "payments.api"),
("accounts/profile.py", "accounts.internal.store"),
]
def check(path, target):
source = path.split("/")[0]
module, _, rest = target.partition(".")
if module == source:
return None # inside one module anything goes
if module not in ALLOWED[source]:
return f"{source} may not depend on {module}"
if rest != "api":
return f"internal to {module}, use {module}.api"
return None
failures = 0
for path, target in imports:
problem = check(path, target)
if problem:
failures += 1
print(f"FAIL {path} imports {target}: {problem}")
print(f"{len(imports) - failures} of {len(imports)} imports respect the boundaries")
FAIL payments/collect.py imports invoicing.internal.pdf: internal to invoicing, use invoicing.api FAIL tariffs/rules.py imports payments.api: tariffs may not depend on payments 4 of 6 imports respect the boundaries
Two violations, two kinds: Payments reaches into Invoicing's internal PDF code instead of its public API, and Tariffs depends on Payments, which the rule table does not allow (tariff rules should not know how money is collected). The real check reads the imports from the source files; checks like this are called fitness functions, and Evolving Architecture builds one. Inside a module (the last import) anything goes.
Each module also owns its tables, ideally in its own database schema, and no other module queries them directly. That keeps the option open to move a module out later: its boundary is already drawn and tested every day. The modules usually follow the business capabilities or bounded contexts of the domain (see Domain-Driven Design).
Microservices
James Lewis and Martin Fowler's article "Microservices" (March 2014) gave the style its common definition: "an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery." In short, microservices is an architectural style in which one application is made of many small services, each with its own deployment unit and its own data, that work together over the network and can be changed and released independently.
The article lists nine characteristics: componentization via services; organized around business capabilities; products not projects; smart endpoints and dumb pipes; decentralized governance; decentralized data management; infrastructure automation; design for failure; and evolutionary design. Two of them deserve emphasis, because they are the ones teams most often skip:
- Decentralized data. Each service owns its data and no other service reads its tables. Services that share one database are not independent: a schema change or a connection leak in one affects all of them (see Data in Distributed Systems).
- Design for failure. Every call to another service can be slow or fail, so every service needs timeouts, sensible retries and a plan for when a dependency is down (see Service Communication).
"Micro" refers to scope and independence, not to lines of code. A service is small enough when one team can understand, change and run it on its own, and when it can be deployed without coordinating with other teams. Services that must always be released together are one deployment unit in disguise, with a network in the middle: the distributed monolith (see Architecture Anti-Patterns).
Where to draw service boundaries
A service boundary is the line around what one service owns, its responsibilities and data, and therefore what other services can reach only through its API. Good boundaries follow business capabilities: things the business does, such as "calculate tariffs" or "collect payments", which change for their own reasons and have their own experts. Bad boundaries follow technical layers:
| Split by technical layer (avoid) | Split by business capability |
|---|---|
| UI service, logic service, database service | Tariffs, Invoicing, Payments, Customer accounts |
| A new tariff type changes all three services and needs three coordinated releases | A new tariff type changes the Tariffs service only |
| Every user request crosses every service | Most requests stay inside one service |
A good test for a proposed boundary: list the last ten changes to the system and count how many would have crossed it. If most of them would, the boundary is in the wrong place, and moving it after the split means moving code and data between services, which is far harder than inside one codebase.
What microservices cost
Fowler calls the extra cost the microservice premium: the additional effort, operational work and complexity a microservice architecture needs compared with a monolith, which only pays off when a system is too complex to manage as a monolith. His 2015 advice is blunt: "don't even consider microservices unless you have a system that's too complex to manage as a monolith", and "The majority of software systems should be built as a single monolithic application. Do pay attention to good modularity within that monolith". The premium comes in several forms:
- Operations. Every service needs its own pipeline, monitoring, alerts, logs, dashboards, security patches and an owner on call. Fowler's "microservice prerequisites" (2014) are three: rapid provisioning, basic monitoring and rapid application deployment. They imply a DevOps way of working, in which the team that builds a service also runs it.
- The network. Calls that were function calls now cross the network and can time out, fail halfway or arrive twice.
- Distributed data. A transaction across two services is no longer one database transaction. Keeping data consistent needs patterns such as the outbox and sagas (see Event-Driven Architecture).
- Debugging. One user request may touch eight services. Finding out why it was slow needs distributed tracing and correlated logs.
- Contracts. Every API between services must be versioned and kept compatible, because the services are released at different times (see API Design Between Services).
The network cost is easy to underestimate. Suppose every service is available 99.9% of the time and answers within its p99 latency in 99% of calls. A request that needs several of them in one synchronous chain needs all of them to be up, and is slow if any of them is slow (assuming the services fail independently; the general math is on components in series and in parallel):
# Every service is available 99.9% of the time and answers within 300 ms
# in 99% of calls (its p99). A user request that needs n services in one
# synchronous chain needs ALL of them to be up, and is slow if ANY is slow.
MINUTES_PER_30_DAYS = 30 * 24 * 60
for n in (1, 3, 5, 8):
available = 0.999**n
down = (1 - available) * MINUTES_PER_30_DAYS
slow = 1 - 0.99**n
print(f"{n} service(s): available {available:.2%} "
f"({down:5.1f} min down per 30 days), "
f"{slow:5.1%} of requests hit a slow call")
1 service(s): available 99.90% ( 43.2 min down per 30 days), 1.0% of requests hit a slow call 3 service(s): available 99.70% (129.5 min down per 30 days), 3.0% of requests hit a slow call 5 service(s): available 99.50% (215.6 min down per 30 days), 4.9% of requests hit a slow call 8 service(s): available 99.20% (344.4 min down per 30 days), 7.7% of requests hit a slow call
Eight services in a chain turn a 99.9% service into a 99.2% one, close to six hours of failed requests a month, and make a slow response almost eight times as likely. That is why microservice designs avoid long synchronous chains and use asynchronous messages where the caller does not need an immediate answer.
What the premium buys, when the conditions are right:
- Independent deployment. A team releases its service several times a day without waiting for, or breaking, anyone else.
- Independent scaling. Only the part under load gets more instances.
- Fault isolation. A crash in one service does not take the others down, if the callers handle its failure well.
- Team autonomy. Many teams can work in parallel, each owning a few services end to end.
Conway's law
In April 1968 Melvin Conway wrote in Datamation that "organizations which design systems (in the broad sense used here) are constrained to produce designs which are copies of the communication structures of these organizations." Fred Brooks later named this Conway's law: the structure of a system tends to mirror the communication structure of the organization that designs it. (The often-quoted short version, "Any organization that designs a system … will produce a design whose structure is a copy of the organization's communication structure", is Conway's own later, informal restatement.)
The reason is practical. Designing an interface between two parts takes conversations between the people who build them. Where people talk easily, inside a team, parts become intertwined; where they rarely talk, between departments, a clear interface appears. The number of people who might need to talk grows fast with team size, which is why organizations split into teams in the first place:
# Pairs of people who may need to talk to each other: n * (n - 1) / 2
def pairs(n):
return n * (n - 1) // 2
print("one team of 5 developers: ", pairs(5), "pairs")
print("one team of 15 developers: ", pairs(15), "pairs")
# Three teams of 5: everyone talks inside their team,
# and each team has one contact person for each other team.
inside = 3 * pairs(5)
between = pairs(3)
print("three teams of 5: ", inside + between, "pairs",
f"({inside} inside teams, {between} between teams)")
one team of 5 developers: 10 pairs one team of 15 developers: 105 pairs three teams of 5: 33 pairs (30 inside teams, 3 between teams)
Kestrel shows the law at work. The portal is built by the portal Scrum team, KBS by the billing team around Joost Bakker and Femke Jansen, and the two systems meet exactly where the two teams meet: a handful of agreed tables on the shared database server db01, and a few API calls.
Turned around, the law becomes a design tool. The inverse Conway maneuver is deliberately shaping teams and their communication to match the architecture you want, so that the system grows toward it. The term comes from Jonny LeRoy and Matt Simons (2010). If you want Tariffs and Payments to be independent, give them to different teams with a clear contract between them; if you want them tightly integrated, keep them in one team. The book Team Topologies (Matthew Skelton and Manuel Pais) builds on this with team types such as stream-aligned teams, which own a flow of business change end to end, and platform teams, which provide shared infrastructure as a service, and with the principle "Respect Cognitive Limits": a team should own no more software than it can understand.
Choosing a style
| Monolith | Modular monolith | Microservices | |
|---|---|---|---|
| Deployment units | One | One | One per service |
| Internal boundaries | Often weak | Enforced in the build | Enforced by the network |
| Calls between parts | Function calls | Function calls through module APIs | Network calls and messages |
| Data | One shared schema | One database, a schema per module | A database per service |
| Transactions | ACID | ACID | Eventual consistency, sagas |
| Scaling | Everything together | Everything together | Per service |
| Operations effort | Low | Low | High (per service) |
| Fits | Small systems, small teams | Most systems and most teams | Large systems, many teams, parts with very different needs |
Fowler's "MonolithFirst" (2015) sums up what he saw in practice: "Almost all the successful microservice stories have started with a monolith that got too big and was broken up", while almost all the systems he had heard of that were built as microservices from scratch "ended up in serious trouble". The reason is boundaries: early in a project nobody knows the domain well enough to draw them right, and wrong boundaries between services are expensive to move.
Good reasons to give a part its own deployment unit are concrete and checkable:
- A different load profile. Kestrel's meter-api receives a reading from 400,000 smart meters every 15 minutes, 444 readings per second on average with peaks at :00, :15, :30 and :45. KBS mostly works in a nightly batch at 02:30. Scaling them together would make no sense, so meter-api is rightly a separate service.
- A different release rhythm, such as a portal that ships daily next to billing logic that changes twice a year under audit.
- A different owning team (Conway's law), with its own on-call rotation.
- Different technology or security needs, such as a component that must run in a separate network zone.
For KBS itself, Joost and Ravi Kumar, the portal's tech lead, looked at three options. Splitting KBS into eight microservices failed the prerequisites: a team of five developers, no container platform in the colocation and no tracing, and Margriet de Wit, the CFO, asked what the extra servers, tooling and on-call shifts would buy. Leaving KBS as it was kept the risky releases. They chose to turn KBS into a modular monolith (ADR-0003, accepted on 21 September 2026), with the boundary check above in the pipeline, and moving single capabilities out only when one of the reasons above applies. How such a move is done step by step, without a big-bang rewrite, is on Evolving Architecture; how the decision is written down for the next team is on Documenting Architecture.