"In the new world, it is not the big fish that eats the small fish, it's the fast fish that eats the slow fish." — Klaus Schwab.
Unfortunately, microservices are the development standard for any big project today. There are many reasons for that.
The main one is that the market has become overloaded with competitors and not only big companies compete with each other but everyone participates in the race. Everyone wants to deliver new features in their products quickly, frequently, and reliably. And the big products have to grow as fast as the small ones. But usually big products are much more complex in terms of communication, deployment, collaboration, maintenance, and so on. As in many other such situations, the solution is the introduction of the additional level of abstraction — decomposition of teams, decoupling, encapsulation of the logic of individual components — some high-level SOLID.
The whole industry is now on the hype of containerization, scrum, DDD. And microservices in some way borrowed ideas from each of them. Businesses want an independent cross-functional team that encapsulates all internal details of their domain and has explicit integration points with other services. And these teams need to be relatively small (two pizza teams) and be able to solve problems quickly but at the same time have all the resources to deliver functionality. The key thing with the microservices view of the world is that services should organize around business capabilities instead of technologies, which is most common in big companies.
"This is the Unix philosophy: Write programs that do one thing and do it well. Write programs that do one thing and do it well." — Doug McIlroy
Individual microservices are only independent and decoupled if they can evolve independently. Isolation is a prerequisite for autonomy. Only when services are isolated can they be fully autonomous and make decisions independently act independently, cooperate, and coordinate with others to solve problems.
This gives each service the freedom to represent its state in any way it wants, and store it in the format that is most suitable. Some services might choose a traditional RDBMS, some a NoSQL database, some a Time-Series database, and some to use an Event Log through techniques such as Event Sourcing and Command Query Responsibility Segregation (CQRS).
Also, if synchronous communication is used between the services even if it is only for a subset of the services — developers are introducing strong coupling and are putting themselves in the hands and mercy of the other systems they are working with. For example, REST is most often synchronous which makes it not a suitable default protocol for inter-service communication. So asynchronous boundary between services is necessary in order to decouple them, and their communication flow, in time — allowing concurrency, and in space — allowing distribution and mobility. This is a major problem with distributed systems — the complexity of asynchronous communication while some services may not be available. Using it we are moving from the ACID world (Atomicity, Consistency, Isolation, Durability) to the BASE world (Basically Available, Soft state, Eventual consistency).
As you can imagine, the deployment of dozens of small services involves much higher overhead than delivering a monolith. Each service requires load balancing, separate CI/CD pipelines, logging, and process monitoring — the same things you would configure once for a monolith. This becomes even more complex and confusing when you have to scale services independently and have different technology stacks. This leads to incredible levels of flexibility, responsiveness, and efficiency, but at the same time, it also comes with huge operating costs in terms of support. However, with the rise of containerization, the advent of k8s, and the development of Platform-as-a-Service, creating a reliable and manageable platform for microservices has become very simple. Without all those new tools even a single service can take over entire teams of IT operations specialists.
So what do we get in the end?
- Microservices allow us to structure our systems in the same way that we structure our teams, sharing responsibilities among team members, and giving them the freedom to own their work.
- Microservices allow a team to implement a new feature or make changes without having to rewrite a large portion of the existing codebase.
- The microservices make it easier for an app to scale and change with increased demand.
- It's easier to choose the technology stack which is best suited for the required functionality instead of being required to take a more standardized, one-size-fits-all approach.
- Services can change direction without massive costs
But microservice architecture brings its own challenges and tradeoffs. While individual services become more robust and less complex, the overall system takes on the many challenges of distributed systems at every level. Each service has its own overhead, and although that cost is reduced by an order of magnitude by running in a PaaS environment, you still need to configure monitoring and alerting and similar things for each microservice. Microservices also make testing and releases easier for individual components but incur a cost at a system integration level.
Despite the challenges, microservices are here to stay because they map better than anything else to the software landscape of the future: cheap and frequent feature delivery, distributed processing, parallel development, platform-as-a-service deployment, and ubiquitous use.