A microservice architecture is an interesting beast, as there is no real prescriptive definition of what makes one that is universal in nature. In my view a software architecture is much more akin to a form of software philosophy than any engineering discipline.
Viewed from that perspective, a microservice architecture is a thinking tool, that emphasises maintaining the ability to change by introducing forms of isolation into your system.
This is what led to the discussion on antifragility that we spawned a few years ago. The key concept there is “optionality”, creating and maintaining options for you to realise later. This is an investment in future agility for your system, in effect. The operative word there is investment. It costs, real money, and real effort. As such, it is a very different strategic choice than being absolutely resource efficient.
All the technological, culture and business advantages/ disadvantages flow from this one root.
Do you want future agility, or to be more efficient with your resources right now?
The true cost of “efficiency” is that you may make the wrong decisions, the world may change, and so you find yourself out of step with the current competitive environment. At that point, it will cost to fix your mistaken predictions for the future.
Creating isolation between microservices enables them to be changed as fast as you need them to be. This is the benefit of the approach. This generally requires isolation at several levels:
Runtime Processes: the most obvious, and one that is commonly adopted quickly. Where before you had one process, now you have many. The primary cost here is adopting some form of distributed computing, which is hard to do right. This may lead you to adopting containerisation, event architectures, various http management approaches, service meshes, circuit breakers and the like.
Team/ Cultural: separating teams, to give you autonomy, means that you partition your human to human communication. This tends to lead to knowledge silos and duplication of work (a working out of the optionality vs resource efficiency choice). I am often reminded of an article by Peter Naur - Programming as Theory Building, which is prescient in so many ways about the way people learn programming. This applies in microservices as much as anywhere.
Data: the largest impact of adopting a distributed computing approach like Microservices is in the way it affects your data. You have partitioned your data in some form, and so you need to re-integrate it at the system level to give the impression of “a system”. This gives you some interesting potential benefits in regards to scaling, but also needs much more thought than a simple monolithic approach to data architecture. Distributed computing is a hard topic, and shouldn’t be taken lightly.
In my experience the two biggest problems you will face when adopting a Microservice architecture are handling of upgrades and understanding performance issues and the general behaviour at runtime.
Upgrade issues come in a couple of forms, the first is that you are running a set of instances of your service at runtime, and you likely have other services using it. To handle this you need to manage orchestrating services and routing traffic appropriately. So you can look at things like Kubernetes or Cloud Foundry to help with rolling out upgrades, possibly with a service mesh such as Istio to manage routing during the upgrade.
A more interesting effect of upgrade is on development, which is how do you know what your dependencies versions will be, does this matter? Conversely, does your upgrade affect your services dependents?
This is into the realm of software contracts and there are a variety of approaches to the problem. This I find is the biggest effect on the dev process, apart from simple delivery speed.
Runtime performance is a gnarly topic. You will witness the full fury of network effects appearing within your application. Generally this is caused by temporary network issues (chatty neighbours, broken links and the like), or by naive communication or API design. Naive designs tend to take the form of over using RPC styles, including overuse of HTTP.
Reactive principles help to manage these kind of problems, but come with their own costs. This is where I spend much of my time on the Muon project, enabling an API approach, beyond what http can provide
Naivety.
The first question I ask my clients when they ask me to help them implement Microservices is “why?”.
The answers I’m looking for are “we want to change our system faster” and “we want to take advantage of cloud tech”.
I make them aware that Microservices, done right, are more expensive, and harder, than building a Monolithic system. It does interesting things to your data, and introduces a network into your data model, which can arbitrarily partition at any time, with data being lost. You expose yourself to the full fury of distributed computing.
To do things right, you will need to invest, and invest in the right things. You will need to understand the benefits you are aiming for, and have a plan on how to achieve them and importantly, how to measure all of that.
Understanding what you actually want is key. If it's simply “deliver faster”, the next question is “why?”, drilling deeper into the problem. Something like Simon Wardleys “Wardley Maps” can help here, as can Gojko Adziks Impact Mapping techniques and related value analysis tools.
The point is that a Microservices initiative is not a simple technical approach to a technical problem. The problem is much broader, and the approach is broad too in order to solve it.
To a large extent the choice of language is orthogonal to Microservices as an architectural choice.
My main focus when implementing microservices is on the data, since that is where most of the pain of distributed computing appears, and where you can deliver many of the benefits of the approach too.
From a data architecture point of view, you need something that can give you data that can be easily synchronised into some usably consistent state across a network between services. There are a variety of approaches for that, which is what I’m actually looking for in my Microservice deployments. So, you can observe the various frameworks and technologies for implementing these kind of patterns (this is where tech such as Kafka, Spring Data Flow, Akka and friends sit).
Once we have decided on these patterns and approaches, you then mesh that with what resources you have available. If you have decided on a data flow approach with lots of reactive programming, and you already have Java devs, then it makes sense to pick Spring, Spring Cloud Data Flow, Kafka and probably deploy onto some form of Cloud Foundry (if you can get it!).
If you need lots of heavier data transforms, bring in Spark, or Kafka Streams to help with that.
If you have javascript developers, then that would not make sense, instead you would look to adopt some functional language on the JS runtime (clojurescript etc), again use some similar reactive integration tech (Kafka is certainly making waves in this space) and take it from there.
A Microservice architecture, when actually implemented as that, explicitly defends you from making strategic choices, and so the question of which language to choose must become a purely tactical one as far as the architecture is concerned. This may be overriden by other concerns, such as maintaining a large software organisation, long term resource planning/ hiring etc...
This is an intriguing question, for which there is a very broad and diverse set of opinions and answers. I’m sure that there is not a best programming language for any of these things, and in any case, they do not exist in isolation. Since we are building some Microservice system we need to select languages that can handle a variety of concerns.
Going back to Peter Naurs article, the key concept he describes there is the notion of the theory of the system, tacit knowledge that cannot be taught, only absorbed over time. Intuitively, we know that when a software developer starts on a new system, they take time to understand the code, and the full context of the system they are presented with. Until they absorb enough of that knowledge, they will find it hard to make a change that fits properly, hard to quickly understand the full implications of that “one line change”.
What this is speaking to is that not all developers are created equal and that this is ok. Some teams are very experienced in their systems and problem domain and are working in very complex environment.
When approaching a piece of code, they need to quickly absorb what it does in totality, along with its context. The more information dense that code is, the better. In those kind of scenarios, I would expect pure functional languages and those others with less boilerplate and more cryptic syntax to win out, since they all tend to be much more information dense. Other situations, where code is left unlooked at for months on end, you need rigid patterns that repeat not only across the codebase, but across the industry, just to enable developers to know where to start unpicking this thing in front of them to understand it. This is where somewhat more high ceremony languages, such as Java, really shine. Derogatory statements about “boilerplate” aside, it serves a very real purpose, helping developers learn and orient themselves in a large codebase.
In the context of Microservices, this is something to consider, do you want information dense? The ability to maintain many services from a single team, move people around?
From a performance point of view, not all runtimes are created equally, but almost all of the time, this simply doesn’t matter. Your performance problems will not be in your application runtime, they will be in your data architecture and communication approach/ API strategy.
This is a guest post from David Dawson. Check out David's blog here.
David is a freelance systems architect and software engineer specialising in Microservices, high performance systems and IoT.