Almost certainly, in this day and age you have heard of Microservices architectures. The term gets thrown around very often when talking about scaling and handling growth within your system.
But what are, really, Microservices?
The fact is, this type of architecture is nowhere near new. Airbnb was building Microservices at scale back in 2012 just to give you an example. Distributed Systems were around many years before that.
Microservices are small services [micro], that have one purpose or serve a specific domain only. By having many of these, you're able to scale horizontally instead of keep growing a big machine.
The idea of Microservices is actually better explained when put against traditional monolith applications, and that's probably the best example out there.
In the earlier days, you'd have a single codebase for your app, using a single programming language, and you'd scale the resources it needed as required given your growth. In hindsight you can clearly see the problem with that, at some point you'll need huge machines to support scale. There were workarounds to manage this issue, but even then you'll hit a cap at some point.
The logical next step was to start separating these applications into different systems that communicate passing messages between each other. These systems could be split by domain for example, say accounting vs inventory. Still, these splits were also somewhat inflexible and would grow in the same way as their strict monolithic peers.
At some point, what Engineers figured out was that making these services smaller you gain flexibility, to the point that when needing to add functionality you could just build a new service, and if needing more power in a particular area of the system, you could spin many of these services to cope with the load or scale them independently.
That was sort of the birth of Microservices.
Jokes, this is when things got exciting!
Now armed with this knowledge, Software Engineers and Architects could build all sorts of different topologies to handle their specific needs.
However, Microservices did come with some challenges of its own.
There are many ways to communicate across services, and that has also evolved over time. The traditional way is to pass messages via HTTP or SOAP in the same fashion other Service-Oriented Architectures did.
Another option is to use shared data stores like databases to write data on one place and read it in another place.
Each of these are not exclusive to Microservices in fact, they were adopted from previous service-oriented iterations. The novelty now is that messages can be smaller and serve a single purpose as well, for example a service could accept only messages related to updating a user's profile information, or making a booking and that decreases complexity overall.
Given the scale at what modern applications grew, even these ways of communication weren't enough, so the current standard of using message queues was born. Companies like Google, Amazon, Facebook built their own implementations of event queues but nowadays we can find more widely adopted options like Kafka or RabbitMQ.
Event-Driven architectures are the modern standard at the time of writing, and they're a whole new level of complexity and awesomeness, which I will write more about in the future.
The practice of having hybrid architectures, running different services on multiple programming languages and setups is widespread now. You can easily have multiple programming languages running within the same system now that small services are running somewhat independently from each other.
This allows you to maximise the benefits of each technology you pick, as long as you find a good way to communicate between them.
This is where it starts getting tricky. There isn't a single rule to define what is Microservice or how micro is micro. There's a guideline that says it needs to be small enough to serve a single purpose and be contained within a single domain but that's vague, probably on purpose.
In practice, when you create Microservices what tends to happen is that you start with something that resembles a Microservice given that definition, but over time the service grows and you need to reevaluate whether it needs to be split as it doesn't serve a single purpose anymore.
You need to find a balance between having too many services that are too small and make it harder to maintain, versus having services too big that are not Microservices anymore and become harder to scale.
You can call it bad practice, bad design, band planning, or whatever, but that's what usually happens, and it's natural to only notice in hindsight. The good news is that if the services are "small enough", splitting or rewriting them is often not much of a problem. Even replacing them completely in a new programming language is also on the table.
One of the main issues in Microservice architectures is the concept of transactions. If multiple services execute different parts of a transaction, how do you rollback when the process fails half-way?
This is a very complex problem and can be solved in different ways depending on your architecture. Most certainly you are better off using an existing design pattern to solve this problem.
Two common patterns to solve this issue are the 2-phase commit pattern (2pc), which uses an orchestrator service to rollback transactions if something goes wrong. This is a synchronous approach to solving the issue; The other pattern is the Saga pattern, which aims to produce events of the different stages of a transaction and compensating the failed transactions when something goes wrong. This is the asynchronous approach.
It's out of the scope of this article to visit those in detail, but it's a good idea to research these patterns in depth as you are likely to encounter these issues if working on Microservice architectures.
Another common problem is having to update the event structure that Microservices use to communicate with each other.
Given that services need to know how to parse a message from other services, when changes in that event are needed you need to find a way to keep the communication working until such services are back in sync.
To solve this issue you probably need to use some form of versioning. In the case you can shutdown the Microservices completely, update them and put them back on, this is not an issue at all.
When that's not possible the next best case scenario is adding new data to an event without affecting the older structure. This way you can upgrade your sender first to add the new field(s) and work on supporting the listeners later on.
If that's not possible either, solving the issue becomes a tad more complicated. A good solution for this is to add versions on your listeners while you transition to the newest events. Then depending on the version of the message received you can parse it one way or another.
There are multiple ways to solve these types of problems, and the solution will be different depending on the message restrictions; For example, having a hard type schema of your event will make it more complex.
Large Microservice architectures are harder to maintain and to update. You can probably Dockerize the instances that are running the services and update all of the machinery at the same time, but if you have multiple services running different programming languages on different versions upgrading at different speeds, then keeping everything up to day becomes a repetitive and time consuming task.
With this in mind, it's important to take as many steps as possible to ensure that domain functionality is not too tangled so services can be upgraded independently from each other as much as possible.
Microservices architectures are a fantastic way to scale your system horizontally and to manage the growth of your business but it doesn't come cheap. There are tradeoffs to be aware of and having a solid understanding of the principles, patterns and benefits is important.
There are also many different topologies to consider, and what works for me may not work for you, so make sure that you understand your business requirements when designing your system.
Another thing to keep in mind is that this is a complex architecture, even though it sounds simple. You don't need to jump on it right away, especially if you're only getting started. This is a complexity trap. If you don't have to handle a high amount of traffic, it's probably best to start with a single monolithic app and slice and dice it as you grow. It will save you time and probably money.
coding designing? 🤔