Monolith? Seriously, Roberto? But it's 2024!
Ugh, I can already see some colleagues raising their eyebrows. I know, I know. In the development world, it seems like if you don't talk about microservices, Kubernetes, Serverless, and the latest distributed architecture trickery, you're stuck in the Stone Age. And look, don't get me wrong. I've played around with microservices, I've seen projects that use them wonderfully, and I understand their appeal. But if I'm being honest, most of the time, for the types of projects I work on – and I'm telling you this from the trenches – a well-thought-out monolith has given us more joy than headaches.
It's like that fever you get from buying the most professional camera on the market when all you're going to do is take pictures of your cat. Do you need a film crew to record your nephew's birthday? That's what I mean. Sometimes, the most powerful tool isn't the right one, but the one that best suits your actual needs. And in my experience, the reality of many projects isn't that of Google or Netflix.
My experience with the "micro-fever"
I've been on teams where the word 'microservice' was almost a dogma. They talked about horizontal scalability, team independence, the ability to use different technologies for each service... and all that sounds like heavenly music, right? But practice, my friends, is another thing. I remember one project in particular where we started with the premise that everything would be microservices. We were a team of seven people. A medium-sized project, a SaaS for internal company management. What was a single application with a database was divided into five or six "services" from the beginning.
At first, everything was excitement. But soon the challenges began. Deployment became an odyssey. A small change in a cross-cutting entity, for example, in authentication, meant touching two or three repositories, deploying two or three different services, coordinating versions. Logs, which used to be relatively easy to follow, were now scattered everywhere. Debugging an error involving two services? Ugh! Hours of tracing, understanding asynchronous communication, retries, network failures between services that were on the same virtual machine. People were starting to get frustrated, the pace was slowing down. The famous "independence" translated into "dependence on a bunch of smaller and more complex things".
And let's not even talk about the cost. Not just the monetary cost of more infrastructure (databases for each service, messaging services, load balancers...), but the cognitive cost. Every new developer who joined took twice as long to understand the overall architecture, to know where things were and how they communicated. The "development speed" we had been promised was diluted in operational complexity.
Why I think a "well-made monolith" is important
Look, at the end of the day, what we're looking for is to build software that works, that is maintainable, and that can be delivered quickly and with quality. And that's where the monolith, well-designed, shines for many scenarios. When I talk about a "well-structured monolith", I'm not referring to the "big ball of mud" of the past, that untamable monster where everything was mixed together without sense. I'm referring to a monolith where responsibilities are clear, modules are well separated, with well-defined interfaces, and where internal cohesion is high and coupling between modules is low.
Think of it this way: in a monolith, communication between "modules" (or functional domains, if you want to call them that) is simply a function or class call in memory. Fast, direct, no network latency, no serialization/deserialization problems, no complexity of asynchronous messaging or distributed failure points. Deployment is a unit. Logs are in one place. Debugging is much simpler. For small or medium-sized teams, this translates directly into:
- Less operational complexity: Fewer things to monitor, fewer services to deploy, less infrastructure to maintain.
- Greater development speed: Developers understand the system faster, changes are easier to propagate and test.
- Reduced costs: Both in infrastructure and in hours of work dedicated to managing complexity.
- Team cohesion: Instead of spreading knowledge and responsibility to an extreme, the team can have a more global vision and collaborate more smoothly.
What I've learned: How to make a happy monolith
The key isn't to avoid complexity, but to manage it. And a well-structured monolith is a fantastic way to manage the inherent complexity of software. Here are some of my "golden rules" for building a monolith that doesn't become your worst nightmare:
1. Internal modularity from day one: Even if it's a single project, treat it as if it were composed of logical mini-applications. Define clear boundaries between domains (Users, Orders, Products, Notifications...). Use patterns like "hexagonal architecture" or "DDD" (Domain-Driven Design) to keep these layers well separated and avoid unwanted coupling.
2. Clear interfaces: If a module needs to interact with another, do so through a well-defined interface, as if it were a contract. This gives you flexibility to change internal implementations without affecting other modules.
3. Robust testing: Having everything in one place makes it easier to write integration and end-to-end tests that give you confidence. Don't skimp here.
4. "Vertical Slicing" or "Feature Slices": Instead of organizing your code by technical layers (controllers, services, repositories), organize it by functionality or domains. Thus, everything related to "Orders" (its controller, its service, its repository, its models) is together. This facilitates understanding and refactoring.
5. Refactor without fear: The monolith allows you to refactor large parts of the application with more confidence, as dependencies are easier to identify and manage within a single codebase.
6. What if we grow a lot?: The beauty of the modular monolith is that, if one day you really need to scale a part of the application independently (and only if you really need it!), you can "extract" that module and turn it into a microservice with much less friction, because it is already designed to be a cohesive unit. This is known as the "Strangler Fig" pattern.
Final thoughts: The right tool for the job
In the end, the choice between monolith and microservices isn't a matter of "modern vs. outdated" or "good vs. bad". It's a matter of suitability. For small, medium, or even large projects that don't have the extreme scalability needs or the complexity of team organization that companies like Amazon would have, the well-structured monolith is, in my humble opinion, the most sensible, efficient, and less painful option.
It has saved me countless hours of frustration, money on infrastructure, and has allowed my teams to focus on what really matters: delivering value to the user. So the next time someone tells you that the monolith is dead, tell them no, it's alive and well, and well cared for, it's a workhorse that will take you far without falling off its back.
What do you think? Have you had similar experiences? I'd love to read your comments!