Have you ever thought about constructing a car? Probably not. But how would you start anyway? You cannot design the whole thing in one complete step. But you can split the problem into minor, manageable ones. So you probably need an engine, some wheels, a chassis. These can then be produced by independent teams that can concentrate on designing a specific solution to a specific problem.
In software engineering, building big software artifacts without dividing the problem domain is nearly impossible. By following the principle of separation of concerns, smaller components that address a specific problem (and doing that well) will be combined to form a solution to the overall problem. Developer of a single component can now concentrate on developing and testing a single component without keeping the whole problem space in their head.
Have you finished the engine by now? Does it fit into your chassis? If not you probably forgot to define the interfacing part between both components.
Component-based software engineering defines a component as a module with a set of related functions communicating with other components via a well-defined interface. This enables among others reusability and replaceability of the component and decouples the dependencies to internals of the component. (Think of replacing an engine of your car with a different type).
I kept the terms deliberately very general and fuzzy because that is what they are. A component can be a function (in procedural programming), an object (in object-oriented programming), or even a whole webservice. The communication can be done via a function call, a remote procedure call (RPC) or a normal HTTP message. So from the software-architectural point of view, it does not matter how components are composed or how they communicate. Microservices are no exception in that they are just components that communicate in some way and that are composed in some way.
So what’s so special about microservices? There are advantages and disadvantages as with everything in life. They probably also depend on your environment as well, so here are our advantages:
- They are dynamic. Because they are small and independent, they can be changed and redeployed very easily.
- They are rigid. These services only communicate via their explicit interfaces. Callers are very decoupled and not affected by internal changes. This can also be done in normal “library”-based development but happens to be harder to achieve.
- They are testable. Their limited and well-defined functionality together with their defined interface allows for easy testing.
But microservices do have disadvantages and they are probably not that easy to spot at first.
Maybe the most obvious disadvantage (in our case) is that you need to have the right development environment to actually be able to effectively use microservices and their advantages. You need to be able to (re-)deploy a service fully automated (that is, you need a full continuous deployment pipeline). Otherwise the overhead of managing lots of these services will become too much. Your environment needs to allow you to set up a new service in no time, otherwise the threshold to create these services is too high (e.g. let administrators set up servers etc). And you should be able to scale your service instances easily (e.g. to react to higher load because of reusage of microservices).
Another disadvantage is that you push a lot of problems “out of sight” because they lie in-between your components. As long as your components are functions or objects, as stated above, intended behavior can be verified by integration tests which are, for example with Spring, easy to create. They will be executed some time before deployment because afterwards, the application will not change that much. But how do you create integration tests for distributed microservices? You system now consists of several services that all interacts with each other (directly or indirectly). Running integration tests on this system means creating a whole test-environment with all services just for the purpose of integration testing. This means a higher effort of implementing and maintaining integration tests.
And finally, due to the underlying technology, a whole new category of problems is created (well not new but not that important to deal with in non-distributed systems). When did your last function-call failed? (Except if you are programming in non-deterministic languages). Because of rising numbers of remote-calls, you have to put higher effort in reliability and resilience of the overall system. And then again you need to monitor the overall distributed system for slowdowns or outages.
People that move towards microservices are conscious about these problems and there exist approaches to each of the mentioned disadvantages. Following blog posts will describe such approaches and best practices (as far as one can talk of best practices in this early stage of microservices).