Auf dieser Seite
Eine Plattform-Bibliothek für zehn Microservices: was hineingehört
Zehn Microservices bedeuten ohne gemeinsame Grundlage zehnmal Fehlerbehandlung, zehnmal Logging und zehnmal dasselbe Upgrade. Jede Lösung sieht ein wenig anders aus, und im Betrieb merkt man das spätestens, wenn eine Anfrage durch vier Services läuft und in jedem anders protokolliert wird.
Ich war anderthalb Jahre Hauptentwickler einer solchen gemeinsamen Bibliothek samt Parent-POM für Maven, auf der zehn fachliche Services aufsetzen. Hier steht, was sich bewährt hat.
Was hineingehört
In die Bibliothek gehört, was jeder Service braucht und was in jedem Service gleich sein muss:
- Fehlerbehandlung. Eine Hierarchie aus Client- und Serverfehlern, einheitliche Fehlerantworten und die Übersetzung von Fehlern aufgerufener Services. Interne Details bleiben in der Antwort ausgeblendet.
- Logging. Strukturierte Logs mit festen Feldern, darunter eine Korrelations-ID, die jede Anfrage durch alle Services begleitet. Sensible Werte wie Schlüssel und Abfrageparameter werden maskiert.
- Resilienz. Ein Circuit Breaker ist für jeden ausgehenden Aufruf standardmäßig aktiv. Wer andere Werte braucht, überschreibt sie.
- Security. Die Auswertung der Informationen, die das vorgelagerte Gateway mitschickt, und die Weitergabe an aufgerufene Services.
- Testhilfen. Profile für Readiness- und Lasttests und Basisklassen für Sicherheitstests.
Der Maßstab ist einfach: Ein neuer Service soll Beobachtbarkeit, Resilienz und Security geschenkt bekommen, ohne eigenen Code dafür.
Eine Regel, die viel Diskussion spart
Manche Dinge lassen sich nur zentral durchsetzen. Zum Beispiel diese:
@ExceptionHandler(ServerError.class)
ResponseEntity<ApiError> handle(ServerError error) {
log.error("Unerwarteter Serverfehler", error); // jeder 500er wird zum Alarm
return ResponseEntity.status(error.status()).body(ApiError.withoutInternals(error));
}
Jeder Fehler mit Status 500 erzeugt einen Log-Eintrag auf Fehlerstufe und damit einen Alarm. Niemand muss daran denken, es steht an einer Stelle.
Was nicht hineingehört
Die Versuchung ist groß, alles Gemeinsame in die Bibliothek zu legen. Zwei Dinge habe ich bewusst herausgehalten:
Fachlogik. Sobald die Bibliothek weiß, was eine Bestellung ist, hängen alle Services an einer fachlichen Entscheidung.
Die Anbindung eines bestimmten Systems. Die Anbindung des zentralen Backends lag zunächst in der Bibliothek. Ich habe sie in eine eigene Bibliothek ausgelagert, die heute der Großteil der Services nutzt. Services ohne diese Abhängigkeit bekommen sie so nicht mehr ungefragt mitgeliefert.
Releases ernst nehmen
Eine Bibliothek, von der zehn Services abhängen, ist ein Produkt mit Stakeholdern im eigenen Haus:
- Semantische Versionen, auch für große Sprünge. Ein Umbau der Paketstruktur war eine neue Hauptversion.
- Abgestimmte Releases über Bibliothek, Parent-POM und Testhilfen, damit Services nicht zwischen zwei Ständen hängen.
- Über 100 Releases in anderthalb Jahren. Kleine, häufige Schritte sind für die abhängigen Teams leichter als seltene große.
Der Hebel: ein Upgrade statt zehn
Der Nutzen zeigt sich beim großen Upgrade – damals zum Beispiel bei der Migration auf Spring Boot 3. Der Sprung von Spring Boot 2.7 auf 3.0 betrifft Namensräume, Security-Konfiguration und zahlreiche Abhängigkeiten. Er lief einmal in Bibliothek und Parent-POM. Die Services haben die neue Version übernommen und nur noch angepasst, was bei ihnen wirklich eigen war. Fünf weitere Upgrades bis 3.3 liefen genauso.
Ein Diagramm, das nicht veraltet
Ein Nebenprodukt, das ich nicht mehr missen möchte: Die Bibliothek erzeugt aus den konfigurierten Aufrufen automatisch ein Diagramm, welcher Service welchen anderen aufruft. Es stimmt immer, weil es aus derselben Quelle kommt wie das Verhalten: aus dem Code.
Mehr zum Hintergrund steht im Fallbeispiel.