Divide and conquer is the old truth that lies at the core of the microservices architectural concept. For a long time, this approach was praised and glorified as an alternative to the old-school monolith idea.
With time, people started raising concerns about whether it's always the right way to go. But even now, microservices are widely considered to be the correct solution to all architectural problems. After all, it's easier to work with a bunch of smaller services than with one big monolith, right?
What's the difference between monolith and microservices architecture?
Regardless of your product's maturity, the architectural decisions may have a great impact on its development and overall performance. It doesn't matter whether it's about making the right call at the beginning of the implementation process or deciding to switch after the application is ready for production. First, let's discuss the difference between the two approaches.
Let's start chronologically with the monolith architecture. A monolithic application is a single, self-contained, and independent program that can perform all the necessary business operations. All the logic is coupled within one code base, which results in a single, deployable software unit. This also means that every change, even the slightest, requires rebuilding and deploying the entire application.
Microservices architecture separates the main parts of the business logic into many independent services, each responsible only for a specific part of the whole application. Each service is implemented, tested, and deployed separately and can exist in an independent repository or even use a different technological stack.
Important note: monorepo does not necessarily mean monolith. You can have as many microservices kept in one repository as you want, although you'd be missing out on a few advantages of microservices right away.
Here's agraphical comparison of monolith vs microservices architecture.
Bold claims
No matter which article you read on the differences between microservices and monoliths, the main advantages of the former are mostly the same. Some of those claims are strictly subjective and often contrast well-managed microservices to poorly managed monolith code bases. After all, the long time needed to check PRs is not necessarily a result of the architectural choice. The same goes for developers' happiness.
For the sake of this article, I'll assume that we work with well-managed codebases. Let's examine the claims of microservices' supposed superiority and see whether the reality holds up to that.
Monoliths vs. microservices comparison
Performance
Programmers always aim to write efficient code (well, almost always). You can do your best to optimize the hell of a piece of the code you're working on, which is definitely easier if the code base is smaller and well-encapsulated. Clear point for microservices. You deployed your application only to see that the deployed service is slower than you expected. Why is that so?
There are two types of performance problems to consider here: objective, measurable web performance, and subjective, perceived performance. The former one is what you, as a programmer, have the greatest impact on. Here, you can display your unmatched optimization skills and algorithmic knowledge. The latter, on the other hand, is probably less (or not at all) dependent on you, and this is where monolith can outshine microservices.
Communication over the network always comes with a cost. Every call to a separate service has some latency that may vary depending on the distance between the communicating servers, available bandwidth, or even transmission medium. You can try to keep the requests between your microservices to a minimum, but you can't eliminate them entirely.
In monolithic architecture, there is no network latency as all the functions “live" in the same space and can be called without unnecessary overhead.
Scaling the infrastructure
Sooner or later (business-wise, hopefully sooner), as more and more people use your service, it will have to scale. There is no way around it. But maybe there is just one part of the service that is under the heavy request fire of your user base. So why scale everything if only a part of it requires more juice?
With the microservices architecture, this is a no-brainer. Scale the service that needs scaling and forget about the rest. With the monolith, this is impossible, as there is no way to scale just a part of the whole monolithic unit. In other words, you are forced to scale your whole service.
The thing is, this is not that big of a deal. There are parts of your scaled service that will have a minor impact on the scaled resources, as they didn't need the scaling in the first place. But new resources will be used by your service anyway, so you don't lose anything.
So, in terms of scaling, monoliths come with some overhead, even though it's rather marginal. But there are a few advantages that may outweigh this cost. Primarily, scaling a monolith involves replicating the entire application, which can be simpler than determining how to scale individual microservices based on their specific resource needs.
Another simplification resulting from the monolith architecture is the load balance configuration. Since the singular instance of the monolith can handle all requests, there is no need to figure out which request needs to go to which scaled service.
Technological freedom
Say you went for the microservices architecture, and you want to add a brand-new service to your flock. Do you want to use Python? Sure! Rust? Go for it. Old-school C++? Why not? Since this new service is completely separated from the rest of your herd, when it comes to technology, you can enjoy unmatched freedom of choice.
This sounds great and theoretically opens many doors, but in reality, how feasible is it? In most cases, your team specializes in a certain range of technologies, so there is no gain in imposing a different language that they are not comfortable with. Sure, code written in Rust may be more performant, but only when written by people who know what they are doing.
Also, nowadays, with webassembly, you're no longer restricted to just one language. So, if you want to mix and play, a monolith is not really a blocker.
Deployment
Finally, the time has come (even if it’s Friday); we want to deploy our application. Most articles point out how easy it is when we can simply deploy an independent service, not worrying about the rest of the system. Additionally, when introducing small changes, we can deploy only the affected code without rebuilding the entire application.
This sounds great in theory, but what if the implemented changes impact more than one service? No matter how well the business logic is separated, sometimes the change is so profound it may require adjustments across more parts of the system.
In that case, it's not only about deploying affected services, but also ensuring careful coordination of the deployment order so as not to allow any downtime. After all, independent microservices still need to communicate. Or even worse, unsynchronized deployment can lead to mismatched data being added to our database, putting the integrity of our data at serious risk.
Database issues
And speaking of databases. Having multiple microservices use the same database results in new dilemmas, like, for instance, choosing the “master” service to apply migrations. To avoid that, each service can have its own dedicated database designed for the business logic handled by that part of the system. Which, again, generates problems like data synchronization, complicated rollbacks, and so on.
Those problems go away in the monolithic architecture. The process typically involves packaging and deploying a single unit without worrying about orchestration and version management. And the same applies in the case of rollbacks. Suppose the newly deployed version introduced a previously undetected bug. In that case, we may have to revert to the previous working version, which again will be easier in the case of a monolithic application.
This is, however, kind of a double-edged sword. On one hand, you introduce changes and deploy them once. But on the other hand, you have to update everything at once. Let's consider a typical scenario. Say you want to update the React version of your application. You have to update each and every component affected by the version change. There is no way of splitting the work into batches as we have to deploy everything at once.
The same goes for all configuration files for eslint, TypeScript, etc. Each microservice requires its own version of that file that has to be maintained to keep it somewhat consistent with the rest of the services. Monolith allows you to manage just one configuration for the whole system, which means less freedom.
Team building
In a typical microservices development team, you can easily allocate developers (even whole teams) to work only on specific services. They don't even have to know what the others are doing. Finally, developers can really focus on specific parts and do not need to waste their precious brainpower conceptualizing the whole project. Just think of the productivity gain over monolith teams.
The truth is that this argument, although often associated with microservices, can be just as easily applied to monolithic architecture (especially modular monoliths, but more on that later).
In monoliths, developers don't have to understand the whole code base. They don't even have to know about all the functionalities. You can have people who specialize in some parts and may only have a vague idea about other parts of the system. After all, you will need the same a similar number of people working on the project, regardless of the architectural choice.
What's more, in monoliths, there is a greater chance for good communication between teams. In the end, you can just as easily separate teams in both types of architecture. Their productivity and communication depend greatly on other factors.
Here are some possible gains:
Easier knowledge sharing
Centralized deployment and release management
Easier collaboration on cross-cutting concerns
Simplified communication channels
In terms of team building, outsourcing simplicity is the main gain resulting from microservices. You can grant access to a single repo without sharing the whole code base. That is if you keep your microservices in separate repositories, which is, as mentioned previously, not required.
The holy grail
We can clearly see that microservices architecture is not the panacea it's often advertised to be. But there is no denying that there are some gains from it, either. Maybe there is a solution that won't force us to sacrifice a good chunk of benefits?
Enter modular monolith.
A modular monolith is basically a monolith with very well-defined and encapsulated business logic in the form of modules (thus the alternative name "modulith").
This approach has all the benefits of a monolith application, like simpler deployment and rollbacks, no communication overhead, straightforward scaling, and so on. Additionally, encapsulating business logic into modules helps maintain a level of isolation between different parts of the system. This makes the points made in the team-building section easier to achieve.
Despite the monolithic nature of this architecture, modular monolith offers a range of benefits that are mostly associated with microservices, such as:
Isolation of concerns - separating business logic into smaller parts that are easier to understand and can be developed separately.
Team independence - each module can be developed by different teams, minimizing the need to conceptualize the whole system.
Architectural freedom - each module can follow a different code structure and practices, including dedicated configuration files.
This variation extension of monolithic architecture, as opposed to traditional monoliths, requires a bit more conceptual work to divide the application into separate encapsulated modules. This, however, may greatly improve code reusability, dependency organization, and dividing responsibilities between developers or even whole teams.
We just scratched the vast surface of modular monolith architecture to get an idea of what the alternative approach might be. The topic is much broader and deserves an article on its own.
Closing thoughts
As you can see, microservices promise quite a lot, and while it's mostly true, it's not exclusive to this architecture.
Monoliths, if implemented properly, can solve many of the discussed problems, simplifying other aspects of development and deployment simultaneously.
Choosing the right architecture is not a trivial decision and definitely depends on the specific case. Both can boost your development process, and neither will prevent you from shooting yourself in the foot if implemented incorrectly.
If you're a startup at the beginning of the journey, the simplicity of monolith architecture can greatly boost the delivery of your product and minimize overhead costs. With time, as your business grows, it may be beneficial to transition to microservices. Starting with a modular monolith may delay the necessity of this transition.
If you feel like microservices are the right choice for you, or you have already made that decision, use them wisely and keep it simple. Creating heaps of services just for the sake of it may cause you quite a headache in the future.
With that in mind, be careful not to divide and be conquered.
Graphics have been inspired by previous works available at Atlassian, Octopus Deploy, and Medium websites.
A Full Stack Developer specializing in frontend, he masters various frameworks and actively engages in Open Source projects. Renowned for his ability to fuel software development with his love for coffee, he transforms caffeine into code. With a strong background in JavaScript, React, and other web technologies, he has developed solutions from facial recognition systems to deepfake analysis. His interests range from collecting Legos to snowboarding and boxing.
Schedule a consultation to assess your software architecture