DevelopmentAll levels

Monolith

A monolith is an application that is built, tested and deployed as one unit, whatever its size and however its code is organised inside. Most complaints made about monoliths are really about the missing boundaries inside one — a different problem with a cheaper fix — and a monolith with real module boundaries is the right default for most systems. The page opens with sixteen common complaints and where each one actually lives, then covers the two axes every architecture argument confuses, why one deployable is the default and the best argument against it, drawing and enforcing module boundaries, running a large monolith, where it genuinely runs out, five documented cases, and where it degrades.

Deploy unit
One artefact, often run in several roles
At scale
GitHub: ~2M lines, 1,000+ engineers, up to 20 deploys a day
Failure mode
Eroded module boundaries, not size

What is actually wrong — sixteen complaints about a monolith, and where each one lives

The word “monolith” usually comes up when something hurts. A change takes forty minutes to reach production. Nobody dares to touch billing. A bug in a report took checkout down with it. Each of these complaints names a real cost, and each is usually followed by the same proposal: split the system into services. But the complaints come from three different places. Some live in the code — in the boundaries between its parts, or the lack of them. Some live in the build, test and deploy tooling. Only the last few live in the fact that the whole thing ships as one unit. Splitting the deployment fixes only that last group. Here are sixteen complaints teams make every day, sorted by where they really live. Later sections cite the rows by number.

#The complaintWhere it actually livesFix it first byWhen a separate service is the answer
1“A change to invoicing broke the login page”Boundaries: modules call each other’s internalsGive each module a public interface and fail the build on any other importNever for this reason: a service with the same coupling breaks over the network instead
2“Nobody knows what depends on billing”Boundaries: the dependency graph exists only in people’s headsDeclare which module may depend on which, and check it in CINever: the graph is the prerequisite for any split
3“Every feature touches files in ten folders”Boundaries: code grouped by technical layer, not by what the business doesRegroup by module — move files, do not rewrite themNever
4“Half the codebase reads and writes the orders table”Boundaries: shared tables and joins across modulesOne owning module per table; everyone else asks that moduleOnly when the owner genuinely needs a different kind of store
5“The common module is the biggest one”Boundaries: a shared drawer that everything depends on and nobody ownsSplit it: true base code with no dependencies stays, domain code moves to its ownerNever
6“Two teams keep editing the same files”Boundaries and ownership: modules do not match teamsAn owner for every module; move boundaries to match teamsWhen teams already own modules and still block each other at release (row 15)
7“Nobody understands the whole thing any more”Boundaries: one global namespaceModules small enough to read alone, each with a page of documentationNever: nobody understands forty services either
8“The test suite takes forty minutes”Tooling: every change runs every testRun only the tests of affected modules — the graph from row 2 — in parallel; fix the slowest testsRarely: test time is a CI problem
9“A build takes forever”Tooling: no incremental build and no cacheA build cache and module-level compilationRarely
10“Merging to main is a fight”Process: long-lived branchesSmall pull requests, trunk-based development, a merge queueNever
11“Deploys are scary, so we do them once a week”Process: big batches and no fast rollbackDeploy smaller and more often; canary releases; feature flags to separate deploy from releaseNever: deploy frequency is not an architecture property — GitHub deploys its monolith up to 20 times a day
12“A new engineer needs two days to run it locally”Tooling: a manual setupOne scripted setup or a dev containerNever: forty services are harder to run locally
13“Image processing needs sixteen times the CPU of everything else”The shape: one build — but not necessarily one process poolRun the same artefact as a separate worker pool on bigger machinesWhen that work needs different hardware, such as GPUs, or a different runtime
14“A memory leak in reports takes down checkout”The shape: one process, one blast radiusSeparate pools per role (row 13), memory limits, timeouts, separate queuesWhen isolation must be guaranteed: a different availability target, or a compliance scope such as card payments
15“Twelve teams wait for one release train”The shape: one deployableRows 10–11 first — most release-train pain is batch sizeWhen teams own modules, deploys are already frequent, and coordination is still the measured bottleneck
16“This part would be far better in another language”The shape: one runtimeUsually nothing — a second language is a second toolchain, a second hiring market and a second set of upgradesWhen the case is technical and measured: machine learning in Python, a hard latency target, a library only one ecosystem has
A monolith is one deployable unit: its modules call each other in-process, and all of them share one database, in which each module should own its own tables. The red dashed arrow is the change that causes decay — one module reading another module’s tables directly, here with a join, instead of asking that module through its interface. From that moment the two modules can no longer be changed, tested or extracted apart. A monolith whose boundaries are enforced stays workable for as long as the business needs it; one without them becomes a system in which no change is local.

Two axes every architecture argument confuses

A monolith is an application whose code is built, tested and deployed as one unit. That is the whole definition. It says nothing about how big the codebase is or how many people work on it. It also says nothing about whether the code inside is well organised. A ten-person product and GitHub are both monoliths in exactly this sense.

The word became an insult more or less on 25 March 2014. That day James Lewis and Martin Fowler published the article that named microservices, and they defined the new style by contrast with the “monolithic application”. Within a few years “monolith” meant old, slow and tangled. But the tangled condition is older and has its own name. Brian Foote and Joseph Yoder called it the big ball of mud in 1997: a system with no recognisable structure, where everything is connected to everything. That is a property of the code, not of the deployment. A big ball of mud split into twenty services is still a big ball of mud — only now its connections cross a network.

How many deployables a system has and how well its code is divided into modules are two independent properties, and most architecture arguments treat them as one. A tangled codebase that is split into services does not become microservices: it moves right, into the distributed monolith, where the same coupling now has network calls in it and the services still have to be released together. Only design moves a system up. So the order is fixed — first give the monolith real module boundaries, then extract a module into its own service only where a measured constraint demands it, because only a module that is already self-contained can become a service that deploys on its own.

So there are two questions, not one. How many deployables does the system have? And how well is its code divided into modules — parts with a clear job, a published interface and nothing reaching into their insides? The answers give four kinds of system. The table below shows what each one costs. Sam Newman’s vocabulary for the left column is worth knowing: a single-process monolith is any monolith, and a modular monolith is a single-process monolith whose modules are real. The bottom-right cell has a name too, and it is the one this page is mostly trying to keep you out of.

KindWhat changes togetherA call between partsConsistency across partsMoving a boundary
Big ball of mudAnything with anythingA function callOne database transactionHard: nobody knows what depends on what
Modular monolithOne moduleA function call through an interfaceOne database transaction, where you allow itA refactor the compiler checks
Distributed monolithSeveral services, released in a fixed orderA network call that can time outA saga — or, often, nothingA migration across repositories
MicroservicesOne serviceA network call through a versioned APIA saga with compensating stepsA versioned API migration

Why one deployable is the right default — and the best argument against it

The case for starting with one deployable is not nostalgia. A single process removes a whole category of problems that would otherwise have to be solved before the first line of business logic. Every row in the table below is work that a set of services needs and a monolith does not. None of it shows up on a product roadmap, and none of it is ever finished.

ConcernInside one processAcross a network
A callA function call: nanoseconds, and it either returned or threwAbout half a millisecond inside one data centre, and it can time out, fail halfway, or succeed after you gave up
Consistency across two partsOne database transactionA saga, an outbox, compensating actions — and explaining “eventually” to the business
Moving a boundaryA refactor the compiler and the IDE checkA versioned API change across two repositories, two pipelines and two deploys in the right order
Debugging one requestOne stack traceDistributed tracing that someone has to build and keep working
Running it locallyOne process and a databaseMany services, their databases and a message broker — or a shared test environment
DeployingOne pipelineOne pipeline per service, and a compatibility matrix between their versions

The deeper argument is about knowledge, and Martin Fowler made it under the name “MonolithFirst” on 3 June 2015. He reported two observations from the projects he had seen. Almost all the successful microservice stories started with a monolith that grew too big and was broken up. Almost all the systems built as microservices from scratch ended up in serious trouble. His explanation was that service boundaries are expensive to move and hard to get right, and that you get them wrong when you do not yet understand the domain — which, at the start of a product, you do not. A monolith lets you move boundaries cheaply while you are still learning where they belong. It defers the expensive commitment until there is evidence for it.

Six days later Fowler published the strongest reply on his own site. Stefan Tilkov’s “Don’t start with a monolith” argued the opposite from experience just as long. Splitting an existing monolith, he wrote, is extremely hard, because over the years every part comes to depend on shared libraries, shared domain objects and a shared database. And the start of a new system is exactly the time to think about carving it into pieces, because it is the last time that is cheap. Tilkov attached one condition: you should know the domain you are building for really, really well. The two essays are still the best short statement of the trade-off.

The most quoted defence of the style came from a small team. In “The Majestic Monolith” (29 February 2016) David Heinemeier Hansson described Basecamp: twelve programmers, millions of users, and a back catalogue of apps they had promised to maintain. His line was that every time you turn a collaboration between objects into a collaboration between systems, you accept “a world of hurt”. The argument is about proportion. A distributed system is a tax paid in people. Twelve programmers cannot afford to spend four of them on it.

Drawing module boundaries, and making them hold

Draw modules around what the business does, not around what kind of code a file is. Orders, billing, catalogue and users are modules. Controllers, services and repositories are not — they are layers, and a layer touches every feature. Two quick tests help. Can you describe the module’s job in one sentence without the word “and”? And does a typical feature change stay inside one module? In domain-driven design the same idea is called a bounded context: a part of the system inside which every term means one thing. “Order” means something different to the warehouse and to billing, and that difference is where the boundary goes.

The same feature — a discount on orders — in two layouts of the same code. Grouped by technical layer, the change lands wherever price code already lives, so it touches five files in three folders that every team shares, and the whole codebase becomes the surface of every feature. Grouped by business module, the change stays inside the orders module and reaches billing only through billing’s published interface, which it calls without changing. Drawing modules around what the business does, rather than around what kind of code a file is, is the first step towards a monolith whose changes stay local.

A module owns four things: its code, its tables, its public interface and the events it publishes. Other modules may call the interface and listen to the events — nothing else. What crosses the boundary should be plain data and ids, not live objects. Passing an entity from one module to another looks harmless, but it hands over the first module’s whole storage layout: the receiver can now load related rows lazily and save changes the owner never sees. The same applies to the database. Each table has exactly one owning module, and no other module joins against it or holds a into it. That is the rule the diagram at the top of the page shows being broken.

Dependencies between modules should form a graph without cycles: base code at the bottom, business modules above it, and nothing at the bottom that knows about anything above. Shopify’s experience adds a correction that is easy to miss. Its engineers found that developers sort code into modules by what its name suggests, not by how it runs. A model called “shop billing settings” lived in the billing package because of its name — but it also held the flag that marks a shop as fraudulent, which every request needs. It belonged at the base of the graph, available to everything. So check where a piece of code is used, not what it is called, before deciding which module owns it.

Making the boundaries hold

Nothing in a monolith enforces a boundary unless you make it. Nothing stops a developer importing a class from another module at two in the morning, and nothing tells them it was wrong. Between services the network enforces the boundary by being inconvenient. Inside a monolith you have to supply that inconvenience on purpose — in the language, in the build, or in a check that fails . What is available depends on the ecosystem:

EcosystemWhat the language or build gives youA check that fails CI
Java, KotlinPackage-private classes; Maven or Gradle modules; Java modules (JPMS) with explicit exportsArchUnit rules; Spring Modulith, which also verifies module structure and documents it
.NETOne project per module; internal types are invisible to other assembliesNetArchTest or ArchUnitNET rules
GoAn internal/ directory can only be imported from inside its parent — enforced by the toolchainUsually nothing more is needed
Ruby on RailsEvery constant is global; engines give some structurePackwerk, Shopify’s dependency checker
PHPNamespaces, with no visibility between themDeptrac rules
PythonOnly a convention: a leading underscoreimport-linter contracts
TypeScript, JavaScriptWorkspace packages whose exports field lists the public entry pointsNx module-boundary rules, dependency-cruiser, ESLint import rules

The check with the best return is the one that fails the build. A dozen lines asserting that nothing outside billing may import billing.internal turn a rule everyone agrees with and nobody follows into a red build with a file and a line number. Without such a check, module boundaries last exactly as long as the person who cares about them stays on the team. For an existing codebase the usual way in is to record every current violation in a to-do file and fail only on new ones. Packwerk calls this declaring bankruptcy. It works, with one caveat Shopify published itself: a checker finds problems far faster than a team fixes them, so the to-do file needs an owner and a budget, or it only grows.

Running a large monolith: scaling, deploys and tests

A monolith scales out like anything else. If the application keeps no state between requests in its own memory — sessions in a database or a cache, files in object storage — you run as many copies as the load needs behind a . Stack Overflow published its numbers for 9 February 2016: 209 million requests and 66 million page loads in a day, served by one ASP.NET application on eleven web servers, with an average question page rendered in 22.71 milliseconds. Its engineers added that the site could by then run on a single web server, and had done so by accident a few times. The limit they worked on was performance inside the application, not the number of deployables.

When a busy monolith runs out of room, the first thing to run out is usually the database, not the application. The answers are the same ones a set of services would need: caching, read replicas for queries that can tolerate slightly old data, and in the end partitioning the data by customer or region. Splitting the code into services does not remove this limit. It gives each service a smaller database and a new problem — the queries and transactions that used to span them.

A monolith is one codebase and one build, but it does not have to be one process pool. The same artefact can be started in several roles — web servers, background workers, a separate queue for CPU-heavy work, one scheduler — and each pool gets its own instance count, machine size, memory limit and restart policy. That answers most of the “one scaling unit” and “one blast radius” complaints without a second codebase: a memory leak in image processing restarts the image pool, not checkout. What the pools still share is the honest limit: one database and one release, which is where a real need for a separate service shows up.

One build does not have to mean one pool of identical processes. The same artefact can start as a web server, as a background worker for one queue, as a worker for a -heavy queue on bigger machines, or as the one scheduler that must never run twice. Each pool gets its own instance count, machine size, memory limit and restart policy. That answers rows 13 and 14 without a second codebase. Image processing gets sixteen times the CPU without the web servers getting any of it, and a memory leak in report generation restarts the report workers, not checkout — the of a fault shrinks to one pool. Prime Video reached the same pattern from the other side in 2023: having merged a distributed service into one process, it ran several copies of it, each with a different subset of the work.

Deploy frequency is a property of batch size and automation, not of architecture (row 11). GitHub described its Rails monolith in 2023: nearly two million lines of code, more than a thousand engineers working on it every day, and deploys as often as twenty times a day. Even the framework is kept current continuously — every Monday a scheduled job opens a pull request that moves the application to the latest commit of Rails itself, and it ships within the week. What makes that possible is the machinery around the code: a merge queue that tests changes in the order they will land, releases to a small share of traffic first, and so that deploying code and switching a feature on are two separate decisions.

Tests are where module boundaries pay a second time. With a declared dependency graph (row 2), CI can work out which modules a change could affect and run only their tests — Shopify gave exactly this as one of the goals of its components. Without the graph, every change runs every test, and the suite grows with the codebase until people stop running it locally. The table lists what grows with a monolith and the fix that does not need a new architecture.

What growsSymptomThe fix inside the monolith
Test timeCI takes longer than a coffee, then longer than lunchAffected-module test selection, parallel runners, quarantine for flaky tests
Build timeEvery change rebuilds everythingA build cache; compiling modules separately
Merge conflictsMain is red because two green changes collidedA merge queue; small pull requests
Deploy riskReleases get bigger, rarer and scarierDeploy every change; canary first; flags for unfinished features
Boot timeAn instance takes minutes to start, so scaling up lags behind loadLoad code lazily; keep start-up work out of the request path
Memory per processEvery instance carries every feature’s cachesSeparate pools per role, each with its own limit

Where a monolith genuinely runs out

There are real limits, and being honest about them is what separates an argument from a preference. They are rows 13–16, plus one that no row captures well: the number of teams. Melvin Conway observed in 1968 that a system’s structure ends up mirroring the communication structure of the organisation that builds it. With three teams, one deployable is a convenience. With thirty, the deployable becomes a meeting everyone has to attend. The table turns each limit into something you can measure before deciding — because each one has a cheaper answer to try first.

LimitThe signal you can measureTry firstWhen extracting a service pays
Scaling profile (row 13)One kind of work dominates CPU or memory, and everything is provisioned for itA separate process pool for that roleIt needs different hardware or a runtime the monolith does not have
Isolation (row 14)Incidents in which one feature took down another; an auditor’s scope that covers the whole applicationPools, timeouts, separate queues, memory limitsA part needs a different availability target, or must leave the compliance scope — card data under PCI DSS is the classic case
Release coordination (row 15)Lead time dominated by waiting for other teams’ changesSmaller batches, feature flags, module ownershipTeams own clean modules, deploy daily, and still block each other
Runtime (row 16)A measured gain from another language, not a preferenceA library, or a small helper process beside the monolithModel serving, GPU work, a hard latency target
Team countDozens of teams; ownership disputes over shared files; a build nobody ownsModule ownership and a platform team for the build and deploy machineryThe modules are already clean, and the shared database and release are what teams trip over

Extracting one service to solve one measured constraint is not the same decision as adopting microservices. The first is a single migration with a clear reason and a clear end. The second is an operating model for the whole organisation. How to do the first safely — a facade in front of the monolith, one capability moved at a time, the data split last — is the subject of the Monolith → Microservices page, and what the second costs is on the Microservices page.

Five cases, and what each one was missing

The five cases cover both directions. Two teams moved from services back to one process, one project merged its own microservices into a single binary, one company spent years making a giant monolith modular, and one site served hundreds of millions of requests a day from a single application. Each is documented by the engineers who did the work.

CaseWhat happenedWhat it shows
Stack Overflow, 2016209 million requests a day from one ASP.NET application on eleven web serversThe limit was performance inside the application, not the number of deployables
Segment, 2018Over 140 single-destination services merged back into one service and one repositoryServices without tooling to test and ship shared changes across all of them cost more than they isolate
Istio 1.5, 2020Four control-plane services merged into one binary, istiodParts that are always released together and run by one team gain nothing from separate deployment
Prime Video, 2023A serverless monitoring pipeline hit a hard limit at about 5% of expected load; one process cut its cost by over 90%Data that crosses a network on every frame pays for it on every frame
Shopify, 2019–2024A monolith built by over a thousand developers made modular with components and PackwerkBoundaries drawn by domain names do not match how code runs, and a checker finds violations faster than teams fix them

Segment’s story is the most quoted, because it went both ways. Its product forwarded customer events to more than a hundred partner . One shared queue meant that one slow partner delayed everyone, so the team gave every destination its own service and queue — a reasonable fix for a real problem. Then each destination got its own repository, to stop one broken test from blocking all the others. Shared libraries followed, and their versions drifted apart across dozens of repositories, because updating all of them was a risk nobody had time for. Each service had its own load pattern and needed scaling by hand. Eventually three full-time engineers spent most of their time just keeping the system alive. The team merged over 140 services into one, pinned 120 dependencies to a single version, and replaced live calls in tests with recorded traffic, so the whole suite ran in milliseconds instead of up to an hour. Improvements to the shared libraries went from 32 in the year before to 46 in the year after. The post also names what it cost: one bug can now crash every destination, and in-memory caches are spread across more than 3,000 processes.

Shopify is the long version, and the most useful one, because it is a modular monolith told from the inside. In 2019 its engineers explained why they chose to make the monolith modular rather than split it: a codebase worked on for over a decade by more than a thousand developers, with checkout and shipping-rate code free to call each other. They sorted thousands of files into a couple of dozen components, one per area of commerce, and in September 2020 released Packwerk to check dependencies between them in every pull request. In February 2024 the same team published a retrospective, and it is unusually frank. Packwerk’s privacy checks, which marked code public or private, were removed, because teams began polishing interfaces instead of fixing dependencies. When the team cleared every violation of one base package, it found a bug in Packwerk that showed it was probably the first user ever to finish a to-do file. Making that one package actually boot and pass its tests in isolation took many months. And components drawn around business domains did not match how the code runs, so they now group smaller packages drawn by function.

Where it degrades

A monolith fails slowly and quietly. Nothing breaks on the day a boundary is crossed for the first time; the cost arrives a year later, when nothing can be changed alone. Almost every failure is one of these:

  • Joins and foreign keys across module tables, which couple modules through storage instead of interfaces (row 4).
  • Packages by layer — controllers, services, repositories — so every feature’s surface is the whole codebase (row 3).
  • A common or utils module that everything depends on and nobody owns (row 5).
  • Module rules agreed in a document rather than checked in CI, so they last a quarter.
  • A to-do file of known violations with no owner, which only grows.
  • ORM entities passed between modules, handing over one module’s storage to another.
  • A test suite slow enough that people stop running it locally, which removes the fast feedback that made the monolith cheap (row 8).
  • Weekly releases defended as caution, when the size of the batch is the risk (row 11).
  • One process pool for every kind of work, so the heaviest job sets the size of every machine (row 13).
  • A split to fix coupling, producing a distributed monolith that keeps the coupling and adds the network.

The honest limit is people. A monolith keeps the cost of a boundary close to zero, and that is both its strength and its weakness: a boundary that costs nothing to cross is crossed whenever someone is in a hurry. Every tool on this page exists to make crossing it cost something again. Even with all of them, a monolith cannot give two teams separate databases, separate release days or separate on-call rotas — and at some number of teams, those are what the teams need. Below that number, the cost of a monolith is discipline; above it, the cost of services is engineers. Most products never reach the number, and the ones that do reach it with a modular monolith extract faster than the ones that arrive with a ball of mud.

The practical check takes an afternoon. List the modules and the team that owns each; if you cannot, start there. Search the queries for joins between tables owned by different modules. Generate the dependency graph and look for cycles. Find out whether any check fails CI when a module imports another module’s internals. Measure the test suite and ask whether it runs only what a change affects. Count the deploys in the last month. And for each complaint you hear about “the monolith”, find its row in the table at the top of this page before anyone opens a proposal to split it.

When to use it

Use it when

  • The product is new and the domain boundaries are still being learned.
  • One team, or a few teams that can release together without pain.
  • Transactional consistency across features matters, and “eventually” would be a business problem.
  • You would rather spend your complexity budget on the domain than on infrastructure.
  • You are prepared to enforce module boundaries in the build, not in a document.

Avoid it when

  • Many teams that already own clean modules still block each other on one release, and the cost is measured.
  • One component needs different hardware or a different runtime, and a separate process pool is not enough.
  • Parts of the system have genuinely different availability targets or compliance scopes.
  • The code is already a ball of mud — then the first job is modules, and splitting it now would only distribute the mud.

Sources and method

What this page stands on

  • 17 sources
  • Checked October 13, 2026

Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.

Found this useful?

Share it with someone who is working on the same problem.