Modular Monolith
Concept
One deployable unit on the outside, with the inside cut into modules that are only allowed to call each other through defined entry points.
Why it exists
Putting everything in one piece is the fastest way to start a service. Payments, notifications and document storage all live in the same program, so any function can call any other directly, and in a rush there was no reason to stop anyone from reaching into someone else's code. About two years later that convenience arrives as a bill.
At Pagedeck, Theo once changed a single line of notification copy and broke the payment screen. The notification code had been reading payment's internal data directly, and nobody knew how many connections like that existed or where. After that the team became afraid of deploying, and a rule appeared that small fixes don't go out on Fridays.
The usual answer at this point is to cut the system into separate services that deploy on their own. But at 45 people, splitting into eight services means eight sets of deployment, monitoring and incident response — and the boundaries themselves are still not certain. A modular monolith fills that gap. It separates dividing the code from dividing the deployment: cut the inside first, and leave what ships as a single unit.
How it works
The deployable unit stays as one piece while boundaries go up inside the code. Those boundaries are enforced by tooling rather than by agreement, and that is exactly what separates this from an undivided monolith.
- Split modules along business capabilities. Payments, notifications, documents, accounts — whatever the business talks about as a distinct thing becomes one module. The cut follows the business, not technical layers like screen, logic and data.
- Give each module a defined entry point. The notification module exposes one thing, "send this notification to this user," and hides which tables it reads and in what order it works.
- Block every call that goes around that entry point. Set dependency rules so the build fails if the notification module reads the document module's internal data directly. Without this step, all you have is folder names.
- Let modules own their data. Only the payment module reads and writes payment tables; other modules receive values through the entry point. The rule holds even when everything sits in the same database.
The result is lower coupling and higher cohesion. Coupling is how entangled components are with each other, and cohesion is how much the code inside one module is about one job. Put the earlier incident in those words: notifications and payments were tightly coupled, and once a boundary went up, changing notification copy left payment code unable to see the change at all.
Modules still call each other as functions inside the same program. No network hop means no call that fails on its own, and an error can be traced through a single stack trace. In exchange, one module exhausting memory stops everything, and fixing notifications still means redeploying the whole thing.
Where it shows up
Teams pick it while they're still unsure where the boundaries belong and not yet large enough to run several services. A company like Pagedeck — 45 people, deploying two or three times a day — is the typical case.
Where to cut is a question domain-driven design (DDD) answers. It draws lines in the language of the business rather than the language of the technology, so the test is whether the business treats payments and documents as different things, not what the database tables look like. Module boundaries become the exact lines you would split services along later, so drawing them carefully once avoids doing the work twice.
That's why many teams use this structure as a path rather than a destination. The sequence usually goes:
- Start as one piece. At a stage where you don't know how many users there will be, don't spend time dividing anything.
- Cut into modules when it starts hurting — the point where fixing one side breaks another, as in the notification and payment incident.
- Pull out only the modules that genuinely need to run on their own. Usually those are the ones with very different traffic or with a team of their own.
Plenty of teams never reach step 3. If modules alone removed the fear of deploying, stopping there is a legitimate end state. This is also where monorepo gets confused with it. A monorepo is a way of keeping several projects in one repository, so it's a question of where code lives; a modular monolith is a question of what ships together. Eight services managed in one repository make a monorepo and not a modular monolith.
How it sounds at work
"Let's cut this into modules instead of separate services. The boundaries aren't settled yet."
Defer the decision to split deployments, and tidy the code first.
"The build blocked because the notification module reads a document table directly."
The boundary is being enforced by tooling, not by memory.
"Payments has different traffic from everything else, so I'd pull just that one out."
Move one module into its own service instead of splitting the whole system.
Pagedeck moved to this structure over six months after that incident. They didn't cut everything at once: notifications and payments — the two involved in the break — got boundaries first, and the rest moved one at a time whenever someone had a reason to open them. What Grace heard in the report was numbers, not architecture terms. Incidents following a deploy went from seven a quarter to two, and the no-Friday-deploys rule went away.
The service still ships as one unit today, and Theo plans to keep it that way for now. Being ready to split and having split are different states, and the operating cost starts the moment you split.
What people get wrong
The most common reading is that sorting code into feature folders makes it a modular monolith. On the surface the two do look alike. But with nothing enforcing the rules, the boundary rests on people remembering it. At 3 a.m. during an incident somebody reads the neighboring module's table directly, that code passes review, and six months later you're back to one piece with tidy folder names. A boundary survives only if the build fails.
The other reading is that this is a waypoint on the way to microservices. It looks like a half-finished split, so the assumption is easy. But splitting has a price. Each service needs its own deployment, monitoring and incident response, and calls that were function calls become network calls, which adds places where things can fail. At 45 people deploying two or three times a day, there's no reason to pay that. Teams that treat it as the destination and stay for years are common.
Easily confused with
| Term | Deployable units | How it differs from a modular monolith |
|---|---|---|
| Monolith | One | The deployable unit is identical. The only difference is whether tooling enforces the internal boundaries, which decides whether notification code can read payment data |
| Microservices | One per capability | Both draw boundaries; the difference is splitting the deployment. Splitting lets you ship and scale each piece alone, at the cost of one deployment, monitoring and on-call setup per service |
Where to go next
- Software architecture
see which options this structure sits between
- Monolith
the starting point, so the difference reads immediately
Related terms
Last updated 2026-09-16