Microservices are a popular software architecture these days, including at Tallence. Their advantages speak for themselves: they scale well, provide a modular structure, are error-resistant, easy to understand and quick to develop. This is achieved by dividing the overall system into the smallest possible ("micro") loosely coupled and independent backend applications ("services"), which mostly communicate via REST-interfaces.
Als Beispiel dient ein kleiner Versandhandel. Statt in einem Online-Shop wird per Telefon bestellt. Die Verkäufer greifen auf einen internen Produktkatalog zu und tragen Bestellungen in ein internes Bestellsystem ein. Eine Versandabteilung kümmert sich um das Verpacken und Versenden der Produkte.
Die Architektur besteht aus folgenden Microservices:
- ProductCatalogService: Verwaltet welche Produkte es gibt und wie viele davon auf Lager sind
- CustomerService: Beinhaltet Kundendaten wie Telefonnummer, Anschrift, Interessen
- OrderService: Verwaltet Bestellungen und deren Zustand (in Vorbereitung, versendet, …). Eine Bestellung referenziert Produkte aus dem ProductCatalogService und Kunden aus dem CustomerService
- SalesCampaignService: Für Rabatte, Kombo-Angebote und sonstiges. Referenziert Produkte aus dem ProductCatalogService
Die Services müssen stellenweise zusammenarbeiten. Zum Beispiel: Wenn Produkte aus dem Lager geholt wurden, um verpackt zu werden muss, als Gesamt-Transaktion, der Lagerbestand im ProductCatalogService verringert und der Bestellstatus im OrderService auf “in Vorbereitung” gesetzt werden.
A small mail order business serves as an example. Instead of an online store, orders are placed by telephone. Salespeople access an internal product catalog and enter orders into an internal ordering system. A shipping department takes care of packing and shipping the products.
The architecture consists of the following microservices:
- ProductCatalogService: Manages which products are available and how many are in stock.
- CustomerService: Contains customer data such as phone number, address, interests.
- OrderService: Manages orders and their state (in preparation, shipped, ...). An order references products from the ProductCatalogService and customers from the CustomerService.
- SalesCampaignService: For discounts, combo offers and other. References products from the ProductCatalogService.
The services need to work together in places. For example: When products have been pulled from the warehouse to be packaged, as an overall transaction, the inventory in the ProductCatalogService needs to be decreased and the order status in the OrderService needs to be set to "in preparation".
Another point of view
Looking at it from a different angle, weaknesses become apparent. In the architecture above, microservices reference data from other services, but they should be loosely coupled. The distributed transaction (packaging) can only be completed with the participation of multiple services. It is complex and tightly couples services.
Is this still in the spirit of microservices? To assess this, let's look at how their characteristics come about.
Ein anderer Blickwinkel
Betrachtet man das Ganze aus einem anderen Winkel, zeigen sich Schwächen auf. In der obigen Architektur referenzieren Microservices Daten anderer Services, jedoch sollten sie lose gekoppelt sein. Die verteilte Transaktion (Verpackung) kann nur unter Beteiligung mehrerer Services abgeschlossen werden. Sie ist komplex und koppelt Services eng aneinander.
Ist das noch im Sinne von Microservices? Um das zu beurteilen, betrachten wir, wie ihre Eigenschaften zustande kommen.
A step back to basics
When considering how the properties of microservices come about, it is helpful to look at the concept of bounded context.1
A bounded context is a technical context in which terms have a specific meaning and certain concepts and rules apply. Such contexts always exist, even if they are not consciously or explicitly defined. Many misunderstandings (and bugs) are based on using the same terms but unconsciously not talking about the same thing.The mail order business from the introduction, for example, has the two bounded contexts of sales and shipping. The term customer is a good candidate for misunderstanding. It exists in both contexts, but with different meanings: sales sees a customer as someone who can be reached for quotes and advice via email address and phone number. In shipping, on the other hand, a customer is someone with a shipping address who has ordered products.
On the other hand, terms like discount or package size and rules like "20% off everything except pet food" and packaging guidelines belong to only one of the two contexts at a time.
Bounded contexts communicate with each other through events. Two examples are: An order was completed. A package of goods has been shipped.
Good context boundaries and events are immensely important. Then, bounded contexts are self-contained, loosely coupled to each other, and strongly internally coherent.
Ein Schritt zurück zu den Grundlagen
Bei der Frage wie die Eigenschaften von Microservices zustande kommen ist es hilfreich das Konzept des Bounded Context zu betrachten.1
Ein Bounded Context ist ein fachlicher Kontext, in dem Begriffe eine bestimmte Bedeutung haben und bestimmte Konzepte und Regeln gelten. Solche Kontexte gibt es immer, auch wenn sie nicht bewusst und nicht explizit definiert werden. Viele Missverständnisse (und Bugs) basieren darauf, dass man dieselben Begriffe benutzt, aber unbewusst nicht über dasselbe redet.
Der Versandhandel aus der Einleitung hat, beispielsweise, die zwei Bounded Contexts Verkauf und Versand. Der Begriff Kunde ist ein guter Kandidat für Missverständnisse. Er existiert in beiden Kontexten, aber in unterschiedlicher Bedeutung: Verkauf sieht einen Kunden als jemanden der für Angebote und Beratungen per E-Mail-Adresse und Telefonnummer erreichbar ist. In Versand hingegen ist ein Kunde jemand mit Lieferadresse der Produkte bestellt hat.
Andererseits gehören Begriffe wie Rabatt oder Paketgröße und Regeln wie “20% auf alles außer Tiernahrung” und Verpackungsrichtlinien zu jeweils nur einem der beiden Kontexte.
Bounded Contexts kommunizieren über Ereignisse miteinander. Zwei Beispiele sind: Eine Bestellung wurde abgeschlossen. Ein Warenpaket wurde versendet.
Gute Kontextgrenzen und Ereignisse sind immens wichtig. Dann sind Bounded Contexts eigenständig, lose gegenseitig gekoppelt und stark intern kohärent.
From Bounded Contexts to Microservices
To relate microservices and bounded contexts, the following approach helps: "Micro" means a service relates to exactly one bounded context. "Service" means "business services" rather than technical ones such as CRUD operations.2 A bounded context is the model, a microservice its implementation as an application.
For the application to be as self-contained as the model, it includes everything. Front-end, back-end, data storage, all resources and all data.3 From this point of view, for example, a database alone is not a microservice. It also follows that synchronous invocations, and REST interfaces in particular, do not make sense between microservices. A synchronous call would mean one service is waiting for confirmation or data from another. REST (and similar technologies, such as GraphQL) models data. Both of these conflict with the application owning everything it needs.
A microservice is therefore a complete application that provides all the business functions required for exactly one bounded context. According to this terminology, a monolith is an application that covers several bounded contexts. This explains typical problems with monoliths. For example, that a term has different meanings in different contexts, but must be reconciled in the implementation. This makes it complex and changes have to be coordinated with many stakeholders. Or that there is a danger that components that are actually technically independent of each other become technically dependent on each other, thus softening the modularization. For software smaller than a bounded context, as in the original example, we need also still another term.
More on that in a moment, before we look at another important factor: teams.
Von Bounded Contexts zu Microservices
Um Microservices und Bounded Contexts in Beziehung zu setzen, hilft folgende Betrachtungsweise: “Micro” bedeutet, ein Service bezieht sich auf genau einen Bounded Context. “Service” bedeutet “fachliche Dienstleistungen”, statt technische wie etwa CRUD-Operationen.2 Ein Bounded Context ist das Modell, ein Microservice dessen Umsetzung als Applikation.
Damit die Applikation so eigenständig ist wie das Modell, beinhaltet sie alles. Frontend, Backend, Datenhaltung, alle Ressourcen und alle Daten.3 Aus dieser Betrachtung ist, zum Beispiel, eine Datenbank alleine kein Microservice. Aus ihr folgt auch, dass synchrone Aufrufe, und insbesondere REST-Schnittstellen, zwischen Microservices keinen Sinn ergeben. Ein synchroner Aufruf würde bedeuten, ein Service wartet auf Bestätigung oder Daten eines anderen. REST (und ähnliche Technologien, wie GraphQL) modelliert Daten. Beides steht im Widerspruch dazu, dass die Applikation alles besitzt, was sie braucht.
Ein Microservice ist also eine vollständige Applikation, die alle fachlichen Funktionen zur Verfügung stellt, die für genau einen Bounded Context notwendig sind. Nach dieser Terminologie ist ein Monolith eine Applikation, die mehrere Bounded Contexts abdeckt. Daraus erklären sich typische Probleme mit Monolithen. Etwa, dass ein Begriff in verschiedenen Kontexten unterschiedliche Bedeutungen hat, aber in der Implementierung unter einen Hut gebracht werden muss. Sie wird dadurch komplex und Änderungen müssen mit vielen Stakeholdern koordiniert werden. Oder dass die Gefahr besteht, dass fachlich eigentlich unabhängige Komponenten technisch voneinander abhängen und so die Modularisierung aufweicht. Für Software kleiner als ein Bounded Context, wie im ursprünglichen Beispiel, brauchen wir auch noch einen Begriff.
Dazu gleich mehr, vorher betrachten wir einen weiteren wichtigen Faktor: Teams.
Teamstrukturen
Wenn es um Teams geht, kommt man um das Gesetz von Conway nicht herum. Es besagt: Organisationen entwerfen Systeme, die die Kommunikationsstrukturen der Organisation abbilden.4 Kurz: “You’re gonna ship your org chart”.
Angenommen, eine Organisation trennt ihre Mitarbeiter nach technischem Fachgebiet: Frontend-Team, Backend-Team, Datenbank-Team, … Das fördert z.B. die Kommunikation der Frontend-Mitarbeiter unter sich, sodass sie sich informell austauschen und eng zusammenarbeiten. Es schottet sie aber von den Backend-Mitarbeitern ab, mit denen sie sich daher viel über Dokumentation und Spezifikation abstimmen. Was dazu führt, dass die Software genau so strukturiert wird: Strikt voneinander getrennte, aber sehr breite Schichten. Diese Organisation produziert auf ganz “natürliche” Weise Monolithen.
Das bedeutet: Wenn Microservices alles von Frontend bis Datenhaltung abdecken sollen, dann gehört es zu den Grundvoraussetzungen, dass alle Fachgebiete im Team zusammenarbeiten. Das sind sogenannte “cross-funktionale Teams”.
Interessanterweise gilt das Gesetz von Conway auch umgekehrt: Die Struktur der Software bestimmt, wie Teams und ihre Mitglieder kommunizieren müssen.
Da Teamgrenzen die Kommunikation bremsen, ist es nicht sinnvoll mehrere Teams an einem Microservice gemeinsam arbeiten zu lassen. Beim umgekehrten Extrem, ein Microservice pro Entwickler, sind häufige Abstimmungen zwischen eigenständigen Services mit Dokumentation und Spezifikation notwendig. Dies bremst Teamwork effektiv aus, das Team wird zu einer Ansammlung von Ein-Personen-Silos.
Zusammengefasst kann man sagen: Für erfolgreiche Microservices sind alle drei Faktoren wichtig: Der Bounded Context, die Teamstruktur und auf Platz drei die Technik.
Team Structures
When it comes to teams, there's no getting around Conway's Law. It states: organizations design systems that map the organization's communication structures.4 In short, "You're gonna ship your org chart."
Suppose an organization separates its staff by technical specialty: frontend team, backend team, database team, ... For example, this encourages frontend staff to communicate among themselves so that they share informally and work closely together. However, it insulates them from the back-end staff, with whom they therefore coordinate a lot on documentation and specification. Which leads to the software being structured in exactly the same way: Strictly separate but very broad layers. This organization produces monoliths in a very "natural" way.This means that if microservices are to cover everything from the frontend to data storage, then one of the basic requirements is that all specialist areas work together as a team. These are so-called "cross-functional teams."
Interestingly, Conway's law also applies in reverse: the structure of the software determines how teams and their members must communicate.
Since team boundaries slow down communication, it doesn't make sense to have multiple teams working together on a microservice. At the opposite extreme, one microservice per developer, frequent coordination between independent services with documentation and specification is necessary. This effectively slows down teamwork, the team becomes a collection of one-person silos.
In summary, all three factors are important for successful microservices: the bounded context, the team structure, and in third place, technology.
Smaller than a bounded context
Back to the original, predominantly technical, example. Each service is a small CRUD backend that barely implements any independent technical concepts or rules. The two contexts of sales and shipping are distributed across service boundaries. There are two names for this. The more advantageous one is "Nanoservices".
Nanoservices cover individual technical functions. But the technical context is not gone! This leads to some problems. For example, the distributed transaction, which not only closely couples two services, but also introduces new sources of error.
The contradiction between domain-oriented context but technical separation leads to three things, among others:
- Business contexts inevitably tightly couple nanoservices technically. Domain-specific changes usually entail modification, testing, and deployment of multiple nanoservices.
- Domain-specific contexts are scattered, and nanoservices have weak cohesion as a result. To understand cohesion, you open more code bases, read more logs, etc.
- All the problems of distributed systems (asynchronous clocks, events in different order, unreliable network, partial failures) are brought into the system at very deep level.
Another view is that this architecture combines the bad features of monoliths with those of distributed systems. The less advantageous name is therefore "distributed monolith".
Kleiner als ein Bounded Context
Zurück zum ursprünglichen, vorwiegend technisch geschnittenen Beispiel. Jeder Service ist ein kleines CRUD-Backend, dass kaum eigenständige fachliche Konzepte oder Regeln umsetzt. Die beiden Kontexte Verkauf und Versand sind über Servicegrenzen verteilt. Dafür gibt es zwei Namen. Der vorteilhaftere ist “Nanoservices”.
Nanoservices decken einzelne technische Funktionen ab. Der fachliche Zusammenhang ist aber nicht weg! Das führt zu einigen Problemen. Zum Beispiel zur verteilten Transaktion, die nicht nur zwei Services eng koppelt, sondern auch neue Fehlerquellen einführt.
Der Widerspruch zwischen fachlichem Zusammenhang aber technischer Trennung führt, unter anderem, zu drei Dingen:
- Fachliche Zusammenhänge koppeln Nanoservices zwangsläufig auch technisch eng. Fachliche Änderungen ziehen meistens Änderung, Test und Deployment mehrerer Nanoservices nach sich.
- Fachliche Zusammenhänge sind verstreut, Nanoservices haben dadurch eine schwache Kohäsion. Um Zusammenhänge zu verstehen, öffnet man mehr Codebasen, liest mehr Logs, usw.
- Alle Probleme verteilter Systeme (asynchrone Uhren, Events in verschiedener Reihenfolge, unzuverlässiges Netzwerk, Teilausfälle) werden auf ganz tiefe Ebene ins System hereingeholt.
Eine andere Sichtweise ist, dass diese Architektur die schlechten Eigenschaften von Monolithen mit denen verteilter Systeme vereinigt. Der weniger vorteilhafte Name ist daher “verteilter Monolith”.
The three factors as a framework for thinking
If one wants to avoid the disadvantages of nanoservices, the question is how to proceed.
Suppose there is ambiguity about the three factors, for example at the beginning of projects. The greater the lack of clarity, the greater the risk of making decisions out of ignorance that later turn out to be suboptimal and difficult to reverse.
Here, it can help to focus first on working out bounded contexts and team structures, making decisions only as far as clarity allows or postponing some for the time being.5 One possible implementation of this is to start with a non-distributed, modular system. This is easier to adapt to the current state of knowledge and to separate later.6
If you are already in a situation where contradictions between the technical context and the technical separation lead to problems, the three factors described can serve as a "thinking framework". With their help, the contradictions can be identified and approaches to their elimination can be developed.
Die drei Faktoren als Denkrahmen
Wenn man die Nachteile von Nanoservices vermeiden möchte, stellt sich die Frage wie man vorgehen kann.
Angenommen, bei den drei Faktoren besteht Unklarheit, zum Beispiel zu Beginn von Projekten. Je größer die Unklarheit, desto größer die Gefahr, aus Unwissenheit Entscheidungen zu treffen, die sich später als suboptimal und schwer rückgängig zu machen herausstellen.
Hier kann es helfen, den Fokus zunächst darauf zu setzen Bounded Contexts und Teamstrukturen auszuarbeiten, Entscheidungen nur so weit zu treffen, wie die Klarheit es zulässt oder manche vorläufig zu vertagen.5 Eine mögliche Umsetzung davon ist, mit einem nicht-verteilten, modularen System anzufangen. Dieses lässt sich einfacher an den aktuellen Kenntnisstand anpassen und später trennen.
Falls man bereits in der Situation ist, dass Widersprüche zwischen fachlichem Zusammenhang und technischer Trennung zu Problem führen, können die beschriebenen drei Faktoren als “Denkrahmen” dienen. Mit ihrer Hilfe können die Widersprüche erkannt und Ansätze zur Behebung entwickelt werden.
Artikel von: Christian Harnisch
Article by: Christian Harnisch
[1] Domain-Driven Design: Tackling Complexity in the Heart of Software, Chapter 14: Maintaining Model Integrity
[2] Create Read Update Delete, Wikipedia
[3] Microservices, Martin Fowler
[4] Law of Conway, Wikipedia
[5] Lean Principles 4 Defer Commitment, 101 Ways
[6] MonolithFirst, Martin Fowler
[1] Domain-Driven Design: Tackling Complexity in the Heart of Software, Chapter 14: Maintaining Model Integrity
[2] Create Read Update Delete,, Wikipedia
[3] Microservices, Martin Fowler
[4] Law of Conway, Wikipedia
[5] Lean Principles 4 Defer Commitment, 101 Ways
[6] MonolithFirst, Martin Fowler