Der perfekte Microservice

Objekte und Versionierung

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" 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.

Der Nutzer der Schnittstelle – a.k.a. Service Consumer – dagegen sollte möglichst tolerant beim Verarbeiten der gelieferten Informationen sein (Tolerant Reader). 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), 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.

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.

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.

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.