This is the third part of a multi-part series of blog posts on Micro-services. There seems to be a lot of ambiguity amongst the community around what these things are, when they should be used and how to approach them. This blog series is basically a regurgitation of 5 years of building and learning about micro-services. I imagine that not everybody will agree with everything I say here, but I hope that there is nothing so controversial as to cause serious disagreements.
The challenges of micro-services (aka Why shouldn't you use micro-services?)
As with every tool that we choose to use, there are costs and benefits. The benefits of micro-services are many and varied (as I outlined in the previous section) but the costs are there. Without mitigation, it is entirely possible for a project with micro-services to be sunk beneath the costs.
I think that it's important to point out that many of the challenges I'm going to list here are also challenges for non-micro-service architectures. These challenges are also present in other architectures or manifest in other ways. There are always going to be trade-offs between the costs and benefits of every architectural decision that you make.
This section of this series is going to focus mainly on the challenges themselves. I will attempt to explain and articulate them in a meaningful, and therefore discussable way. The solutions to these challenges are quite varied and some are very technology dependent. You will solve them differently using a .NET stack than a Ruby stack or a JAVA stack. I will discuss some architectural mitigations to these challenges in the following section (or sections).
a) Cognitive complexity
In a way, Microservices can dramatically reduce the complexity of a given system. If a developer wants to know how a system works, it is much easier to grok several systems with 1k lines of code than a single system with 10 - 15k. Additionally, given really small services, poorly or incomprehensibly written services can be reimplemented with little impact to the rest of the system. However, this comes with a cost. Tracing execution across service boundaries can be challenging. Logs become distributed and tracking down issues can take a developer a long time. Simply understanding the responsibilities of the services, and more importantly maintaining the integrity of those service responsibilities can be a challenge.
James Lewis, speaks about some mitigations around this challenge cognitive complexity by stating that each level of abstraction should be small enough to fit in his head. It's "a vital component of this - at each level of abstraction a micro-service architecture should make sense." Using the neighbourhood analogy again, James implies that each neighbourhood of functionality should be small enough to understand the interactions between the services within it. While architecting your system "have to be conscious of chunking up and down the levels of abstraction (why? takes you up, How? takes you down)." James believes that the additional cognitive load of understanding a micro-service is not much greater than and non-trivial system.
I generally agree with James, but I believe that you have to be very careful to ensure that your abstractions are correct and are willing to change them as you need to. It is possible for a micro-service architecture to add additional cognitive load and you need to ensure that you are mitigating this cognitive load as much as possible.
b) More Integration Work
Integration between two services always causes additional work. Additional effort for the developers. When a system is broken down and each part of the system must integrate with other parts of the system, this causes additional work. As soon as you have data flowing over http and systems communicating with each other you need to start thinking about things like transport layer security and system authentication. You need to consider integration approaches using a transport mechanism that needs to be fault tolerant. Patterns like circuit breaker need to be considered so you can avoid overloading other micro-services.
c) Duplicated code/library coupling
There is an on going debate about whether or not you should share client libraries for each micro-service you build. If you don't, you end up with code duplication and additional challenges around API versioning. If you do, you end up with tighter coupling between your services. It's not an easy decision and both methods have their own costs and benefits.
A couple of rules that I always follow are: 1) NEVER share client libraries with a third party or even between different code bases within your company. If you have different code bases they can not be coupled via a client library as you lose a lot of the value of REST. 2) NEVER NEVER NEVER put ANY domain logic inside your client libraries. Client libraries are meant ONLY to provide a convenient method for integration NOT any sort of domain processing. I will delve slightly deeper into this concept in the following post.
d) Configuration Management
Configuration management in a distributed system is a challenge. In a way this is a solved problem, but none of the solutions come for free. To put the challenge in numbers, say that you have 5 micro-services in your architecture, if you have ONLY 3 environments (dev/local, pre-prod, prod) you will need a minimum of 15 configuration files to support this. I won't go into too much detail about this challenge, but suffice to say that you need to deal with it in an effective manner or get buried in a configuration nightmare.
Additionally, it could be very easy to break some of granular benefits of a micro-service architecture if your configuration management system re-couples the dependencies at deploy-time due to insufficient thought given to deployment architecture.
e) Glue Code
Each service that you create requires some sort of application container to make it work. These application containers require some level of glue code to make them work. Glue code being defined as code that provides the scaffolding upon which you can build business functionality. In 'Technologies of Old'(TM) the amount of glue code required would make a micro-service architecture virtually impossible to build. Tech stacks like J2EE or ASP.NET make the micro-services a very unpalatable option. Fortunately, there have been a raft of technologies coming out recently that provide lightweight HTTP containers that don't take massive amounts of glue code. Sinatra (Ruby), Nancy (.NET), Dropwizard (Java) and Play (Scala) are great examples of new technologies that dramatically help reduce the amount of glue code required for a micro-service architecture.
Deploying more than one service is more challenging than deploying a single service. It can be argued that the complexity chart goes something like this 1 < 2 = n, or the majority of the work required is to deploy more than one service rather than each service adding incrementally more work. If done right, this is close to the truth, but regardless, it costs more to deploy multiple services than it costs to deploy a single service.
Each one of these challenges can be mitigated either through technology choices or architectural choices. It is a silly architect that goes into a micro-service architecture without understanding these challenges and having a plan in place to mitigate them. From here, this series of blog posts will move more into the how about building micro-services. If there is anything specific that you'd like to see me talk about, please let me know and I'll make sure that I cover it. The velocity of my posts may slow down even a bit further as I haven't quite worked far enough ahead at this point.
Copying of this content is allowed only with reference to the source and indication of the author of TQM systems' material.