Monolith vs Microservices: An Engineering Decision by Numbers, Not Fashion

A well-structured monolith is the right choice for most teams under 20 developers. Microservices turn a nanosecond function call into a millisecond network call and multiply operational complexity. Decide by numbers, not fashion.
For most teams, specifically every team under 20 developers, the correct engineering answer is a modular monolith, not microservices. The reason is not taste but arithmetic: microservices pay a real, measurable price for a benefit only large teams need. When you split your app into independent services, every operation that used to be an internal function call (costing nanoseconds, a billionth of a second) becomes a network call over HTTP or gRPC costing 1 to 10 milliseconds at best inside the same data center, hundreds of thousands of times slower. A single page that assembled its data with five function calls in microseconds may become five network calls of tens of milliseconds, and each network call can fail, so you need retries, circuit breakers, timeouts, and distributed tracing. The direct answer: start with a modular monolith with clear internal boundaries, and move to services only when you have a specific numeric reason (two independent teams colliding on deploys, a component needing radically separate scaling, or a completely different deployment cadence), not because microservices is a trending headline.
The core rule: microservices solve an organizational problem (many teams colliding) before they solve a technical one. If you do not have that problem, you are buying its complexity without its return.
The real cost by numbers
Let us compare honestly. The difference is not code quality but where the complexity lives.
| Dimension | Modular monolith | Microservices |
|---|---|---|
| Call between components | Function call, nanoseconds | Network call, 1 to 10 ms |
| Call failure probability | Near zero | Always possible, needs handling |
| Deployment | One unit | Dozens of independent pipelines |
| Tracing an error across the system | One log, one stack trace | Distributed tracing across services |
| Transaction across components | One database transaction | Complex distributed transaction (saga) |
| Infrastructure | One or two servers | Container orchestration, service discovery, gateway |
Take the distributed transaction number specifically. In a monolith, updating the order and deducting inventory together in one database transaction either all succeeds or all fails, with free ACID guarantees. In microservices, the order is in one service and inventory in another, so you lose the single transaction and must build a saga pattern with manual compensations on failure, one of the hardest and most error-prone parts of distributed engineering.
Why the modular monolith is right under 20 developers
A monolith does not mean messy code. A modular monolith splits the system internally into modules with clear boundaries, each with its public interface, never touching another's internal tables, but all deployed together sharing one process. You get the organizational benefits of separation without paying the network tax:
- Development speed: one developer runs the whole system on their machine with a single command, no need to start 12 services to test a feature.
- Direct debugging: an error shows in one connected stack trace, you do not chase it across the logs of five services.
- Safe refactoring: moving code between modules is a local change the compiler catches, not a contract between two deployed services.
- Lower running cost: it runs on one or two cloud servers, not on a container orchestration stack that needs a team to maintain.
The companies famous for microservices built them after reaching hundreds of developers with independent teams colliding on deploys. Copying their solution before reaching the size of their problem is an expensive mistake.
The actual decision points
When do you truly move? When one of these measurable numeric signals appears:
- Independent teams colliding: you have two or more teams each blocking the other's deploy, so independent deployment becomes a real organizational need.
- Radically different deployment cadences: a component updated ten times a day next to one updated once a month, where coupling them slows the fast one needlessly.
- Sharp separate scaling need: an image-processing or AI component devours the CPU while the rest of the system is light, so isolating it to scale alone saves resources.
- Mandatory technical boundaries: a component that must run in another language or under a compliance requirement forcing isolation.
The absence of all these signals means the monolith is still the cheapest and fastest choice.
The gradual path: from modular monolith to services
The best strategy is not choosing once and forever, but building a modular monolith with clean boundaries from day one, so extracting a service later is an easy operation, not a rewrite. Apply these principles:
- Modules with explicit boundaries: organize code into modules (orders, inventory, billing) where none reaches another's tables directly, only through a defined interface.
- No shared tables across modules: each module owns its tables. This is the single most important condition that makes extraction possible later.
- Extract only when needed: when a real decision signal appears, carve out only the relevant module into a service, so you pay the network tax on one boundary, not across the whole system.
This way you start cheap and fast, and defer distribution complexity until you actually reap its benefit, not before. A typical app like this runs comfortably on the Aamaal or Muassasa plan with memory enough for an active database and a cache layer in one process.
Common mistakes when going microservices too early
Many small teams choose microservices early then pay the price in recurring forms:
- The distributed monolith: the worst outcome, separate services so intertwined that none deploys without the others. You got the network tax and deployment complexity with no independence, all the cost without the benefit.
- A shared database across services: several services write to the same tables, ownership boundaries vanish, and any table change becomes a potential break for services you do not know about. This cancels the most important benefit of separation.
- Hard debugging: a simple error in a user request now requires opening the logs of five services and correlating distributed traces to find where it happened, when it used to be one stack trace in the monolith.
- Doubled running cost: a small team now maintains container orchestration, service discovery, a gateway, and distributed monitoring, work that consumes real development time without adding a customer feature.
A typical scaling story by numbers
Imagine a realistic path for a Saudi company with a custom system. It starts with a team of 3 developers and a modular monolith on one cloud server, shipping features fast and debugging in one stack trace. It grows to 8 developers, staying on the monolith but tightening module boundaries and forbidding shared tables between them, so deployment stays one easy unit. At 15 developers in two teams, the billing team's deploys start colliding with the orders team, and the first real decision signal appears. The fix is not to break everything apart, but to extract only the billing module into an independent service, so you pay the network tax on one known boundary and the two teams deploy independently. The rest of the system stays a modular monolith, so you reap the benefit of isolation only where you needed it, not everywhere. This gradual path saves years of premature complexity, and defers every cost until the moment a number in your system justifies it.
Notice the economics of this decision by numbers. Extracting one service adds about 1 to 5 milliseconds per call that crosses the new boundary, and needs an independent deployment pipeline and extra monitoring, an acceptable cost on one boundary serving an independent team. But if you broke the system into ten services at once at 15 developers, you would multiply this cost by ten with no organizational justification, and slow every page that assembles its data from several services. The difference is that going gradually ties each cost to a matching benefit, while a full early breakup pays all costs before any benefit matures, and this is the core of why the modular monolith stays the numerically correct choice for most teams.
Practical conclusion
Decide with this balance: if your team is under 20 developers with no teams colliding on deploys and no component needing sharp separate scaling, build a modular monolith with clean module boundaries and no shared tables. Run it on one or two servers and watch the metrics. When a specific numeric decision signal appears, extract the single relevant module into a service, not the whole system at once. Do not choose microservices because it is in the headlines, choose it when a number in your system says it has become cheaper than keeping the monolith. And if you want a server with enough resources for an active modular monolith, start from Riyadh servers with a response time near 8ms.


