Leer en español · Lire en français · Auf Deutsch lesen
Section 5.8 of 5.8 · Intermediate · 24 min read
When a monolith is the right answer
Video lesson
5 min · narrated, with captionsListen only (audio)
Read the transcript
Before Chapter six crosses the network, it's worth asking directly: when is a monolith the right answer? You'll see the arguments, how far a monolith can scale, and the evidence that says it's time to split.
A new restaurant usually starts with one kitchen, where every cook can see and help every other. Opening five separate kitchens across town on day one, each with its own staff, deliveries and manager, would cost far more, and make every change to the menu a negotiation. Only when the one kitchen is truly overwhelmed, or one dish needs special equipment, does another make sense. Software is similar. And remember: monolith describes deployment, not quality.
In 2015, Martin Fowler sketched the microservice premium. It's illustrative only, with no real data. Across the bottom is complexity; up the side, productivity. Microservices bring costs of their own: deploying many units, monitoring, partial failure, versioned APIs. Below a certain complexity, that's a premium that buys nothing. A monolith does slow as it grows. Only beyond the crossover do microservices pay their way. Fowler also observed that almost all successful microservice systems began as a monolith that grew too big, and almost all built as microservices from scratch got into serious trouble. Good boundaries are hard to find before you understand the domain.
David Heinemeier Hansson, creator of Ruby on Rails, made the case for small teams in The Majestic Monolith: don't distribute your computing unless you truly have to. Stefan Tilkov argued the other side: splitting a monolith later is often harder than expected, because its parts grow tightly coupled. The two positions are closer than they sound. Both say the hard part is the boundaries, and a monolith whose modules are enforced from day one answers most of Tilkov's concern.
So when is a monolith right? With a small team: the communication cost that microservices remove is low, but their operational cost is not. With an uncertain domain or an early product. Moving a boundary between modules is a refactoring; moving it between services is a migration. When you need strong consistency. One database transaction across orders and payments is simple and correct in a monolith. When operations capacity is limited, because each service needs its own pipeline, monitoring and on-call knowledge. And when parts of the domain talk a lot. In-process calls take nanoseconds to microseconds; network calls take milliseconds, and can fail. In every case, make it a modular monolith.
A common belief is that monoliths can't scale. They scale further than most systems ever need. Run many identical copies behind a load balancer, which works only if the application is stateless. So sessions and caches live in a shared store, and any replica can serve any request. All writes go to the primary database, while read replicas, a little behind, serve order history and reports. And slow work, such as emails and payouts, goes onto a queue. Worker processes take it from there. They run the same code with a different start command, so web and worker roles scale separately.
Let's test that with illustrative numbers. A food-delivery start-up with seven engineers expects a peak of one thousand two hundred requests a second within two years. One replica handles a hundred and eighty within its response-time target. Twelve hundred divided by a hundred and eighty is about six point seven, so seven replicas. With about fifty per cent headroom, enough to survive an availability zone failing, that's about eleven at peak. About fifteen per cent of requests write to the database, each making about three writes. That's about five hundred and forty writes a second, well within a single PostgreSQL primary. A cache for menus absorbs most of the reads, and two read replicas take order history and reports. So one codebase, run as about eleven web replicas plus workers, meets the target. The architecture budget goes on module boundaries and statelessness, not on splitting.
When is it time to split? When there's evidence that one deployment unit is the constraint: teams wait on each other, and small fixes queue for days behind unrelated work. Or one module, such as real-time courier tracking, needs fifty times the capacity of the rest at peak, or must stay up when everything else is down. Or a part has different needs, such as card data that justifies a separate, tightly controlled unit. Slow code is not a reason; splitting won't fix it. Then split incrementally, one module at a time, along bounded contexts, starting with the one that has the clearest boundary and the strongest reason to leave.
That completes Chapter five. The full section has a decision guide, a comparison of every style in the chapter, and a healthy-monolith checklist. When you're ready, try the optional chapter test, then move on to Chapter six, where we take these questions across a network. Well done.
What you need to know
- A monolith is a deployment decision (one unit built and released together), not a verdict on internal quality; a well-modularised monolith is a respectable, often superior, architecture.
- Martin Fowler's microservice premium and MonolithFirst (2015) argue that microservices pay off only above a level of complexity, and that most successful microservice systems began as monoliths.
- David Heinemeier Hansson's majestic monolith (2016) makes the case for small teams; Stefan Tilkov (2015) warns that unmodularised monoliths are hard to split, so enforce module boundaries from the start.
- A monolith is usually right for small teams, uncertain domains, early products, strong consistency needs and limited operations capacity.
- Monoliths scale through stateless replicas behind a load balancer, read replicas, caching, background workers and, if needed, partitioning by tenant.
- Split when there is evidence: team contention, deployment queues, wildly different scaling or compliance needs; split incrementally along bounded contexts.
The question behind the chapter
The styles in this chapter all deploy as one unit. Chapter 6 takes the same questions across a network, where every call can fail, be slow or arrive twice. Before crossing that line it is worth asking directly: when is a monolith the right answer? For much of the 2010s the industry's default answer was "rarely", and many teams split young systems into microservices because large, famous companies had done so. A decade of experience has produced a more balanced view, and this section sets it out.
The word monolith needs care. As Section 5.1 explained, it describes the deployment (one unit that is built, tested and released together), not the internal quality. A monolith can be a well-structured modular system or a big ball of mud, and the same is true of a set of services. Many of the problems blamed on monoliths are really problems of poor modularity, and splitting a poorly modularised system into services produces a distributed big ball of mud, which is worse.
MonolithFirst and the microservice premium
In May 2015 Martin Fowler described the microservice premium: microservices bring real costs of their own (automated deployment of many units, monitoring, handling partial failure, eventual consistency, versioned APIs between teams), and those costs slow a team down unless the system is complex enough for microservices to pay them back. His advice was to default to a single, well-modularised monolith and move to microservices only when complexity demands it.
A month later, in an article titled MonolithFirst (June 2015), he added two observations from the projects he had seen: almost all successful microservice systems had started as a monolith that grew too big and was then broken up, and almost all systems built as microservices from scratch had ended up in serious trouble. One reason is that good service boundaries are hard to find before the domain is understood, and a wrong boundary between services is far more expensive to move than a wrong boundary between modules. He described several routes: build a monolith and peel services off its edges later; build a deliberately disposable monolith; or start with a few coarse-grained services and split them further once boundaries settle.
The microservice premium
The majestic monolith, and the opposing view
In February 2016 David Heinemeier Hansson, creator of Ruby on Rails, published "The Majestic Monolith" on the Signal v. Noise blog. He described Basecamp, then built by a team of about a dozen programmers, as one integrated application serving web, mobile, desktop and email clients, and argued that small teams should embrace a well-crafted monolith instead of copying the architecture of organisations with thousands of engineers. His key line was Don't distribute your computing!
unless you truly have to.
Practitioners do not all agree. Also in June 2015, Stefan Tilkov argued on Fowler's site, in "Don't start with a monolith", that splitting a monolith later is often harder than expected, because its parts grow tightly coupled through shared libraries, domain objects and the database. If the system is clearly large enough to justify separate parts and the domain is well understood, he recommended designing separate subsystems from the start. The two positions are less far apart than they sound: both say the hard part is the boundaries, and a monolith whose modules are enforced from day one (Section 5.3) answers most of Tilkov's concern.
Real cases point both ways but share a lesson. Segment described in 2018 how it had consolidated more than 140 small services back into a single service after operational overhead and diverging shared libraries slowed delivery; Amazon Prime Video moved one monitoring workload from distributed serverless components to a single process in 2023 (see Section 1.3). Equally, organisations with hundreds of teams, like the large-scale cases of Section 6.3, gain real independence from services. The lesson is fit: the deployment style must match the organisation, the domain's maturity and the qualities required.
When a monolith is the right answer
A small team. With one to a few teams, the communication cost that microservices remove is low, while their operational cost is the same as for a large organisation. A monolith lets everyone understand and change the whole system.
An uncertain domain or an early product. When you are still discovering what the product is, boundaries will move. Moving a boundary between modules is a refactoring; moving it between services is a migration with data, APIs and deployments.
Strong consistency needs. A single database transaction across orders, payments and stock is simple and correct in a monolith. Across services it needs sagas, outboxes and compensation (Section 6.7).
Limited operations capacity. Each service needs its own pipeline, monitoring, alerting, scaling rules and on-call knowledge. A team without a platform for this will spend its time on plumbing.
Performance-sensitive interactions. In-process calls take nanoseconds to microseconds; network calls take milliseconds and can fail. Chatty interactions between parts of the domain are far cheaper inside one process.
In all these cases the recommended form is a modular monolith, not an unstructured one. It keeps the option to extract services open, as Section 5.3 explains.
Scaling a monolith
A common belief is that monoliths cannot scale. They scale further than most systems ever need, using the X axis of the scale cube described by Martin Abbott and Michael Fisher (running many identical copies) together with a few standard tactics. Chapter 9 treats each in depth (Section 9.1).
Horizontal replicas behind a load balancer. Run many identical copies of the application and spread requests across them. This only works if the application is stateless: sessions, uploaded files and caches must live in shared stores, so any replica can serve any request (Section 9.2). It also gives redundancy: one replica failing no longer stops the service.
Read replicas. Most systems read far more than they write. Database read replicas copy the primary's changes and serve reads such as order history and reports, at the cost of replication lag: a replica may be a fraction of a second behind (Section 7.4).
Caching. Menus, product pages and configuration change rarely and are read constantly; caching them in memory or in a shared cache removes most database load (Section 7.6). Static assets go to a CDN.
Background workers. Slow or bursty work (emails, PDF invoices, imports, payouts) goes onto a queue and is processed by worker processes. Workers usually run the same codebase with a different start command, so the system stays one codebase but the web and worker roles scale separately (Section 9.3).
Bigger machines and partitioning. Vertical scaling of the database is often the simplest next step. For very large systems, data can be partitioned by tenant or region, with a full copy of the monolith per partition (the Z axis of the scale cube). Stack Overflow is a well-known example of how far a largely monolithic design can go: according to its engineers' own write-up, on one day in February 2016 its load balancers handled about 209 million HTTP requests, with the main Q&A application (one multi-tenant application for all its Q&A sites) running on nine primary web servers, with four SQL Server machines serving the whole network.
A monolith scaled out
Worked example
Worked example: how far will one monolith go?
A new food-delivery start-up has 7 engineers and expects, by the end of year two, a peak of 1,200 HTTP requests per second at Friday dinner time. A load test of the modular monolith shows one replica (4 vCPU) handles 180 requests per second within the 300 ms p95 target. About 85% of requests are reads (menus, order tracking). Figures are illustrative.
| Replicas for peak | 1,200 / 180 = 6.7, so 7 replicas at peak |
|---|---|
| Headroom | Keep utilisation well below saturation (Section 2.4) and survive one availability zone failing: plan for about 50% headroom, so 7 x 1.5 = about 11 replicas at peak, scaled down off-peak |
| Database writes | 15% of 1,200 = 180 requests per second, each doing about 3 writes: about 540 writes per second, well within a single managed PostgreSQL primary on a mid-sized instance |
| Database reads | 1,020 read requests per second; a cache for menus (which change rarely) absorbs most, and two read replicas take order history and reports, accepting a second or so of replication lag |
| Background work | Receipt emails, courier payouts and restaurant reports move to a worker pool fed by a queue, so a slow email provider cannot slow checkout |
| Conclusion | One codebase, deployed as one unit and run as about 11 web replicas plus workers, meets the two-year target. The architecture budget goes on module boundaries and statelessness, not on splitting. |
The limit that arrives first is rarely CPU. More often it is team coordination as the engineering group grows, or a single module (such as dispatch) with very different scaling or release needs.
Every figure here should be replaced by measurements from your own load tests; capacity planning is covered in Section 9.7.
Signs that it is time to split
Splitting should be a response to evidence, not to fashion. The signs that one deployment unit has become the constraint are mostly organisational and operational:
Team contention. Several teams change the same code and wait for each other; merge conflicts, coordination meetings and broken builds caused by other teams rise as the organisation grows. If modules are already well bounded and contention persists, deployment independence may be what is missing.
Deployment queues. Releases are batched into trains because every change needs the whole system re-tested; a small fix waits days behind unrelated work; one team's failed change blocks everyone's release.
Wildly different scaling or availability needs. One module (say, real-time courier tracking) needs fifty times the capacity of the rest at peak, or must stay up when everything else is down. Scaling the whole monolith for one module's needs becomes expensive.
Different security or compliance scope. Card data or health records may justify a separate, tightly controlled unit to keep the rest of the system out of audit scope.
Different technology needs. A machine-learning model best served from Python inside a Java monolith, for example.
Some reasons are not good reasons on their own: "monoliths are legacy", a desire to use a new technology, or slow code (which is a design problem that splitting will not fix and may make worse by adding network calls).
How to split when the time comes
Split incrementally, one module at a time, starting with the module that has both the clearest boundary and the strongest reason to leave. Modularise first if the code is not yet modular: extracting a service from a tangle drags the tangle along. Then extract along bounded contexts (Section 5.7): put a network API in front of the module's existing API, move its data to its own database, replace in-process events with a message broker and replace any shared transactions with explicit sagas. Route traffic gradually to the new service and keep the old path until the new one has proved itself.
This is the strangler fig approach and its relatives, covered in Section 11.6. The destination is often not "all microservices" but a monolith with a few extracted services where the evidence demanded it; the service-based architecture of Section 6.2 is a common, pragmatic end state.
The Chapter 5 styles compared
| Characteristic | Layered | Modular monolith | Pipes and filters | Microkernel | Hexagonal / clean |
|---|---|---|---|---|---|
| Main partitioning | Technical | Domain | Processing steps | Core plus plug-ins | Inside versus outside |
| Simplicity | High | Medium to high | High | Medium | Low to medium |
| Overall cost | Cheap | Cheap | Cheap | Cheap | Cheap to medium |
| Modifiability for business change | Low to medium | High | High within the flow | High in planned directions | High for infrastructure |
| Testability | Medium | Medium to high | High | Medium to high | High |
| Deployability | Low | Low to medium | Low to medium | Medium | Set by deployment style |
| Scalability | Low | Low to medium | Low to medium | Low | Set by deployment style |
| Fault tolerance | Low | Low | Low to medium | Low to medium | Set by deployment style |
| Typical fit | Small to medium apps, one team | Growing products, several teams | ETL, media, logs, compilers | Products with many variants | Logic that must outlive frameworks |
The ratings summarise the judgements made in each section and assume typical implementations. Domain-driven design does not appear as a column because it is a method for finding boundaries, which any of these styles can use. Real systems combine styles: a modular monolith whose modules are hexagonal inside, with a pipes-and-filters import job and a microkernel for tax rules, is entirely normal.
Decision guide
Decision guide: choosing among the Chapter 5 styles
Use it for a new system or a major restructuring that will run as one deployment unit. Work down the questions; several styles can apply at different levels.
- Is the system mainly a flow of data through a series of transformations (import, clean, enrich, export), rather than users interacting with shared state?YesUse pipes and filters for that flow (Section 5.4). Keep each filter stateless and the data format between filters explicit.NoGo to the next question.
- Does the product have a stable core workflow but many variants of particular steps (per country, customer, provider or file format), or do third parties need to extend it?YesUse a microkernel (Section 5.5) for those variation points, ideally after building two or three variants so the contract is grounded in real cases.NoGo to the next question.
- Is it small (one team, a few dozen screens), with simple rules, and unlikely to grow much?YesUse a layered architecture (Section 5.2) with closed layers enforced by an architecture test. Do not add more structure than the problem needs.NoGo to the next question.
- Will several teams work on it, or does the domain span several areas with their own language (menus, orders, dispatch)?YesUse a modular monolith (Section 5.3) with modules drawn along bounded contexts (Section 5.7), each owning its data and API.NoA layered or modular structure both work; prefer modules by domain if growth is likely, because restructuring later is costly.
- Inside each module or application, is the business logic substantial, or does it depend on external services likely to change?YesStructure the inside with ports and adapters (Section 5.6): domain at the centre, adapters for the web, database and providers.NoKeep the framework's default structure inside the module; add ports only at boundaries that are likely to change.
Checklist: a monolith that stays healthy and splittable
Most of these cost little when done early and a great deal when retrofitted.
- Modules follow business domainsTop-level packages are named after bounded contexts (orders, dispatch, payments), not technical layers.
- Each module has a small public APIEverything else is internal; an architecture test fails the build on a forbidden import.
- Each table has one owning moduleSeparate schemas or naming conventions make ownership visible; no module reads another's tables.
- The application is statelessSessions, uploads and caches live in shared stores, so any replica can serve any request and replicas can be added at will.
- Slow work runs in background workersEmails, reports, payouts and imports go through a queue, so web replicas stay fast and the two can be scaled separately.
- Build and tests stay fastTrack build and test times; keep the main pipeline under about 10 to 15 minutes by testing modules separately and in parallel.
- Deployment is automated and frequentSmall, frequent releases with automated rollback; feature flags decouple deployment from release.
- Observability by moduleLogs, metrics and traces are tagged with the module, so you can see which module is slow, failing or expensive.
- Signs of strain are measuredLead time, deployment queue length, merge conflicts per team and resource use per module are reviewed regularly, so the decision to split is based on evidence.
Check your understanding
Why is a wrong boundary between services more expensive than a wrong boundary between modules?
Moving a module boundary is a refactoring inside one codebase and one database transaction; moving a service boundary changes network APIs, data stores, deployments and often team ownership, and must be done without downtime.
What must be true of a monolith before you can scale it by adding replicas?
It must be stateless: sessions, files and caches must live in shared stores so any replica can serve any request.
Name two signs that a monolith has become the constraint, and one reason for splitting that is not good enough on its own.
Signs: persistent team contention and deployment queues, or one module with very different scaling or compliance needs. Not enough on its own: fashion, wanting a new technology, or slow code.
How do the MonolithFirst and "Don't start with a monolith" positions overlap?
Both say the hard problem is finding and keeping good boundaries; a monolith with enforced module boundaries addresses Tilkov's concern while keeping Fowler's lower cost.
Sources and further reading
Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.
Ask: “We are five developers building a new B2B scheduling product and investors keep asking about microservices. How do I explain why we are starting with a modular monolith, and what would make us change?”