zurück zum Artikel

Der perfekte Microservice

Architektur/Methoden
Der perfekte Microservice

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
Schaubild zum Verständnis von Microservices

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 "goldenen 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 Ü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)

Design der Schnittstellen

Eine wesentliche Voraussetzung für einen guten Service ist die in sich geschlossene Fachlichkeit. In Anlehnung an Eric Evans' Domain Driven Design [1] 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.

Microservices (0 Bilder) [2]

[3]

Shared Kernel als Teilmenge des Domänenmodells

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
Zwei Bounded Contexts mit einem Shared Kernel

Objekte und Versionierung

Gemeinsame Objekte

Wenn man im Rahmen des Shared Kernel von gemeinsamen Objekten redet, ist ausdrücklich darauf hinzuweisen, dass sich dabei nicht auf ein Datenaustauschformat oder eine Programmiersprache festgelegt wird. Die Objekte können je nach Implementierungsvielfalt der aufrufenden und des aufgerufenen Microservices durchaus in unterschiedlichen Repräsentationen vorliegen. Eine Repräsentation im JSON- und XML-Format ist die Regel, aber auch eine Java-, PHP- oder C#-Version der Objekte ist sinnvoll, falls die beteiligten Microservices in der entsprechenden Sprache implementiert sind. Gleiches gilt auch für jede erdenkliche Skriptsprache.

Microservices lassen sich also unabhängig voneinander weiterentwickeln und in Betrieb nehmen. Dabei ist immer damit zu rechnen, dass ein Service durch einen "veralteten" Consumer aufgerufen wird. Das gilt selbst dann, wenn der Supplier parallel mehr als eine Service-Version unterstützt. Es stellt sich somit die Frage, wie sich gewährleisten lässt, dass die Anwendung auch in einem solchen Fall noch korrekt funktioniert.

Generell gilt, dass eine Schnittstelle aus Sicht des Anbieters so sauber und stabil wie irgend möglich definiert sein sollte. Dabei ist es hilfreich, wenn der Service Supplier seine Consumer beziehungsweise deren Erwartungshaltung an die Schnittstelle kennt. Als Pattern hat sich hierbei das Service Evolution Pattern "Consumer-Driven Contract [4]" etabliert. Bei ihm teilen die Consumer dem jeweiligen Supplier ihre Erwartung an die Schnittstelle mit und definieren so in Summe die tatsächliche Schnittstelle. Als positives Abfallprodukt können sowohl Supplier als auch Consumer ihre aktuelle Schnittstelle gegen diese Definition testen. Zur Unterstützung existieren Tools wie Pact [5].

Der Nutzer der Schnittstelle – a.k.a. Service Consumer – dagegen sollte möglichst tolerant beim Verarbeiten der gelieferten Informationen sein (Tolerant Reader [6]). Kommen zum Beispiel lediglich Attribute hinzu oder ändert sich die interne Struktur der Daten, sollte die konsumierende Schnittstelle auch weiterhin die Daten verarbeiten können. In der Praxis hat sich zusätzlich das Versionierungskonzept Semantic Versioning etabliert (SemVer [7]), bei dem die jeweilige Stelle der Versionsnummer signalisiert, ob Service-Supplier und -Consumer noch kompatibel sind oder nicht. Eine gute Erläuterung dazu findet sich im Blog von Hugo Giraudel [8].

Natürlich ist damit zu rechnen, dass ein Service vorübergehend gar nicht erreichbar ist oder aber Daten liefert, mit denen der Consumer nichts anfangen kann. Auch für diesen Fall sollte über Retry-Mechanismen oder Bereitstellung beziehungsweise Verwendung von Standardwerten Vorsorge getroffen werden.

Versionierung der Schnittstellen

Selbst wenn man Schnittstellen-Supplier und -Consumer so fehlertolerant wie möglich gestaltet, kann es immer wieder zu inkompatiblen Änderungen in der Schnittstelle kommen. Damit das reibungslos funktioniert, ist ein besonderes Augenmerk auf die Versionierung der Schnittstellen zu legen. Denn auch bei einer Weiter- oder Neuentwicklung sind alte Schnittstellenversionen bis zu einem gewissen Grad weiter zu unterstützen.

Um Abwärtskompatibilität seitens des Suppliers sicherzustellen, gibt es mehrere Möglichkeiten, die unter anderem auch vom verwendeten Datenaustauschformat abhängen. Je schemaloser das Datenformat, desto einfacher ist das Sicherstellen der Abwärtskompatibilität durch Implementierung der beschriebenen Fehlertoleranz.

In JSON ist es zum Beispiel bei der Umbenennung eines Attributs leicht möglich, für einen gewissen Zeitraum das Attribut sowohl mit seinem alten als auch mit seinem neuen Namen über die Schnittstelle zu liefern. Wird bei einem Aufrufparameter ein Attribut umbenannt, sollte der aufgerufene Code mit beiden Attributnamen zurechtkommen.

In schemabasiertem XML gestaltet sich das Ganze schon deutlich schwieriger. Kommt es zur Umbenennung des Attributs, muss es automatisch ein neues Schema geben. Wichtig ist dann, dass der Schnittstellen-Supplier für einen Übergangszeitraum mit beiden Schemata umgehen kann. Hier empfiehlt es sich, für jeden Aufruf eine Schemaversionserkennung zu implementieren. Man kann dadurch den Code, der mit dem alten Schema umgehen kann, von dem Code trennen, der die aktuelle Version der Schnittstelle implementiert. Als gute Strategie hat sich erwiesen, dass der Code, der die alte Version der Schnittstelle implementiert, die Daten in die neue Version konvertiert und sie dann aufruft. Das Ergebnis ist hierauf von neu nach alt zurückzukonvertieren.

Zwar ist es so theoretisch möglich, beliebig viele Schnittstellenversionen zu bedienen, in der Praxis wird das aber auf die Dauer unwartbar. Supplier und Consumer sollten sich also auf einen Zeitraum einigen, in dem die alte Version der Schnittstelle noch funktioniert und in dem der Consumer Zeit hat, die neue zu implementieren. Danach ist eine weitere Unterstützung der alten Schnittstelle nicht mehr garantiert. Auf diese Weise ist sichergestellt, dass sich alte Zöpfe beizeiten abschneiden lassen.

Versionierung (6 Bilder) [9]

[10]

Versionierung

Die ersten drei Bilder zeigen den im Text beschriebenen Versionierungsansatz, bei dem kurzfristig zwei Versionen desselben Services bzw. zwei Endpoints parallel zur Verfügung gestellt werden.

Im Zentrum des Ganzen

Hat man zwischen den Bounded Contexts Shared Kernels gebildet, verdeutlicht das in der Regel schnell, dass es einen Teil von Domänenobjekten gibt, die in mehreren, wenn nicht in nahezu allen Shared Kernels enthalten sind. Bei diesen Objekten handelt es sich um den Kern der Fachlichkeit der Applikation. Bildet sich ein solcher Kern heraus, ist es sinnvoll, ihn auch explizit zu benennen, für alle Bounded Contexts gemeinsam zu definieren, weiterzuentwickeln und damit auch zu versionieren. Im Domain Driven Design wird dieser Kern "Core Domain" genannt.

Im Beispiel sind sicherlich der Kunde und das Produkt Teil des fachlichen Kerns. Dabei ist aber zu beachten, dass es sich jeweils nicht um die kompletten Kunden- und Produktdaten handelt, wie sie in der Kundenverwaltung oder in der Produktpflege benötigt werden. Auch auf Attributebene ist darauf zu achten, dass nur die wesentlichen Eigenschaften der Objekte in die Core Domain wandern. Beim Kunden sind das wahrscheinlich nur die Kundennummer und ein Anzeigename. Beim Produkt kommen zu Artikelnummer und Name eventuell noch eine Kurzbeschreibung und ein Preview-Image hinzu.

Definiert man die Core Domain sinnvoll – also nicht zu groß und nicht zu klein – und beachtet hier zusätzlich die oben genannten Regeln der Schnittstellenversionierung, läuft das darauf hinaus, dass die meisten über Schnittstellen übertragenen Daten nur genau diese Core-Objekte sind.

Trennungen

Trennen nach Domänenobjekten?

Wie lassen sich aber Microservices fachlich sinnvoll zuschneiden? Ein zunächst naheliegender Schnitt der Microservices ist der nach Domänenobjekten. Für die Beispieldomäne hieße das zum Beispiel, dass es einen Kunden- einen Warenkorb- und einen Produkt-Service gibt. Bei näherer Betrachtung erweist sich dieses Schneiden aber als suboptimal. Insbesondere beim Blick auf den Warenkorb-Service wird schnell klar, dass dieser direkt von den beiden anderen Services abhängt und daher bei jeder Änderung einer der Services mit zu ändern ist.

Eine solche Abhängigkeit ist auf jeden Fall zu vermeiden, da man sich so den zusätzlichen Overhead für eine Microservice-Architektur einkaufen würde, ohne dabei im Gegenzug die gewünschte Unabhängigkeit bezüglich Entwicklung und Bereitstellung der Services zu gewinnen.

Trennung nach Use Cases

Wie lassen sich die Services also so abgrenzen, dass sie möglichst wenige fachliche Berührungspunkte und somit auch wenig Abhängigkeiten untereinander besitzen? Wenige Berührungspunkte bedeutet in dem Kontext, dass sich die Services zur Laufzeit selten gegenseitig aufrufen müssen beziehungsweise Änderungen an einem Service nicht automatisch auch Änderungen an aufrufenden Services mit sich bringen. Um festzustellen, wie häufig sich Microservices zur Laufzeit gegenseitig aufrufen, reicht ein Blick auf die Use Cases. Idealerweise lässt sich ein Use Case mit möglichst wenig (am besten gar keinen) Microservice-Interaktionen abarbeiten. Der ideale Microservice ist demnach für einen kompletten Use Case verantwortlich und benötigt für dessen Abarbeitung keinen weiteren Microservice.

In der Praxis lässt sich das selten realisieren. Dennoch wird deutlich, dass das Abgrenzen der Microservices nach Use Cases ein sinnvolles Vorgehen ist. Überträgt man das auf den Web-Shop, zeigt sich, dass die Probleme des Aufteilens nach Domänenobjekten dadurch behoben werden. Betrachtet man einen klassischen Use Case "Kunde kommt auf die Website, sucht ein Produkt und kauft es", gibt es Teilschritte, die unterschiedliche Microservices problemlos behandeln können. Die Produktsuche kommt zum Beispiel vollständig ohne Abhängigkeiten zu den anderen Services aus. Und auch der zum Ändern von Kundendaten benötigt keine Abhängigkeiten zu anderen Services.

Es sei daran erinnert, dass die Warenkorbverwaltung beim Aufteilen nach Domänenobjekten der kritische Service war, da er starke Abhängigkeiten zu anderen Microservices besitzt. Orientiert man sich allerdings an die Abschnitte über Shared Kernel und Core Domain, wird deutlich, dass die Warenkorbverwaltung nur einen kleinen Teil der Domänenobjekte von Kundenverwaltung und Produktsuche benötigt, nämlich die Core-Versionen von Kunde und Produkt. Bei sinnvoller Modellierung der Core Domain hätte also die Warenkorbverwaltung keine Abhängigkeit zur Kundenverwaltung oder Produktsuche, sondern nur zur Core Domain. Der Knopf "In den Warenkorb legen" besteht lediglich aus einem Aufruf der Warenkorbverwaltung, der ein Core Product übergeben wird.

Dass man dennoch nicht ganz ohne Abhängigkeiten zwischen Microservices auskommt, verdeutlicht das Betrachten des Bestellvorgangs: Hat sich der Kunde einmal entschieden, den Inhalt seines Warenkorbs zu bestellen, werden die benötigten Daten an den Checkout-Microservice übergeben. Auch hier ist festzustellen, dass lediglich Core-Kunde und -Produkt zu übergeben sind. Der Checkout-Microservice benötigt nun einen Teil der Daten, die im Kundenverwaltungs-Microservice eingegeben wurden: Lieferadresse, Zahlungsinformationen usw. Sie sollen sich auch im Bestellprozess noch ändern lassen. Das führt zur Frage der Datenhaltung: Wie sollen Daten in Microservices gespeichert werden? Wer darf Daten ändern? Was stellt die Datenkonsistenz sicher?

Wohin mit den Daten?

Ein Gendankenspiel vorweg: Welcher Vorteil wäre erreicht, wenn jeder Microservice für sich autark – Stichwort "Bounded Context" – auf einem eigenen Domänenmodell agiert, sich aber alle Services eine Datenquelle, zum Beispiel eine Datenbank und deren Tabellen, teilen? Keiner? Korrekt! Eine Änderung der Datenbank würde in der Regel auch die Änderung mehrerer Services nach sich ziehen. Optimal wäre es dagegen, wenn jeder Microservice auf seiner eigenen Datenquelle arbeiten könnte.

Besitzt ein Service eine eigene Datenquelle, organisiert er sich für das Abarbeiten eines Use Case lediglich rudimentäre Informationen von anderen Services – Stichwort "Core Domain" – und nimmt sie als Basis, um in seinen eigenen Datenquellen nach der für ihn relevanten Modellrepräsentation zu suchen. Ein Liefer-Service könnte so zum Beispiel beim Kunden-Service via Core-Domain-Objekt "Kunde" die für den Bestellvorgang relevante Kundennummer erfragen und im Anschluss mit dieser Information innerhalb seiner eigenen Datenquelle nach passenden Lieferadressen suchen und sie zur Auswahl anbieten.

Es stellt sich die Frage, wie der Service an die Daten für seine Datenquellen kommt und sich diese konsistent halten lassen. In der Praxis wird das zum Beispiel durch replizierte Datenbanken/-tabellen, Event-Mechanismen und Microservice-eigene Caches erreicht. Im Beispiel würde die Datenquelle des Liefer-Services durch die Stammdaten des Kunden-Services gefüllt werden. Ändern sich Lieferdaten im Kunden-Service, werden diese Änderungen über Replikation oder Event-Mechanismen an den Liefer-Service übermittelt.

Wichtig ist dabei, dass zu jedem Zeitpunkt klar definiert ist, was die aktuelle Version eines Datensatzes ist und welcher Service zu welchem Zeitpunkt die Hoheit über die Daten hat. Am einfachsten wird das erreicht, indem man für jeden Datensatz einen Microservice definiert, der für ihn verantwortlich ist. Im Beispiel wäre das für die Kundendaten der Kunden-Service, für die Produktdaten der Produkt-Service und so weiter. Ändert nun ein anderer Microservice Daten – der Kunde gibt zum Beispiel im Rahmen des Checkout-Prozesses (Checkout-Service) eine neue Lieferadresse an –, muss die Änderung der Daten über den verantwortlichen Microservice geschehen, im Beispiel also den Kunden-Service. Auf diese Weise ist sichergestellt, dass er immer die aktuellste Version des Datensatzes verwaltet.

Eine verteilte Datenhaltung auf Basis replizierter Daten in einem stark verteilten System führt unweigerlich zur Frage der Datenkonsistenz, da verteilte Transaktionen in diesem Kontext nur schwer zu realisieren sind. In der Praxis wird bei stark verteilten Systemen, wie sie im Falle von Microservices vorliegen, gerne auf eine etwas abgeschwächte Variante des ACID-Paradigma zurückgegriffen: BASE (Basic Availability, Soft-State, Eventually Consistent).

Sie legt nahe, dass ein Zugriff auf gewünschte Daten zu jedem Zeitpunkt möglich ist, diese aber eventuell nicht 100 Prozent aktuell sein könnten. Interessant ist, dass viele reale Use Cases ebenso funktionieren. Ein schönes Beispiel hierfür finden sich im Blog-Beitrag "Starbucks does not use Two-Phase Commit [11]", in dem Gregor Rambling beschreibt, wie sich dem Problem der fehlenden Transaktionen mit dem Conversation Pattern [12] begegnen lässt.

Zuschneiden der UI

Eine besondere Herausforderung beim "richtigen" Zuschneiden von Microservices stellen die zugehörigen Nutzerschnittstellen dar. Hier stellt sich die Frage, ob die UI als direkter Bestandteil des Services gesehen wird oder nicht. Trennt man die Microservices strikt nach Use Cases, ist es erst mal naheliegend, dass sie auch jeweils die für ihren Use Case zugehörige UI mitliefern. Das hat mehrere Vorteile: Zunächst einmal ist eine einheitliche User Experience innerhalb des Use Case gewährleistet. Die Umsetzung der UI erfolgt dabei durch das Team, das sich fachlich am stärksten mit dem Use Case beschäftigt hat. Da sich die UI auch als Consumer eines Services ansehen lässt, wird in dem eben beschriebenen Szenario – Microservice(-Team) liefert die zugehörige UI – die Entwicklungsperformance deutlich erhöht, da das Team die meisten Schnittstellen und Daten des Microservice zur Verfügung stellt, das sie auch verwendet. Ein weiterer positiver Nebeneffekt ist zudem, dass weniger Schnittstellen für externe Services benötigt werden.

Der große Nachteil dieses Ansatzes ist die über Microservice-Grenzen hinweg verteilte User Experience. Wenn jeder Microservice seine eigene UI liefert, können die einzelnen sich mitunter in "Look und Feel" deutlich unterscheiden. Dem Problem kann man nur mit einem ausführlichen Styleguide, inklusive gemeinsam genutzter Design-Ressourcen wie CSS und Images, sowie einer permanenten Überwachung seiner Einhaltung Herr werden. Ein weiterer Nachteil ergibt sich dadurch, dass die endgültige UI erst auf Seiten des Clients orchestriert wird, was zu unnötigen vielen Server-Calls über das relativ inperformante Internet führt.

Aus den oben genannten Gründen findet man daher in der Praxis häufig einen Ansatz, bei dem die UI separat entwickelt wird und via API-Gateway [13] auf das Backend zugreift. Das Gateway wiederum führt die eigentlichen Microservice-Calls aus, bereitet die Ergebnisse für die jeweiligen UIs auf und liefert das Ergebnis an diese aus. Als positiver Nebeneffekt des Ansatzes lassen sich die verschiedenen UI-Typen (z. B. für mobile Endgeräte, Web-Browser, Spielkonsolen, TVs) mit unterschiedlichen Daten beziehungsweise Visualisierungen versehen.

Neben den beiden genannten, gegensätzlichen Ansätzen existiert noch eine dritte Variante. Bei ihr stellen die einzelnen Microservices lediglich UI-Komponenten (z. B. in Form von HTML-Widgets) zur Verfügung, die eine zentrale UI verwenden kann.

Zusammenspiel zwischen UI und Microservice (4 Bilder) [14]

[15]

Zusammenspiel zwischen UI und Microservice

Diese Bilderstrecke zeigt die verschiedenen im Text genannten Varianten zum Zusammenspiel zwischen UI und Microservice.

Für welche der Varianten man sich entscheidet, hängt, wie so oft, von den Gegebenheiten ab. Wie viele UIs sind zur Verfügung zu stellen? Welche Anforderungen existieren an sie? Und last, but not least wie sind die einzelnen Teams aufgestellt, welche die Microservices und die UI umsetzen sollen.

Fazit

Eine Microservice-Architektur macht nur wirklich Sinn, wenn es gelingt, die Services so zuzuschneiden, dass sie fachlich möglichst unabhängig voneinander sind. Anders formuliert sollte ein Service eine in sich geschlossene Fachlichkeit – Bounded Context – abbilden. In der Praxis hat sich gezeigt, dass dabei die naheliegende Einteilung nach Domänen-Objekten eher selten zum Ziel führt. Passender scheint eine Einteilung nach Use Cases zu sein.

Die notwendige Kommunikation der Microservices untereinander sollte so klar definiert wie notwendig und so flexibel und fehlertolerant wie möglich sein. Patterns wie Tolerant Reader und Semantic Versioning, aber auch Consumer-Driven Contract können dabei helfen, Schnittstellen über lange Zeit kompatibel zu halten.

Eine besondere Herausforderung stellen die verteilte Datenhaltung und die damit verbundene Sicherstellung der Datenkonsistenz dar. Gelockerte Konsistenzregeln wie BASE und die damit verbundene Eventual Consistency in Verbindung mit Datenreplikationen, Event-Mechanismen und geschicktem Daten-Caching innerhalb der Microservices stellen in den meisten, praxisnahem Szenarien eine gangbare Lösung dar.

Wie beim Schneiden von Microservices mit der zugehörigen UI umgegangen werden soll, ist schon fast eine philosophische Frage. Die UI kann sich entweder aus den UI-Fragmenten der einzelnen Microservices zusammensetzen oder alternativ en block implementiert werden und lediglich – über den Umweg eines API-Gateways – die zur Aufbereitung der UI notwendigen Daten von den Microservices beziehen. Als dritte Alternative ist ein Kompromiss beider Varianten denkbar, bei dem jeder Microservice die passenden UI-Komponenten inklusive Daten liefert und eine übergeordnete UI-Instanz diese sinnvoll zusammensetzt. (ane [16])

Lars Röwekamp
ist Gründer des IT-Beratungs- und Entwicklungsunternehmens open knowledge GmbH und beschäftigt sich im Rahmen seiner Tätigkeit mit der eingehenden Analyse und Bewertung neuer Software- und Techniktrends. Ein besonderer Schwerpunkt seiner Arbeit liegt derzeit in den Bereichen Enterprise und Mobile Computing.

Arne Limburg
ist Enterprise Architect bei der open knowledge GmbH in Oldenburg. Er verfügt über mehrjährige Erfahrung als Entwickler, Architekt und Trainer im Enterprise-Umfeld. Darüber schreibt er regelmäßig Artikel, spricht auf Konferenzen und führt Workshops durch. Außerdem ist er im Open-Source-Bereich tätig, unter anderem als PMC Member von Apache OpenWebBeans und Apache Deltaspike und als Urheber und Projektleiter von JPA Security.


URL dieses Artikels:
http://www.heise.de/-3091905

Links in diesem Artikel:
[1] http://dddcommunity.org
[2] https://www.heise.de/developer/bilderstrecke/bilderstrecke_3091939.html?back=3091905
[3] https://www.heise.de/developer/bilderstrecke/bilderstrecke_3091939.html?back=3091905
[4] http://martinfowler.com/articles/consumerDrivenContracts.html
[5] https://github.com/DiUS/pact-jvm
[6] http://servicedesignpatterns.com/WebServiceEvolution/TolerantReader
[7] http://semver.org
[8] http://www.sitepoint.com/semantic-versioning-why-you-should-using/
[9] https://www.heise.de/developer/bilderstrecke/bilderstrecke_3091950.html?back=3091905
[10] https://www.heise.de/developer/bilderstrecke/bilderstrecke_3091950.html?back=3091905
[11] http://www.enterpriseintegrationpatterns.com/ramblings/18_starbucks.html
[12] http://www.enterpriseintegrationpatterns.com/ramblings/09_correlation.html
[13] http://microservices.io/patterns/apigateway.html
[14] https://www.heise.de/developer/bilderstrecke/bilderstrecke_3091963.html?back=3091905
[15] https://www.heise.de/developer/bilderstrecke/bilderstrecke_3091963.html?back=3091905
[16] mailto:ane@heise.de