Monolith vs Microservices
Leaders of many new companies and teams wonder, "should we build microservices, or a single monolith"? Here, I'll give you some of the pros of each, to tell you what I would pick if I were starting today.
What are monoliths and microservices?
A monolith is when all of the business logic for an app is run as a single process. That may mean a single operating system process, a single container, or however your organization deploys code. It also commonly means that most of the code (excluding dependencies) for the app is contained in a single repository.
A microservice architecture is when the code for an app is split up among different processes, possibly on different machines or in different containers. Each service has a specific job. For example, a single social media app may have separate services to handle posting, feed generation, friend lists, notifications, and payments. Most commonly, each of those services will also exist in their own code repository, and be owned by separate teams.
The most common situation is one service to one repository, so we'll be operating under that assumption in this article.
Reasons to build a monolith
Monoliths are easier for small teams
Keeping all of your code in a single repository offers several advantages, especially for a small team. One codebase to synchronize, one CI/CD pipeline, and one dependency list all make managing code easier.
However, a single repository can start to work against larger organizations. As more parallel development work is done in a single codebase, stalls due to interdependent efforts or merge conflicts become more common. That's why it's important to also consider the growth trajectory of your organization.
Monoliths are more efficient
Having all of the business logic inside a single app means fewer API calls, fewer resources spent serializing and unserializing data, and less waiting for network traffic. Overall, that will translate to faster response times and a lower total cloud cost for your app.
Monoliths require less infrastructure
Secure microservice architectures require compute clusters, firewalls, pubsub queues, configuration management, image repositories, and other infrastructure. That's a lot of effort and complexity to take ownership of. No wonder many companies have dedicated devops teams!
If you build a monolith instead, much of that infrastructure isn't necessary, which removes a large amount of overhead for your team.
Monoliths are easier to change later
It is easier to move from a monolith to microservices, if the change needs to be made. Additionally, running as a monolith initially will allow your team to collect valuable information about which parts of the app are a bottleneck, which parts are stateful vs stateless, and which parts aren't needed all the time, which will lead to better decisions about how to structure the microservices in your organization.
The change from monolith to microservice doesn't need to happen all at once, either. A single high-demand portion of the app can be broken out to allow for flexible scaling to meet the demand, for instance. In fact, that approach is what I recommend. Adapting your strategy to meet a specific and measured need is far better than imposing a strategy, and trying to make it fit your need.
Reasons to build microservices
Microservices are great for large or rapidly growing organizations
Microservice architectures are great for large companies. Many developers working on a single repository will eventually cause code interdependence, waiting for work to complete, and merge conflicts. Microservices address that issue by allowing smaller teams of developers to own one small service that does a specific job within a company.
Microservices can be easier to shard
Since microservices have specified jobs within an app, they are usually easier to shard (run multiple copies in parallel). Sharding has several benefits, including allowing seamless rollouts of new code, and scaling to meet demand.
In contrast, monoliths can be difficult to shard, because the same code may need to handle stateful and stateless interactions. Any stateful interactions that aren't shared across the monolith shards would cause inconsistent user experience at best, and race conditions leading to catastrophic system failures at worst.
Microservices can be designed around that problem. Since a microservice is designed to do one task, developers can either build it to be stateless, or more easily find a way to synchronize the simplified state across shards of the service.
Microservices are more resilient
Since microservices are split by concern, each service in a microservice architecture is comprises less code than a monolith. This often means that, if one microservice fails, it will not cause failures in the whole app in the same way that it would for a monolith.
For instance, if the friend request service of a social media app was down, then users may still be able to post, or look at their feed. Many users may not even notice anything was wrong at all, because the incident was contained to the friend request service.
This resilience doesn't come for free; each microservice has to have a built-in tolerance to other services failing. However, it is often easier to engineer microservices to be fault-tolerant than it is for a monolith.
Best practices
Try not to over-engineer from the beginning
Software development is a highly iterative process. If you're starting from zero, you likely don't know how your business is going to grow and change, and what technical needs that will place on your software. Try not to pigeon-hole your team by committing to a certain architecture.
This means making sure your code is divided into packages with separate concerns, even in a monolith. And, it means only splitting out code into a microservice when you have a tangible reason to do so1.
Measure twice, cut once
When you do split out microservices, make sure to carefully measure the traffic your app experiences in production, and that creating a new service will solve a specific problem.
For example, the team building a monolithic social media app may notice that the app's bandwidth is dominated by serving feeds, to the point that users trying to add friends are experiencing bugs or slow service. This would be a good scenario to split the feed-serving code into a separate microservice, while leaving other functionality as part of the main service. The new feed microservice can scale independently to meet demand without causing degraded functionality for the other parts of the app.
Build resilient infrastructure
When splitting out microservices, it's important to ensure that both they and your infrastructure are fault-tolerant. If you don't do this step, you're just adding more complexity to your app and setting your team up for failure.
That means making smart use of pubsub, dead-letter queues, database transactions, REST APIs, and idempotent operations to ensure that a hiccup doesn't cause the house of cards to collapse. If you're planning on moving to a microservice and you don't have at least one person on your team comfortable with each of those technologies, you should hire someone who is.
What would I pick?
If you're starting fresh, my recommendation would be to start with a monolith. Within that monolith, organize your code into modules with well-defined roles. Resist the urge to over-engineer by splitting into microservices before you know where the pain points are. When a specific, measurable need does arise, split that code into a microservice. Your architecture should serve your team, not the other way around.