Monolith, Modular Monolith, or Microservices: How to Choose Your Backend Architecture
Microservices, microservices, microservices: like Steve Ballmer on stage, a sizeable part of the industry has been chanting this refrain for years. As if microservices had become a prerequisite for any resilient application worthy of becoming the next Netflix.

I’ve been guilty of following the trend myself. After working at Molotov TV, in an environment where microservices addressed real constraints, I tried to apply the same instincts to NuCorder. I let its ageing Java monolith deteriorate, then gave in to the siren call of microservices out of habit rather than need.
In a previous article, I described migrating NuCorder’s backend from Java to Go with AI as a tool . This time, I want to step back and ask the real question: which architecture best fits each phase of a project’s life cycle, and why?
What are we actually talking about?¶
To make the comparison easier, let’s use the same fictional application in all three cases: an online shop made up of a catalogue, orders, and a payment system. Only its organisation changes.

Monolith: one box¶
Let’s start with the monolith. Despite the imposing name, the idea is quite simple: the whole application is built and deployed as a single unit.
In our shop, catalogue management, orders, and payments live in the same application. Their code may be separated internally, but they are tested and released together.
You can picture the monolith as one box containing several mechanisms. To change one of them, you generally have to rebuild and redeploy the entire box.
This says nothing, by itself, about code quality: a monolith can be very well structured, just as it can become a plate of spaghetti code that is hard to evolve. Monolith describes how it is assembled and deployed, not its state of health.

Projects often start as monoliths¶
Most projects start this way naturally: one project to run, one environment to reproduce, and one deployment.
A monolith is not necessarily temporary: the core applications of GitHub and GitLab still rely on large Rails monoliths that are more than a decade old, even though they are surrounded by other services. The question is when their limitations cost more than the complexity required to change them.
Modular monolith: one box, several compartments¶
A modular monolith keeps the principle of a single application, but organises its internals around explicit boundaries.
Our catalogue, orders, and payments are still built and deployed together. However, each forms a separate module responsible for its own logic. A module does not directly poke around in its neighbour’s mechanisms; it goes through the exposed entry points.
Visually, it is still one box, but one made of clearly defined compartments. It keeps much of the operational simplicity of a monolith while preventing, by construction, all its functions from gradually blending together.
This separation is not only about the file tree: it must also be reflected in dependencies, interfaces, and data ownership. Without boundaries that are actually respected, a “modular” monolith quickly becomes just a monolith again.
In practice, these boundaries are sometimes blurry and require real design work. You have to decide which module owns each responsibility, which direction dependencies flow, and how far data can be shared. That can mean rethinking the data model or how the ORM represents relationships: an association that is convenient in code can also couple two modules that are meant to remain separate.

Microservices: several connected boxes¶
In a microservices architecture, our shop is no longer shipped as a single unit. The catalogue, orders, and payments become separate applications that are independently deployable and communicate over the network.
Each service has a clearly defined responsibility and exposes a way to communicate with others, usually through a synchronous API—or by exchanging asynchronous messages. The order service can ask the catalogue whether a product is available, then send a payment request to the relevant service.
The API gateway¶
This organisation is also often accompanied by an API gateway: a common entry point that routes requests and hides internal APIs, at the cost of an additional layer to configure and operate.
You get several specialised boxes instead of one versatile box; each can be changed, restarted, or scaled without necessarily touching the others.
The word “micro” is misleading, by the way: it does not define a precise size. The important idea is less about making very small services than about finding boundaries that let them evolve with enough autonomy.

Presented in this order, these architectures can look like a natural progression: an application would start monolithic, become modular, then eventually be split into microservices. That is not an inevitable path. Every step adds boundaries, but also costs that must be justified by a real need.
In practice, each architecture moves complexity to a different place. That is precisely what makes the choice interesting.
Three projects, three answers¶
These definitions provide reference points, but they are not enough on their own to choose an architecture. To make the differences more concrete, here are three projects I have worked on, with very different constraints and scales.
STB Tester: a monolith was simply enough¶
At Canal+, I turned a local Python prototype into STB Tester: an internal Node.js web application used to run test campaigns on TV set-top boxes and produce a daily report. The scope was limited, the project was handled by a single developer, and the workload was entirely predictable.
In that context, the monolith was not a first step toward a more serious architecture: it was the right architecture. Splitting the application into several services would have added deployments, network communication, and monitoring without providing any autonomy or resilience the project actually needed.
NuCorder: a modular monolith, out of necessity rather than doctrine¶
NuCorder has a much broader functional scope, but not a larger team—I am still its main technical contributor. I had gradually created several Go services around the existing Java monolith, more as an accumulation of local decisions than as the result of deliberate architectural design.
The result was predictable: several applications to build, deploy, monitor, and maintain, without a team able to absorb that cost. During the backend rewrite, I consolidated everything that had no reason to live separately into a modular monolith, while keeping components with genuinely different constraints isolated.
Molotov TV: microservices at the right scale¶
At Molotov, the context was completely different. Several squads worked on the platform in parallel, with distinct cadences and scopes, and very different load profiles across components.
Microservices made it possible to isolate those components, scale them independently, and give teams more autonomy. The additional cost was therefore justified by real organisational, scalability, and availability requirements. I saw this first-hand when I accidentally broke the bookmarks service: the incident remained local, the rest of the platform kept working, and I could quickly roll back to the previous version.
The risk of over-splitting¶
A microservices architecture does not guarantee good boundaries. At Molotov, some changes ended up requiring the same developer to work across three or four services: the network boundary had not removed coupling, it had only made it more expensive to cross.
This is sometimes called a distributed monolith: a set of separately operated applications that still retain the coupling their decomposition was meant to remove.
Product size alone is not enough to choose an architecture. You also need to look at team size, differing load profiles, availability requirements, and the ability to operate the system.
How to decide in practice¶
I do not choose an architecture based on needs the product may have one day, but on constraints the team already faces—or whose arrival is sufficiently certain.

How many people will actually work on the project?¶
One person or a small team has little to gain from splitting the system into multiple services, while several autonomous teams may need distinct development and deployment cycles. If a change regularly forces several teams to coordinate, the chosen boundaries may be wrong—regardless of the number of services.
Will this split genuinely let two teams move forward independently?
Do some parts need to evolve or be deployed separately?¶
A service becomes worthwhile when a domain genuinely evolves at its own pace: more frequent releases, particular security constraints, specialised technology, or clearly assigned ownership. If everything is always tested and released together, multiple services may only produce a distributed deployment.
Does this component need to be deployed without coordinating with the rest of the application?
Are load or availability constraints different?¶
Overall load does not justify microservices on its own: a monolith can be replicated across several machines or given more resources on the same machine. Extraction becomes more relevant when a component has a genuinely distinct profile: a resource-hungry process, an API used far more than the rest, or a critical function that must remain available despite another domain failing.
Do I need to scale or isolate this component independently, or can I simply replicate the whole application?
Are functional boundaries stable enough?¶
As long as the domain evolves quickly, responsibilities change and features cross boundaries imagined only a few months earlier. In a monolith, moving a responsibility is mainly a refactoring exercise; between two services, it may also require moving data and temporarily maintaining several compatible versions.
Does this boundary come from our understanding of the domain, or only from the current organisation of the code?
Can the team and company absorb the cost of a distributed system?¶
Creating another service does not only mean a few files and another deployment: it also needs hosting, monitoring, and a plan for when its dependencies stop responding. The cost is paid twice, in human time and on the infrastructure bill. These costs may look modest individually, but multiplied across ten, twenty, or fifty services, they become a central architectural concern.
Who will diagnose this service when it goes down on a Sunday morning?
Prime Video: when distribution costs too much¶
In 2023, a Prime Video team explained that it had brought the distributed components of an audio and video quality analysis tool into a single application. Orchestration transitions and data exchanges in the first serverless version had become expensive as load grew.
By bringing the processing together in a single process, deployed with ECS on EC2 instances, the team reported reducing its costs by more than 90% while increasing system capacity. Prime Video did not abandon microservices: it corrected the over-splitting of one specific subsystem.
My decision rule¶
| Situation | Reasonable starting choice |
|---|---|
| Limited internal tool, maintained by one person | Simple monolith |
| Product expected to evolve, small team | Modular monolith |
| Stable domain with a distinct constraint | Targeted service extraction |
| Several genuinely autonomous teams | Microservices may be appropriate |
| Uncertain boundaries or immature operations | Keep the system monolithic |
Without a specific reason to distribute the system, I now prefer to start with a monolith, then evolve toward modular. Not because the monolith is inherently superior, but because it keeps more decisions easy to change.
I do not extract a service because a domain may one day need to be scaled separately, isolated, or given to another team. I extract it when that need exists, its boundary is sufficiently understood, and the expected gain exceeds the cost of operating it.
Real-world architectures are rarely binary. They can combine a monolith for the core domain, a few separate services for specialised processing, and external SaaS components.
What I’ve learned¶
None of these architectures is the inevitable next step for a project: STB Tester, NuCorder, and Molotov needed different answers because their team structures, load profiles, and availability requirements were markedly different.
With NuCorder, I reproduced an architecture suited to a much larger context without facing the same problems or having the same team size. A modular monolith is now a better compromise for its core domain, while leaving components with truly specific constraints separate.
Architecture is a compromise to revisit as constraints change. The right architecture is not the one with the most services, but the one whose complexity the team can handle today without unnecessarily closing off tomorrow’s options.
Is your architecture starting to cost too much?¶
Has your monolith become hard to evolve? Have your microservices accumulated to the point where a feature requires changes in several of them? Are you considering a migration without knowing whether the problem lies in the architecture, the code, or the organisation?
I help teams understand their real constraints, identify what should be separated or brought back together, and define a path that fits their context—without imposing a target architecture or a new technology stack.