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:

Three ways to structure the same billing system. Left, a monolith: one box in which tariffs, invoices, payments, accounts and PDF code are tangled together, using one database: one deployment unit. Middle, a modular monolith: one box containing four separate modules, tariffs, invoicing, payments and accounts, which call each other only through a few allowed arrows, using one database: still one deployment unit. Right, microservices: four separate services, each with its own data, calling each other over the network: four deployment units. Monolith Modular monolith Microservices tariffs invoices PDF payments accounts one database 1 deployment unit tariffs invoicing payments accounts one database 1 deployment unit tariffs own data invoicing own data payments own data accounts own data dashed: calls over the network 4 deployment units
The same four business capabilities, cut three ways. The modular monolith and the microservices have the same boundaries; what differs is whether crossing one is a function call or a network call.

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.