Microservices, microservices, microservices … One of the hottest topics in the industry nowadays and the new shiny thing everyone wants to be doing, often without really thinking about the deep and profound transformations this architectural style requires both from the people and organization perspectives.
In this tutorial we are going to talk about practical microservice architecture, starting from the core principles and progressively moving towards making it production ready. There is tremendous amount of innovations happening in this space so please take everything we are going to discuss along the tutorial with some grain of salt: what is the accepted practice today may not hold its promise tomorrow. For better or worse, the industry is still building the expertise and gaining the experience around developing and operating microservices.
Table Of Contents
- 1. Introduction
- 2. Monoliths Around Us
- 3. Saying “Yes!” to Microservices
- 4. The Danger of the Distributed Monolith
- 5. Conclusions
- 6. What’s next
There are a lot of different opinions on doing the microservices the “right way” but the truth is, there is no really the magic recipe or advice which will get you there. It is a process of continuous learning and improvement while doing your best to keep the complexity under control. Please do not take everything discussed along this tutorial as granted, stay open-minded and do not be afraid to challenge things.
If you are looking to expand your bookshelf, there is not much literature available yet on the matter but Building Microservices: Designing Fine-Grained Systems by Sam Newman and Microservice Architecture: Aligning Principles, Practices, and Culture by Irakli Nadareishvili are certainly the books worth owning and reading.
For many years traditional single-tiered architecture or/and client/server architecture (practically, thin client talking to beefy server) were the dominant choices for building software applications and platforms. And fairly speaking, for the majority of the projects it worked (and still works) quite well but the appearance of microservice architecture suddenly put the scary monolith label on all of that (which many read as legacy).
This is a great example when the hype around the technology could shadow the common sense. There is nothing wrong with monoliths and there are numerous success stories to prove that. However, there are indeed the limits you can push them for. Let us briefly talk about that and outline a couple of key reasons to look towards adoption of microservice architecture.
Like it or not, in many organization monolith is the synonym to big ball of mud. The maintenance costs are skyrocketing very fast, the increasing amount of bugs and regressions drags quality bar down, business struggles with delivering new features since it takes too much time for developers to implement. This may look like a good opportunity to look back and analyze what went wrong and how it could be addressed. In many cases splitting the large codebase into a set of cohesive modules (or components) with well established APIs (without necessarily changing the packaging model per se) could be the simplest and cheapest solution possible.
But often you may hit the scalability issues, both scaling the software platform and scaling the engineering organization, which are difficult to solve while following the monolith architecture. The famous Conway’s law summarized this pretty well.
“… that organizations which design systems … are constrained to produce designs which are copies of the communication structures of these organizations.” – http://www.melconway.com/Home/Committees_Paper.html
Could the microservices be the light at the end of the tunnel? It is absolutely possible but please be ready to change dramatically the engineering organization and development practices you used to follow. It is going to be a bumpy ride for sure but it should be highlighted early on that along this tutorial we are not going to question the architecture choices (and push you towards microservices) but instead assume that you did you research and strongly believe that microservices is the answer you are looking for.
So what the terms “microservices” and “microservice achitecture” actually mean? You may find that there are quite a few of slightly different definitions but arguably the most complete and understandable one is formulated by Martin Fowler in his essay on microservice architecture:
In short, the microservice architectural style … is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies. – https://www.martinfowler.com/articles/microservices.html
The prefix “micro” became the constant source of confusion as it is supposed to frame the size of the service somehow. Unsurprisingly, it turned out to be quite hard to justify exactly. The good rule of thumb is to split your system in such a way that every microservice has a single meaningful purpose to fulfill. Another somewhat controversial definition of the “micro” scale is that the microservice should be small enough to fit into the head of one developer (or more formally, maintainer).
There are basically two routes which may direct you towards embracing microservice architecture: starting the green-field application or evolving the architecture of the existing one (most likely, the monolith). In order to succeed while starting the journey by taking any of these routes, there are a number of the prerequisites and principles to be aware of.
First of all, microservices are all about domain modeling and domain design done right. There are two distinguishing books, The Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans and Implementing Domain-Driven Design by Vaughn Vernon, which are the credible sources on the subject and highly recommended reads. If you do not know your business domain well enough, the only advice would be to hold on with opening the microservices flood gate.
The importance of that is going to become clear in a few moments but it is particularly very difficult problem to tackle when you start the development of the application from scratch since there are too many unknowns in the equation.
Overall, microservice architecture mandates to structure your application as a set of modest services but does not dictate how the services (and communication between them) should be implemented. In some sense, it could be thought of some kind of supreme architecture.
As such the architectural choices applied to the individual microservices vary and, among many others, hexagonal architecture (also known as ports and adapters), clean architecture, onion architecture and the old buddy layered architecture could be often seen in the wild.
The definition of the bounded context comes directly from the domain-driven design principles. When applied to the microservice architecture, it outlines the boundaries of the business subdomain each microservice is responsible for. The bounded context becomes the foundation of the microservice contract and essentially serves as the fence to the external world.
The cumulative business domain model of the whole application constitutes from the domain models of all its microservices, with bounded context in between to glue them together. Clearly, if the business domain is not well-defined and decomposed, so are the domain models, boundary contexts and microservices in front of them.
Each microservice has to serve as the single source of truth of the domain it manages, including the data it owns. It is exceptionally important to cut off any temptations of data sharing (for example, consulting data stores directly), as such bypassing the contract microservice establishes.
In reality, the things will never stay completely isolated so there should be a solution. Indeed, since data sharing is not an option, you would immediately face the need to duplicate some pieces and, consequently, find a way to keep them in-sync.
Every implementation change which you are going to make should be deconstructed into pieces and land in the right microservice (or set of microservices), unambiguously.
From organizational perspective, the ownership translates into having a dedicated cross-functional team for each microservice (or perhaps, a few microservices). The cross-functional aspect is a significant step towards achieving the maturity and ultimate responsibility: you build it, you ship it, you run it and you support it (or to put it simply, you build it, you own it).
Each microservice should be independent from every other: not only during the development time but also at deployment time. Separate deployments, and most importantly, the ability to scale independently, are the strongest forces behind the microservice architecture. Think about microservices as the autonomous units, by and large agnostic to the platform and infrastructure, ready to be deployed anywhere, any time.
Being independent also means that each microservice should have own lifecycle and release versioning. It is kind of flows out from the discussion around ownership however this time the emphasis is on collaboration. As in any loosely-coupled distributed system, it is very easy to break things. The changes have to be efficiently communicated between the owning teams so everyone is aware what is coming and could account for it.
Maintaining backward (and forward) compatibility becomes a must-have practice. This is another favor of responsibility: not only make sure the new version of your microservice is up and running smoothly, the existing consumers continue to function flawlessly.
The microservice architecture truly embraces “pick the right tool for the job” philosophy. The choices of the programming languages, frameworks, and libraries are not fixed anymore. Since different microservices are subjects to different requirements, it becomes much easier to mix and match various engineering decisions, as far as microservices could communicate with each other efficiently.
Let us be fair, the microservice architecture has many things to offer but it also introduces a lot of complexity into the picture. Unsurprisingly, the choice of the programming languages and/or frameworks may amplify this complexity even more.
When decision is made to adopt microservices, many organizations and teams fall into the trap of applying the same development practices and processes that used to work in the past. This is probably the primary reason why the end result quite often becomes a distributed monolith, a nightmare for developers and horror for operations. Let us talk about that for a moment.
Since each microservice lives in a separate process somewhere, every function invocation may potentially cause a storm of network calls to upstream microservices. The boundary here could be implicit and not easy to spot in the code, as such the proper instrumentation has to be present from day one. Network calls are expensive but, most importantly, everything could fail in spectacular ways.
Quite often one microservice needs to communicate with a few other upstream microservices. This is absolutely normal and expected course of action. However when the microservice in question needs to call dozens of other microservices (or issues a ton of calls to another microservice to accomplish its mission), it is huge red flag that the split was not done right. The high level of chattiness not only is subject to network latency and failures, it manifests the presence of the useless mediators or incapable proxies.
The extreme version of chattiness is existence of the cycles between microservices, either direct or indirect. Cycles are often invisible but very dangerous beasts. Even if you find the magic sequence to deploy such interdependent microservices into production, sooner or later they are going to bring the application to its knees.
Managing the common ground between microservices is particularly difficult problem to tackle. There are many best practices and patterns which we as the developers have learnt over the years. Among others the DRY and code reuse principles stand out.
Indeed, we know that the code duplication (also widely known as copy/paste programming) is bad and should be avoided at all costs. In context of microservice architecture though, sharing code between different microservices introduces the highest level of coupling possible.
It highly unrealistic (although worth trying) that you could completely get rid of shared libraries or alike, especially when your microservices are written using the same programming language or platform. The goal in this case would be to master the art of reducing the amount of shared pieces to absolutely necessary minimum, ideally to none. Otherwise, this kind of sharing will drag you down the monolith way, but this time the distributed one.
In this section we quite briefly talked about the microservice architecture, the benefits it delivers on the table, the complexity it introduces and the significant changes it brings to engineering organization. The opportunities this architectural style enables are tremendous but on the flip side of the coin, the price of the mistakes is equally very high.
In the next section of the tutorial we are going to talk about typical inter-service communication styles proven to fit well the microservice architecture.