When starting a new project, architecture is one of the defining decisions you want to make. If you have followed the hype, microservices seems to be the young and hip choice. But you should not rule out the monolith to quickly. Both architectures have a profound impact on not only the code but also on the team(s) writing it. In this article, I’m going to cover all aspects you need to consider when making a choice, not just on the code part. I will also include the organizational, infrastructure, deployment, and security aspects. Please remember to check out the checklist at the bottom that can help you choose.
The monolith is the traditionel way of building systems and has been used for many years.
A system built using monolithic architecture is a single large system with many responsibilities. It created as one code base and is deployed in one go if the system consists of a frontend and backend; they are usually deployed together.
The internals of a monolith can take many forms. At one extreme, we have Big Ball of Mud, which is a system that lacks any internal architecture and is very unstructured. At the other extreme is a system implemented following philosophies like Clean Architecture and Domain-Driven Design.
Usually a monolith has a single database that backs the persistency requirements of the application.
Monolithic architecture style tells us nothing about the internal quality of the system. It only tells us that it is a single code base that is deployed as one pice. Even if crappy legacy systems are usually monoliths, you should not assume that it is the same for all systems.
With a microservice architecture, the application is split into many smaller parts that communicate with each other to create an application using composition. Usually, the individual services communicate with each other using HTTP.
Each individual service are developed and deployed individually. They all have a single responsibilty that is centered around a business requirement.
Since each service is decoupled from the other services they don’t and often doesn’t share a common database.
Because each service is deployed independently of the others and they communicate across the network, errors are bound to happen so designing for failure is a key requirement when building microservices.
Each service is independent of other services so there are no requirements to use the same technology in every service. It could easily be that most services are built-in .NET but if a service needs to do data analysis, Python could be a better choice for that particular service.
If built correctly, the individual services can scale independently of each other, and specialized hardware needs can easily be accommodated by deploying only the services needing it on different hardware.
Monolithic architectures are the classical way to build application and has existed always.
The idea to split a system into multiple smaller services seems to originate with Peter Rodgers in a research project a Hewlett Packard from 1999. The problem they tried to solve was:
As software grows large a number of problems surface:
The actual term “microservice” seems to be coined at a conference in 2007 and the standardize the term in 2012.
In February 2020, the Cloud Microservices Market Research Report predicted that the global microservice architecture market size will increase at a CAGR of 21.37% from 2019 to 2026 and reach $3.1 billion by 2026
Microservices are an area that is evolving rapidly. It is the newest of the architectural styles and is developed to target specific problems like development teams size and scalability. But the solutions do come with a cost. As Martin Folwer notices, there is a microservice premium to pay by going that route.
MonolithFirst: Building microservices are difficult and most projects will be better of starting with a monolithic architecture before evolving to a microservice architecture. The term is coined by Martin Fowler and you can read more here
Microservice: A single part of a larger system. Microservices are independently deployable, are fault-tolerant, and provide a stable interface for other microservices to interface to.
Monolith: An architecture style that bundles a complete system into a single deployable artifact. It is important to remember that it is possible to develop great architecture in a monolith. But it is difficult to scale the amount of developers on a monolith.
Code integration: When a developer starts to develop a new feature, he will make a branch of the existing codebase. When the feature is completed other developers will have made changes to the codebase and the change needs to be integrated with those changes. As we add more and more developers to a codebase, intergrations become more frequent and more error prone since the amount of changes are higher.
Clean Architecture: A concept developed by Robert C. Martin on how to develop software to keep productivity high even as the system grows. You can read more her
Domain-Driven Design: A concept developed by Eric Evans on how to structure different parts of a software system to make sure it is able to support business requirements and support changes to the requirements. A key concept is a ubiquitous language, stating that key components in the software architecture should reflect the concepts and words that are in the business domain being modeled. You can read more about it her.
One of the largest and most publicly documented microservices architecture is Netflix. They are writing extensively about it in their tech blog. An estimate is that they are running around 700 distinct microservices in their setup.
Another example is Amazon
“If you go back to 2001, the Amazon.com retail website was a large architectural monolith,”
Amazon’s Rob Brigham told his audience at Amazon’s re:Invent 2015 conference.
Microservices are at the heart of many of the internet-scale companies today. But it is worth noticing that many of them started out with a monolith and evolved to a microservice architecture over time.
There are many examples of evolving a monolith to a microservice architecture.
Monoliths are the architectural style of choice for a long time. It is easy to develop, test, and deploy. It is also often the starting point for almost all software components. But since the style isn’t novel in any way it serves no bragging right so it is difficult to find case studies.
When developing a monolith the team works on a single shared code-base. It gives a few but critical benefits.
A critical aspect of splitting up a system into a microservice architecture is how to do the splits. If the splits are wrong, changing them in a monolith is cheap. But if boundaries are drawn wrong in a microservice architecture it is expensive to change, because a change requires changes to multiple services, redeployment, and syncing between teams.
[The] issue with starting with microservices is that they only work well if you come up with good, stable boundaries between the services - which is essentially the task of drawing up the right set of BoundedContexts. Any refactoring of functionality between services is much harder than it is in a monolith.
Martin Fowler - https://martinfowler.com/bliki/MonolithFirst.html
When starting a new project we don’t know how successful it will be or which part of it is going to be the most popular. So we need all the flexibility we can get to do changes as fast as possible. A monolith excels in this area. The power of a microservice architecture is not achieved until the software is large enough that many teams need to collaborate building it or the scaling requirement arrives.
With a monolith, we can start the application in a known state and perform end-2-end testing easily because everything is contained in a single deployment. There is no need to manage multiple services and their state.
All communication inside the application is happening within the same server and inside the same process. We don’t need additional security to make sure nobody can eavesdrop on what is going on. This is in contrast to a microservice architecture where we need to secure the communication between services. We would also need to manage identities and permissions of each service to access other services.
The attack surface of a monolith is more uniform and gathered in a single place, where a microservice architecture will have individual attack surfaces for each microservice.
This makes both development, governance, and manintenance of the security aspect much easier in a monolith, because it is centralized.
Because the complete system is deployed as a single component we don’t have any dependencies to manage. Deployment is simpler because it is just one or a few artifacts that need to be copied to the servers.
Since there are only one version to worry about we decrease the risk of breaking a deployment because of missing updated to dependencies.
If we want to scale the development process, we can’t just throw more developers into a team. A lot of research has shown that the maximum team size is around 5 to 12 people. Any more than that they will step on each other trying to do their work.
When there is a single code-base there is a limit to how many people can work on it at the same time without the integration effort griding the process to a halt. As the system reaches a more mature level it is possible to start peeling microservices from the monolith to allow more teams to contribute to the overall system without the need to integrate code changes to the same code-base.
A monolith imposes no strict requirement on the separation of concerns inside the code. If the development team is mature, they can set up their own rules and build a great architecture. But there is a risk that a less mature team will fail to add rules to their development process and end up with a big mess, a big ball of mud.
A Big Ball of Mud is a haphazardly structured, sprawling, sloppy, duct-tape-and-baling-wire, spaghetti-code jungle. These systems show unmistakable signs of unregulated growth, and repeated, expedient repair. Information is shared promiscuously among distant elements of the system, often to the point where nearly all the important information becomes global or duplicated.
A key characteristic of a monolith is that it is built in a single code-base. If for example the monolith is built in C# and we get a requirement to do advanced data analysis which python is a great fit. Then we can’t easily leverage the capability because we are bound to the C# environment.
There are no easy way to integrate new technology into an existing monolith because there are no obvious extension points to hook into.
While the deployment process is simpler with a monolith, we just need to copy a single asset to a server. It is also means that any change requires a full deployment. If you deploy as often as Amazon, every 11th second, then the deployment time and startup time will cause a queue of deployements that we need to wait for, creating a bottleneck in the deployment pipeline.
Since we don’t have clean waterproff separations inside a monolith we must run the complete test suite to verify that the new version can be deployed. This overhead might be a bottleneck compared to smaller and more separated test suites in microservices.
When looking at startup time, it is not just the time from deployment until the application can receive requests. In some cases, for example, .NET and Java have warmup times, where they perform JIT optimizations and caching. Until everything is warmed up the performance of the application will suffer. With a monolith, every part of the application is affected at a deployment. We might even need a special warmup script that exercises the system to make sure its performance is as expected.
Since a monolith is created as one block, a problem in one area might leak into other parts of the application. One example could be a memory problem, it will not just affect the area where it originates, but might bring down the complete application.
You can’t easily run different versions of a feature and make a fallback if a feature shows an unintended bug.
A microservice architecture is fundamentally a distributed system. It solves many of the problems with a monolith, but it also introduce a fair share of issues that needs to be considered.
In a monolith you work with a complete system. Unless the engineering team put in extra effort to define system boundaries, there will not be any strictly bound separation of parts of the system. It might also be difficult to isolate which part of the code that is responsible for a feature.
In a microservice architecture each boundary defines a separate service that are working standalone. There are no larger system that can distract us from understanding the code. It allows the developer to quickly understand the purpose and functionally of a service.
Since each microservice are selfcontained they can be deployed independently of other services. When deploying a new version we can start to redirect traffic to the instance. We can keep the original version running and only redirect part of the traffic to the new version to see if it behaves as intended.
Independent deploys also try to correct the issue that a single monolithic deployment can bring down the whole system.
Each microservice are running on its own stack, independent of other services. It gives us the freedom to implement the service with a different technology. For example, if we have a C# microservice architecture, and we need to add a service that are working with data analyses. Then we might want to leverage python. A microservice architecture allows us to do just that.
We don’t need to implement using a completly new technology stack, if we want to evaluate different technologies like logging frameworks, we can implement it in different services to see how they fair in the production sytem.
Microservices are the most scalable architecture you can imagine. Lets way we have a service that does 3D image manipulation so it needs a powerful GPU to do that. We can deploy just this service on specialized hardware, and the rest of the ssystem can still run on the existing hardware.
If a server is not powerful enough to process incoming requests to a service we can scale it across multiple servers. Which is also a powerful trait of a microservice architecture.
When we scale the amount of developers on a system they need to coordinate a lot. In the Mythical Man Month Frederick Brooks describe how just 50 developers need 1225 channels of communication to coordinate. As we scale the number of developers it quickly becomes not feasible.
Luckely that is one of the areas that microservices excel, because each service is independent, as long as we agree with the other teams on the interface on the surface of the service. Then we don’t need to coordinate on how it is implemented inside the service.
This makes it possible to scale to thousands of developers. It still requires dicipline to make sure we still coordinate on the interfaces. But it is much better than a monolith.
The two most difficult problems in software are naming and caches. Distributes systems come in on a close third in complexity. As soon as we split a system across multiple processes and hosts we introduce a magnitude of complexity.
Debugging a distributed system is a topic in it self. We need to make sure some kind of correlation id is passed between the system to be able to track a request across the whole system. When building a service we must expect that any dependent services are not available and create a fallback mechanism. Depending on the need of the service that can range from just returning an error message to the calling service to do a rollback of a distributed transaction.
Since requests between services are a network call we could get transient errors like timeouts, we need to implement a retry mechanisme.
When going with a microservice architecture you must evalute if the added complexity are worth it compared to the added value.
In a monolith we usually have a single database, so it is easy to use the transaction possibilities in the database to do a rollback if something failed. With microservices each service are running independently of the others and have their own database.
When spanning multiple databases we can’t use the database transactions to ensure consistency. We need to implement distributed transactions. One way to handle it is by using the saga pattern. While it is possible to handle it, the complexity around it increases dramatically.
If we control all our services, setting up an integration test is relatively easy. But when we need to align with other teams it increases complexity. To integration test a service, we need to set up the dependencies to a known state so the tests will run as expected. Since dependent services per definition aren’t under our control we can’t easily control their state.
When we get a new business requirement we usually have to make changes throughout our system. Changes to the frontend, changes to the backing APIs, changes to the database. Depending on the structure and depth of the architecture we might end up with many small changes distributed across our system lanscape. If each service is managed by individual teams the coordination effort to get the new feature implement can get substantial. Compared to a monolith where a single developer is able to make all the changes without the need for coordination.
Since each service needs to be indenpendently deployable each service will have its own deployment process. If a service is depending on other services we might need to have infrastructure in place to make sure the correct versions are available. It creates a highly dynamic and chaotic environment to deploy changes.
As with any good question in computer science, the answer is: “it depends”. Both architectures have advantages and disadvantages. As Martin Fowler proposed in his MonolithFirst principle you will most likely want to start with a monolith and move to a microservice architecture if the need arises. But that is not because a monolith is better, it comes down to understanding the business domain. Without a deep understanding of the domain, it will be a high-risk task to define the boundary of each microservice.
A microservice architecture adds a nontrivial layer of complexity, if we don’t need many of the advantages a microservice architecture provides us, then it will just slow down development.
If you are starting a greenfield project I would strongly suggest starting with a monolith and if needed in the future migrate towards a microservice architecture.
If you are working on an existing system and you need to scale the number of teams you should consider carving out microservices from the monolith. Starting with a microservice is not something I would recommend unless you are building a system for a domain you are an expert in. And even then you should consider going monolithfirst or at least go with a hybrid solution.
Conway’s law states that we are bound to produce an architecture that is a copy of the organization. Keep that in mind when picking your architecture. Even if you on paper target a specific architecture, if your organization is not aligned to it the end result might be a whole lot different.
Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure
M. E. Conway
If you have many small teams, a microservice architecture might be a good fit because they can develop and maintain a service each. But if you have a small team of developers the overhead of a microservice architecture might not be worth it.
The selected architecture has a profound impact on the organizations it is built by. The relationship goes both ways. As we saw above, Conway’s law states that there will be created an alignment between the organization and the architecture.
In a small organization there is no need for a microservice architecture, the overhead is simply to large and the benefits to small.
As the software grows we might need to add more developers. To avoid them getting in each others way we could benefit from carving our a part of the system as a standalone service. In a mature system it should be possible to identify a bounded context within the monolith that is more or less selfcontained. It could provide a good candidate for a new microservice.
It might be that the bounded context is larger than we would like to have in a microservice context, but it will be a transition state, and as the new team starts to work on it, it might end up getting split even further.
Sometimes a microservice system is actually a monolith in discuise. Key traits of a microservice architecture is design for failure, independently deployable.
In some cases, the services are not designed for failure. It could be that calls between services don’t implement adequate error handling. It essentially makes the microservice architecture a distributed monolith, giving us the worst from both worlds.
It could also be that the bounded context boundaries are sliced in a bad way. It can cause changes to cascade across multiple services. When this happens we break the principle of independent deployability and make it hard to deploy new versions without breaking functionality.
A key principle in a microservice architecture is independence. Since each microservice is isolated on many levels from other services it supports independence broadly as you will see in the principles.
Independently deployable: Each microservice can be built and maintained by different teams. Each services needs to be independently deployable to allow the team to release new versions.
Independently upgradable: With fast phased development services will evolve in different cadences. Even if our service depend on another service we don’t want this dependency to force a pause on upgrading our service. It should be possible to handle gracefully within our service. If we couldn’t upgrade it would mean that all bug fixes and other features in the service would be blocked from being delivered.
Independently scalable: Each service have different performance requirements and resource consumption. By the way, a microservice archicture is built we get independent scalability almost for free. Since each service is running separated from other services it should be easy to move it to another server or spin up more instances of the service without.
Improved fault isolation: In a monolith errors can easily end up bringing the whole system down. Part of a microservice architecture is to have each individual part be resilliant to errors. Such a capability doesn’t come for free, it is a difficult aspect to control.
Faster deployments: Because each service is small the deployment time is shorter. It causes the feedback cycle in the deployment pipeline to be shorter. In turn it leads to better productivity for the development team.
Picking between a monolithic or microservice architecture is a choice that will influence how your software will evolve during its lifespan. It is a choice you should put a fair amount of effort into. A wrong choice will give you a lot of extra work that could have been avoided.
Remember that even though a microservice architecture seems like the right choice based on how much hype it receives, it is often not the right choice for a new project. Going with a monolith and evolving into a microservice architecture over time gives you the best of both worlds.