The idea behind micro services
Micro services are so much more than just a way to build a large application. They are not about having a whole bunch of independent tiny web servers talking to each other. It is a design pattern you can use to structure your code, any code. At heart, micro services are about small independent units working together toward a larger goal. Micro services are probably the absolute best hammer for just about every single job. Here’s why.
The first thing micro services bring to the table is a strong concept of integrity. That is strong cohesion and low coupling. The very concept of having a small object taking care of some very specific needs goes hand in hand with the problem of cohesion and coupling. To understand this, we must first look at what is the service pattern. In OOP, a service is a piece of code, typically a class, that handles processes on a related model. If you look at it from an MVVM point of view, services are what your view models should call to acquire their data, apply your business processes and push the data back to some permanent storage.
Here is a quick example of an application using the service pattern to display a list of employees whom contact information have not been filed:
Here, we are using an IEmployeeService to fill a list of invalid employees in the view model when a refresh is triggered. The goal of this service is to hide the complexities of the underlying query, leaving only display logic to be taken care of in the view model. The service takes care of forwarding the security token to the remote repository and gives a meaning to “Invalid Contact Info” by saying that an employee must have a contact and an emergency contact to be valid.
The more you think in term of services, the more you end up with reusable block of independent features. This is because you can clearly see what belongs into a service and what doesn’t. The code that doesn’t ends up distracting when trying to accomplish the main service’s task just feels out of place. It might be a method that deals with significantly different data than the others or a block of code that doesn’t look like it has been written in the same style that the others. It might sound counter-intuitive, but the latter is usually easier to spot than the former. Code that is imperative stands out in a sea of declarative or fluent calls, highly nested loops or conditions are clearly, visually not aligned with more linear algorithms and finally injecting a dependency for a single line of code is the epiphany of those examples. On the other side, it is hard to tell if the
Employee class have anything to do with the
Person class or if you should handle a
Person emails or hand that over to a different service. In our case, it is safe to say that an
CreateEmployee method would make quite a lot of sense in our
IEmployeeService, but a
CreateManager method would probably deserve its own service as it deals with a different entity and repository.
In other words, well designed services tends toward having a single responsibility and micro services are exactly that: a single service for a single task.
Another reason why you might be interested in using micro services is that their nature makes them highly extensible. They tends to have a fair amount of injectable dependencies that you can swap to create new behaviors. They also expose almost everything they can do through an interface making it easy to override part of the behaviors. Finally, in most languages, it is trivial to implement the most complicated behavior the application might need in a service and expose this behavior through overloads that offers simpler variations as needed.
Let’s say we update our example to support paging and filtering. The interface will probably be updated to look like this:
But you might also want to be able to take some shortcuts when some values can be inferred like in our existing view model:
This can easily be accomplished using a simple extension method.
While those are just simple overloads, you could go a lot further using simple extension methods. The important thing to remember here is that the service itself does not have to be modified to support those overloads. Everything can be done from outside in separate classes. Smaller services makes for easier code organization when dealing with those extensions.
The second your start thinking in terms of services, you have to deal with dependencies. Since services are highly cohesive, they strongly depends their users to provide them with their dependencies. This means that services ends up being shared between each other in your code quite often. Taking a look at our previous example, many view models will probably use an
IEmployeeService and many services will use an
IUserAccessToken. Their code ends up being reused dozens of times throughout your application.
Sometimes, some services might have a common codebase and you might be tempted to leave everything in a single class to simplify the data flow. This is questionable, but in such a case, nothing forces your to expose a single interface for both behaviors. You can easily implement two interfaces in a single service. This enables you to publish your service to other developers while keeping the ability to split it in two later on when the need arise.
For instance, we could have an
ICreateEmployeeService and an
IUpdateEmployeeService. We can always add an
IDeleteEmployeeService later if we need to. In the end, all of that code can end up in the same
EmployeeService class we already have. The important thing is, we can always change our mind later on.
As I already mentioned, a side effect of dealing with highly decoupled services is that you end up having to deal with dependencies a lot. Because of this, micro services are a clear use case for constructor dependency injection and inversion of control. Each services should always expose an interface. Other services can then depend on them through constructor injection without imposing a specific implementation. You can then truly let your dependency injection framework shine and take care of resolving those dependencies for you.
In fact, you can even inject new services at run-time through a plugin architecture which lets you extend your application as it is running. Having all of those reusable blocks of code helps in exposing points of interactions with the plugin’s developers and simplify their job. Everyone wins!
Conclusion… or is it?
As you can see, there are a lot of advantages to micro services, the main one being that they not only respect but actually impose SOLID principles onto your code. It is harder to write micro services without ending up respecting those principles than trying to force your way against them. This makes the micro services pattern a really good design candidate for just about any code base. But, there are even more advantages…
Aside from the SOLID principles, micro services also comes with other additional goodies that you might like.
When dealing with very small, reusable piece of code, it gets harder to track and store states. There are so many places where you could store data that you are better-off without states at all. This encourage the use of stateless patterns like pure methods which not only improve code readability but also reduce the chances of bugs. Good micro services design usually abuse of the principle of immutability to reduce side effects as they would get amplified very quickly as a service gets reused.
For instance, instead of adding a
SetMaxResultCount method on the service, we prefer to share those states through the method parameters. Having to deal with such a state would be really troublesome if we started to have multiple instances of the
EmployeeService class. In this case, immutability is just a simpler path.
Usually, in a serious development environment, when you write a piece of code, you end up owning it. People can easily track who wrote a block of code through the use of source control systems or even through knowledge sharing meetings. Your coworkers will ends up depending on your knowledge of the code when using your services or figuring out issues. As the original writer, you will be the first resource everyone will go to when they have questions.
This can be really painful when you author a large piece of code because by the time you come back to them a few months later to answer questions, you will have a much more complex puzzle to wrap your mind around before answering anyone’s questions. With micro services, there are also less chances someone else will change your code in a way that makes it unfamiliar to you. If a large refactor happens, they will have to take ownership of the new services and your name will usually end up erased from history as new classes gets created. Normally though, you will rarely see someone else change a service you own. Instead, most people will be creating new files all the time which makes it easier when you come back to your code a few months later.
You probably noticed that I never attached micro services to any specific communication technologies . This is why they are in fact a pattern. They can be implemented inside an application or through an entire cloud application by using multiple segregated applications that communicate to each others using message queues, rest or protocol buffer endpoints or even simple piping. In fact, *nix is strongly based on the micro services pattern as it is built as a collection of small tools like
chmod. Thing is, it wasn’t a thing at the time so they didn’t advertised it as such. In my head though, linux is a perfect example of a very successful micro service architecture.
Next time you design an application or refactor an existing one, consider this: micro services are a pattern that provides a SOLID foundation and helps reduce bugs through immutability and fix them through better code ownership. They truly are a recipe for success.
Read ya later!