Der perfekte Microservice

Architektur/Methoden  –  8 Kommentare

Die Wunderwaffe Microservices ist in aller Munde. Was in den vergangenen Jahren Webgiganten wie Netflix, Amazon oder Twitter erfolgreich vorgemacht haben, möchten nun auch kleinere Unternehmen adaptieren: Weg vom starren, nicht wartbaren Monolithen, hin zu einer flexiblen, Microservice-basierten Architektur. Aber ist das wirklich so einfach?

Microservices zeichnen sich dadurch aus, dass jeder Service für sich genommen lediglich einen kleinen, in sich mehr oder minder geschlossenen Teil der Anwendung abbildet. Die durch ein Team umzusetzende Fachlichkeit ist in der Regel leicht zu verstehen und zu beherrschen. Selbst technologisch – "build", "test", "deploy", "run" und "monitor" – ist ein einzelner Service deutlich einfacher zu handhaben als ein Monolith. Im Idealfall ist ein Microservice unabhängig erweiterbar, austauschbar und skalierbar. Richtig umgesetzt funktioniert die Anwendung auch noch dann, wenn ein Service nicht verfügbar ist.

Schaubild zum Verständnis von Microseervices

So wie es scheint, bringen Microservices eine Menge Vorteile mit sich. Allerdings kommt ein Microservice per Definition selten allein. Die Komplexität der Anwendung verlagert sich, im Vergleich zum Monolithen, auf das korrekte Zusammenspiel der Services. Wie also müssen die Services geschnitten sein, um die eben aufgeführten Bedingungen – unabhängig erweiterbar, austauschbar und skalierbar – in einer auf Microservices basierenden, stark verteilten Architektur zu erfüllen? Und wie kommt man zu diesem "golden Schnitt"?

Diese Fragen sollen an einem Beispiel dokumentiert werden, bei dem ein klassischer Web-Shop auf Basis von Microservices realisiert wird. Zur Fachlichkeit gehören unter anderem Kunden, Produkte und Warenkörbe.

Eine Übersicht der verschiedenen, zum größten Teil im Artikel auch genannten Pattern zur Sicherstellung der Modellintegrität bei größeren, verteilten Modellen in DDD (nach Eric Evans)

Eine wesentliche Voraussetzung für einen guten Service ist die in sich geschlossene Fachlichkeit. In Anlehnung an Eric Evans' Domain Driven Design spricht man hier auch von einem Bounded Context. Natürlich existiert ein Service in der Praxis nie vollständig autark von anderen Services. Entsprechend wichtig ist ein gut durchdachtes Design der gemeinsamen Schnittstelle. Dabei geht es nicht nur um die Granularität der auszutauschenden Daten und der damit verbundenen Anzahl prozessübergreifender Service-Calls. Von ebenso großer Bedeutung sind die Stabilität der Schnittstelle und ein gut durchdachtes Vorgehen bei der Versionierung. Nur so lässt sich gewährleisten, dass Änderungen und Erweiterungen an der Schnittstelle nicht auch zwingend zu einer Anpassung und einem Redeployment aller nutzenden Services führen. Denn anders als bei internen Änderungen an einem Service sind bei einer Änderung einer Schnittstelle immer mindestens zwei Services betroffen.

Für das Design von Schnittstellen haben sich mehrere Patterns aus dem Domain-Driven-Design-Umfeld etabliert, die in der richtigen Kombination angewandt ein hoch flexibles Gesamtsystem ermöglichen.

Ein besonderes Augenmerk beim Design von Serviceschnittstellen ist auf die Objekte beziehungsweise Strukturen zu legen, die innerhalb einer Schnittstelle verwendet und somit zwischen den Services ausgetauscht werden. Also den Objekten, die mehr als ein Bounded Context benötigen. Um den Aufwand zum Befüllen der Schnittstelle auf der Seite, die den Microservice zur Verfügung stellt (Service Supplier), gering zu halten, ist es optimal, wenn die Schnittstellenobjekte direkter Teil ihres Domänenmodell sind. Das gilt auch dann, wenn der anfragende Service (Service Consumer) nicht alle zur Verfügung gestellten Attribute benötigt. Hier gibt es verschiedene Techniken, die Menge der zu übertragenden Attribute je nach Aufrufer zu optimieren.

Auf der aufrufenden Seite der Schnittstelle gibt es zwei Möglichkeiten im Umgang mit den Schnittstellenobjekten: Die eine ist, dass sie dort in ein eigenes Modell gemappt werden. Das Vorgehen heißt im Domain-Driven Design "Anti-Corruption Layer". Da es in der Regel mit einigem Aufwand verbunden ist, sollte es nur in Ausnahmefällen angewendet werden. Die zweite Möglichkeit ist, dass die
Domänenobjekte direkter Bestandteil des Domänenmodells der aufrufenden Seite sind. Letztere Variante nennt Eric Evans "Shared Kernel", da sich Supplier und Consumer ein Teil des Domänenmodells teilen.

Die Verwendung eines Shared Kernel scheint zunächst einmal den Grundprinzipien der Unabhängigkeit von Microservices entgegenzustehen und erfordert ein hohes Maß an Abstimmung zwischen den beiden Bounded Contexts bei der Weiterentwicklung dieses gemeinsamen Teilmodells. Gleichzeitig bietet ein Shared Kernel aber den enormen Vorteil, dass der Code zur Integration des Modells auf beiden Seiten der Schnittstelle vollständig entfällt. Wenn die Kommunikationskultur der zugehörigen Teams eine solche Art der Schnittstellengestaltung ermöglicht, ist diese Variante den anderen durchaus vorzuziehen. Selbstverständlich ist es so, dass ein Shared Kernel möglichst stabil bleiben und Änderungen an ihm möglichst abwärtskompatibel durchgeführt werden sollten.

Zwei Bounded Contexts mit einem Shared Kernel