TL;DR
- Start with a monolith for a team under roughly eight engineers; it ships faster and has far fewer moving parts to operate.
- A modular monolith keeps one deployable but enforces module boundaries and separate data ownership, so it scales with the team without distributed-systems overhead.
- Extract a module into its own service only when it has a genuinely different scaling, deployment, or ownership need, not because microservices feel more modern.
- Amazon Prime Video moved one monitoring system from distributed serverless back to a monolith and cut infrastructure cost by over 90%.
- Clean module boundaries and isolated data are what make a later migration to microservices a strangler-fig extraction instead of a rewrite.
For a backend serving a small-to-mid team, a monolith or a modular monolith is almost always the right starting point, not microservices. A plain monolith is the fastest way to ship and the cheapest to run. A modular monolith keeps that single deployable but adds internal boundaries and separate data ownership, so the codebase does not turn into mud as the team grows. You reach for microservices later, and only for the modules that actually need independent scaling or deployment.
We spend a lot of our time at AlterSquare undoing the opposite decision: teams that split into fifteen services at seed stage and now spend more time on deployment plumbing than on product. This piece is strictly about the backend and the services layer. If you are weighing the same trade-off on the client, we covered that separately in monolith vs modular frontend architecture, which is a different problem with different failure modes.
What is the difference between a monolith and a modular monolith?
A monolith is one deployable unit where all the code shares a process and usually one database, with no hard walls between features. A modular monolith is still one deployable, but the code is split into modules with defined boundaries: each module exposes a public API, owns its own data, and cannot reach into another module’s tables. Same deployment, very different internal discipline.
The distinction that matters is the boundary, not the box. Both ship as a single artifact. The difference is whether a change in the billing code can quietly break the ordering code because they share a table and a tangle of imports. In a monolith it can. In a modular monolith it should not, by construction.
| Dimension | Monolith | Modular monolith |
|---|---|---|
| Deploy unit | One | One |
| Internal boundaries | None enforced | Modules with public APIs |
| Data ownership | Shared, freely joined | Per module, no cross-module joins |
| Best team size | Under ~8 engineers | ~8 to 30 engineers |
| Migration cost to services | High, boundaries unknown | Low, boundaries already drawn |
When is a plain monolith the right call?
A plain monolith is right when the product is still finding its shape and the team is small enough to hold the whole thing in their heads. Under roughly eight engineers, the overhead of formal module boundaries buys you little, and the ability to refactor freely across the codebase is an asset, not a risk. Ship the monolith, learn what the domain actually is, then add structure.
Concrete signals a plain monolith is still the correct default:
- The team is under eight engineers and everyone touches most of the code.
- The domain boundaries are not stable yet; you are still renaming core concepts.
- Traffic is modest and no single feature has a wildly different scaling profile.
- You need to demo or reach revenue quickly and cannot afford operational drag.
The ByteByteGo comparison lands in the same place: start simple, add structure when real constraints appear. We have watched more startups die of premature distribution than of a monolith that got too big. The monolith rarely kills you. Fifteen half-owned services often do.

What actually makes a monolith modular?
Modularity is enforced separation, not folders named after features. A codebase with a modules/ directory but shared tables and free cross-imports is still a monolith wearing a costume. Real modularity means a module can only be reached through its public interface, and its data belongs to it alone.
The three rules that hold up in production
Milan Jovanović’s modular monolith guidance reduces to three rules we apply on client work, and they are the ones that make or break a later split:
- Define boundaries by domain. Each module maps to a bounded context with one clear responsibility and minimal dependencies on others.
- Talk through public APIs only. Modules call each other’s public interfaces, never internal classes or private methods.
- Isolate the data. Each module owns its tables or schema. No shared tables, no cross-module joins, no reaching into another module’s store.
Data isolation is the rule teams skip first and regret most. The moment two modules join across each other’s tables, you have welded them together, and any future extraction becomes a rewrite. Drawing these lines is easier when you have already mapped how the current system hangs together, which is why we start engagements with dependency graph mapping before touching code.
When should you extract a module into its own service?
Extract a module only when it has a need the rest of the system does not share: a different scaling curve, an independent deployment cadence, a separate team that should own its release, or a compliance boundary. Absent one of those, a well-bounded module inside the monolith is cheaper to run and easier to change than a network call.

The cautionary data point is Amazon Prime Video’s monitoring service, which was built as distributed serverless components, hit a hard scaling wall at around 5% of expected load, and was consolidated back into a single process on ECS. That change cut infrastructure cost by over 90%. Distribution is a cost you pay for a benefit; if the benefit is not there, you are just paying. We make the same argument in our microservices vs monolith performance test, where the network hop dominated once the workload was chatty.
How do you migrate a modular monolith to microservices?
You migrate by replacing logical boundaries with physical ones, one module at a time, using the strangler-fig pattern. A module that already exposes a public API and owns its data can be lifted out behind that same interface: route its calls over the network, move its schema to a separate database, and the rest of the system barely notices. The migration is an extraction, not a rebuild, precisely because the seams were cut in advance.
The order that works: pick the module with the strongest independent need, harden its interface, split its data, then flip traffic. Do the next one only after the first is stable in production. Our strangler-fig field notes cover the mistakes that undo this in the first three months, usually shared data that was never truly separated. And sometimes the honest answer is that a bounded rewrite of one module beats a slow strangle; we lay out that call in when incremental modernisation is the wrong call.
Where backend teams get this decision wrong
The most expensive mistake is treating microservices as a maturity badge. Splitting early feels like discipline; in practice it hands a small team the operational load of a large one, before there is anyone to carry it. We have seen this break systems well past $2M ARR, where the roadmap stalls under deployment complexity nobody chose deliberately.
The second mistake is the reverse: a monolith with zero internal boundaries that grew for five years, where every change is a landmine. That team does not need microservices. It needs modules first. Architecture decisions are hard to reverse and rarely account for what already broke, a point worth sitting with before any big split, and one we make in stop letting AI make your architecture decisions. The modular monolith is the option that keeps the most doors open for the longest time, which is usually what you want when you cannot afford to get it wrong.
If you are staring at this decision on a system you cannot afford to destabilise, that is the work we do. Start here, no pitch, just a conversation.
Frequently Asked Questions
Is a modular monolith just microservices in one process?
No. It shares the boundary discipline of microservices, defined modules, public APIs, isolated data, but keeps everything in one deployable process. You get the design benefits of clear separation without the operational cost of network calls, separate databases, service discovery, and distributed tracing. The tax of distribution only arrives if and when you extract a module into its own service.
Can a modular monolith scale to high traffic?
Yes. A single deployable scales horizontally by running more instances behind a load balancer, and that carries most products a long way. Amazon Prime Video consolidated a distributed system back into a monolith and improved scaling while cutting cost by over 90%. You reach for per-module services when one module has a scaling profile so different from the rest that scaling the whole app for it becomes wasteful.
How many engineers before a plain monolith stops working?
As a rough rule, a plain monolith works well under about eight engineers. Past that, contention on the shared codebase and the risk of unintended cross-feature breakage start to hurt, and module boundaries pay for themselves. The number is a heuristic, not a law: a disciplined small team can run a modular monolith early, and a large team can survive a plain monolith if the domain is simple.
What is the biggest risk when moving from a monolith to microservices?
Shared data that was never actually separated. If two modules read and write each other’s tables, extracting one into a service turns a clean split into a rewrite, because you have to untangle the data at the same time as the code. Isolate each module’s data inside the monolith first. Once the boundaries hold logically, the physical split with a strangler-fig approach is far lower risk.



Leave a Reply