You (probably) don't want microservices

- Architecture
- AWS
- DevOps
I've long been a believer that one of the fastest ways to learn is to do so from people better than myself. At school I had teachers - people who understood the topics well. When I practice BJJ I ask my coach questions and I make sure to train (at least sometimes) with fellow practitioners who are further long their journey than me. When it comes to photography, I look to the greats, and ask myself what it would take for me to produce such works of art. So, when it comes to building web applications, we should look at what big tech companies do, right? After all, they genuinely do have some of the best talent in the world.
Twenty years ago, when you deployed your website, you uploaded a bunch of files to a server and called it a day, but the web has evolved (a lot) since then. More people are online, and more data is being generated, processed and transferred across the internet than ever. Now microservices are the way that pros ship web apps, and it's a trend that has trickled down from the largest tech corporations in the world into development teams of all sizes. After all, if it's good enough for the big guys, then it's good enough for us to, right?
What are microservices?
Microservice architected applications are considered by many to be the 'modern way of deploying large web applications', or the 'successor' of monolithic applications - applications which exist as a single, large, unified application, bundled up and deployed as a single unit. But to understand why microservices are all the rage these days, we first have to understand what kind of problems they solve and what benefits they bring.
- Independent scalability - in large applications consisting of multiple components, some components may have higher demand than others. Microservices allow provisioning more capacity to certain services.
- Fault isolation & resilience - When a particular services goes offline, other services are not affected
- Mixing technologies - Some services may benefit from being written in different languages. A heavy I/O service can be written in NodeJS, whereas a computationally intensive service could be written in Rust.
- Delivery speed - deploying a change requires only testing, compiling and distributing the affected service
- Data privacy - Since each microservice owns it's own data store, data that is subject to strict regulatory requirements can be kept separate from other data.
The benefits of microservices are real. Microservice architectures are what allow tech giants like Meta, Amazon and Google to operate. Many enterprise businesses simply wouldn't work as a monolith.
So why do you probably not want microservices?
The answer is very simple - microservices solve problems that you probably don't have.
Microservices solve hyper-scale problems
Any engineering decisions should be justified, and the reality is that a lot of startups are building paper airplanes, and not A380's, but still want to follow Airbus' engineering practices. Just because Airbus is great at making planes, it doesn't mean you also need a huge factory with it's own runway when making your paper airplane.
Microservices introduce huge levels of complexity that a lot of teams aren't prepared for or don't foresee. In fact, microservices introduce more engineering complexities than they solve. The reasons that the hyperscalers still use them is because the problems they do solve, they solve well and there are few alternative approaches to solving them. The complexities introduced are considered a considered acceptable trade offs.
Microservices have to be executed extremely well or they fall apart
"Microservices... it's an architecture that cannot be architected except by those who already know how to architect it" Captain Jack Sparrow (probably)
There are a set of core principles of building with microservices that have to be extremely well understood and actively enforced at all times otherwise chaos will ensue
Core Design Principles:
- Single Responsibility : Each microservice focuses on one well-defined business capability or job (e.g., user authentication, payment processing, or order management).
- Loose Coupling: Services interact through well-defined API contracts (such as REST, gRPC, or asynchronous events) so that a change in one service does not break another.
- High Cohesion: All logic and functionality grouped inside a single microservice are closely related to its specific domain purpose.
- Decentralized Data Management (Database-per-Service): Each microservice owns and manages its own private database or data storage, preventing direct database sharing across services.
- Independent Deployability: Services can be built, tested, updated, and deployed independently without coordinating a full-system release.
- Fault Isolation and Resilience: Failures in one service (handled via circuit breakers, retries, and fallbacks) are isolated to prevent the entire application from crashing.
- Observability and Monitoring: Distributed systems require centralized logging, health checks, and tracing to track requests and debug issues across multiple services.
There's an undertone that all these principles have in common (except the final one) - that is isolation. Each service really knows nothing about other services except the small API contracts they hold with each other.
These principles are far easier to naturally follow when different isolated teams are working on different services (or small groups of services), and far harder to naturally follow with a single large team spread across all services, who must follow these principles religiously, requiring a huge amount of precision planning, discipline and zero tolerance for corner cutting with every engineering decision made across the lifetime of the project - and we all know how things end up actually working in real life.
Microservices reflect organisational structure
The mistake many companies make is using microservices to split up a large application into smaller applications, thinking that microservices are a good fit for huge, complicated applications. This is often not the correct approach, since these smaller applications are inherently tightly coupled, and a core principle of microservices is loose coupling.
The correct approach is that microservices are a good fit for huge organisations serving huge amounts of users. Microservices work best in large companies with isolated teams who are genuinely working on separate pieces of software which form a part of a complete product. At huge scale with properly isolated teams, it's hard for different teams to avoid getting in each others way when working on totally different parts of a product which runs as a monolithic application. It's also far easier to enforce loose coupling and isolation when you have different people working on different microservices. When you have a small or medium sized development team working across different services, it's extremely easy to break the isolation principle and end up in distributed-monolith spaghetti pile.
Microservices introduce an entirely new league of engineering problems
Every engineering team wants to build the plane that can fly around the world, but not every team is equipped to deal with the engineering complexities that are associated with that (or even want to deal with those complexities).
- Got a nice relational database with enforced foreign key constraints and ACID compliance?
- Now your database is distributed - no foreign key enforcement, and data flying across the network that can end up inconsistent
- Got a simple cross-module function that you'd like to call?
- Now your function is a network request, which:
- needs to be authenticated
- can fail (and needs to be retried)
- adds latency
- doesn't have language level typing
- Did the downstream service return a 500 error?
- Now you've got 2 errors
- Now your function is a network request, which:
- Got a simple cross-module function signature that you'd like to update?
- Now you've got a coordinated effort across multiple services
- Got a nice place to view your logs/metrics/traces?
- Now your observability is distributed
- Have fun with distributed tracing
- Now your observability is distributed
- Got a nice lean development team?
- Now you need an ops team too
So when do you actually want microservices?
Treat microservices as something to avoid by default. Even if you're building a huge application, or something that you plan to scale vastly in the future, you should start as a monolith in 99% of cases. Even tech giants like Instagram and Slack started off as monoliths until they were massive, and to some degree still operate monolithically.
That being said, here are some green flags to look out for that might indicate microservices are for you:
- Your monolith has been around for a long time already and it's design is well understood and stable.
- You have a large number of engineers (50+) operating in multiple teams and each team specialises in a certain part of your monolith.
- You have people in your team who have built successful microservice architectures before, or you are willing to bring on members who have.
- You have hundreds of thousands or millions of users
- You've identified 'hot modules' or areas of your application which have significantly more demand than others.
- You're willing to accept the huge operational burden of microservices
And here are some red flags:
- You're designing a new project from scratch.
- Your development team is a small, medium or large team (even if it's split into subteams) that know and communicate with each other frequently and work on the same overall project.
- You would you expect any developer, or development team to be responsible for, or have knowledge of the inner workings of more than a few different microservices.
- You just want to be 'scalable'.
- You have a highly related data model that would end up being split over multiple services.
- You plan to have 'isolated services', but practically they still depend on each other.
Signs your existing microservice-architected application should probably be a monolith
- You have more microservices than engineers
- You have one or two teams that look after all microservices
- You have a 'shared library' which all services import to 'decrease code duplication'
- You have data consistency nightmares, relying on distributed transactions (or none at all) leaving you with constant data fixes.
- You have, or are subject to have, cascading outages where a microservice cannot operate properly when one or more downstream services are not functioning.
- You experience problems finding the root cause of an error or outage, as each service throws their own exceptions in response to downstream exceptions.
- You're executing queries on a microservice database from another service or application, bypassing the API layer.
- Developers cannot work locally because they need to run too many containers
Thank you for making it to the end!