Die Frage kommt selten als Frage. Sie kommt als Vorhaben: Wir wollen den Monolithen in Microservices zerlegen. Und sie kommt mit einer Zahl, die nie begründet ist, meistens zwischen acht und zwanzig.
Dieser Artikel beantwortet sie anders, als es die meisten Texte tun, und die Antwort ist unbequem für beide Lager. Der Zielzustand, der sich in gewachsenen Systemen bewährt, ist weder der Monolith noch eine Landschaft aus zwanzig Diensten. Er ist ein Monolith mit inneren Grenzen und zwei oder drei echten Diensten daneben, und zwar an den Stellen, an denen ein Dienst etwas kann, was ein Modul nicht kann.
Warum es fast immer gewollt wird
Der Wunsch entsteht selten aus der Architektur und fast immer aus vier Beobachtungen, die alle stimmen.
Ein Ausrollen dauert Stunden und alle warten. Ein Team blockiert das andere, weil beide dieselbe Datei anfassen. Ein Fehler an einer Stelle nimmt die ganze Anwendung mit. Und ein Teil des Systems braucht zehnmal so viel Rechenleistung wie der Rest, bekommt aber dasselbe.
Was daran auffällt: Drei der vier Beobachtungen sind Probleme der Auslieferung und der Organisation, nicht der Aufteilung. Ein langsames Ausrollen wird durch Teilung nicht schneller, sondern vervielfacht. Zwei Teams, die sich blockieren, blockieren sich über eine geteilte Datenbank genauso. Nur die vierte Beobachtung, der Teil mit dem anderen Lastprofil, ist ein echtes Argument für einen eigenen Dienst.
Deshalb steht am Anfang nicht der Schnitt, sondern eine ehrliche Zuordnung: Welches der vier Probleme haben wir, und löst ein Dienst es wirklich?
Was ein Dienst kostet, bevor er etwas leistet
Ein Dienst ist nicht ein Modul mit Netzwerk dazwischen. Er bringt einen festen Grundpreis mit, und der fällt unabhängig davon an, wie gut er geschnitten ist.
Er braucht einen eigenen Auslieferungsweg, eine eigene Überwachung mit eigenen Alarmen, einen Ort für seine Geheimnisse, eine Fehlerbehandlung für den Fall, dass der Aufrufer ihn nicht erreicht, und eine Antwort auf die Frage, wie man eine Anfrage über seine Grenze hinweg verfolgt. Dazu kommt der Teil, den niemand einplant: Eine fachliche Änderung, die zwei Dienste berührt, braucht Abstimmung, und Abstimmung ist der einzige Aufwand in einem Vorhaben, der sich nicht parallelisieren lässt.
Zwölf Dienste heißen deshalb nicht zwölfmal Modul, sondern zwölf Auslieferungswege, zwölf Alarmketten und eine Testumgebung, die nur vollständig funktioniert. Der Preis ist tragbar, wenn er sich auf viele Teams verteilt. Bei einem Team mit sechs Leuten trägt ihn niemand.
Was ein Modul schon kann
Der übersprungene Zwischenschritt ist die Grenze innerhalb der Anwendung. Sie kostet fast nichts und löst zwei der vier Beobachtungen von oben.
Ein Modul mit einer eigenen Schnittstelle nach außen, eigenen Tabellen und der Regel, dass niemand quer hineingreift, ist in derselben Anwendung möglich. Was fehlt, ist nur die Durchsetzung, und die ist eine Prüfung im Bau und keine Architektur:
// Ein Test, kein Kommentar. Die Grenze existiert nur, wenn
// etwas sie prueft; eine Absprache im Team haelt ein Jahr.
public function testBestellungGreiftNichtInsLager(): void
{
$verstoesse = [];
foreach ($this->dateienIn('src/Bestellung') as $datei) {
foreach ($this->importeIn($datei) as $import) {
// Erlaubt ist genau die oeffentliche Schnittstelle
// des Nachbarn, nicht seine Modelle und Tabellen.
if (str_starts_with($import, 'App\\Lager\\')
&& $import !== 'App\\Lager\\LagerApi') {
$verstoesse[] = "$datei -> $import";
}
}
}
self::assertSame([], $verstoesse, implode("\n", $verstoesse));
}Der Gewinn ist größer, als er aussieht. Zwei Teams arbeiten an getrennten Verzeichnissen, ohne dass eine Netzwerkgrenze dazwischen liegt. Und wenn später doch ein Dienst daraus wird, ist die Arbeit schon getan: Ein Modul mit einer sauberen Grenze lässt sich herauslösen, ein verwobenes nicht.
Das ist der eigentliche Grund, diesen Schritt nicht zu überspringen. Er ist entweder das Ziel oder die Vorarbeit, und in beiden Fällen ist er derselbe Aufwand.
Wann ein Dienst wirklich einer ist
Bleiben die Fälle, in denen ein eigener Dienst mehr leistet als ein Modul. Es sind vier, und sie haben gemeinsam, dass ein Modul sie nicht abbilden kann.
Ein anderes Lastprofil. Ein Teil braucht zehnmal so viele Instanzen wie der Rest, oder er braucht sie zu anderen Zeiten. Getrennt skalieren geht nur getrennt.
Eine andere Sprache. Wenn ein Teil in Go um eine Größenordnung besser läuft als in PHP, ist das ein Grund. Wenn er nur in Go schöner aussieht, ist es keiner.
Ein anderer Auslieferungsrhythmus. Ein Teil, der mehrmals täglich ausgeliefert werden muss, während der Rest monatlich geht, oder umgekehrt ein Teil, der aus Zulassungsgründen selten geändert werden darf.
Eine andere Verfügbarkeitsanforderung. Der Teil, der auch dann antworten muss, wenn der Rest steht.
Was auf dieser Liste nicht steht: Teamgröße, Sauberkeit, Modernität und die Zahl der Zeilen. Wer keinen der vier Gründe nennen kann, baut ein Modul.
Die Zahl, die im Vorhaben steht
Bleibt die Zahl aus dem ersten Absatz. Sie steht in fast jedem solchen Vorhaben, und sie ist nie hergeleitet.
Hergeleitet wird sie so: Man zählt die Gründe, nicht die Bereiche. Drei Teile mit einem anderen Lastprofil und einer mit einem anderen Auslieferungsrhythmus ergeben vier Dienste. Alles andere bleibt im Kern. Kommt dabei eine Zahl unter drei heraus, ist das kein Zeichen von Mutlosigkeit, sondern das übliche Ergebnis.
Eine zweite Größe begrenzt die erste, und sie wird regelmäßig übersehen: Die Zahl der Teams ist eine Obergrenze, kein Ziel. Ein Dienst, für den sich niemand zuständig fühlt, wird nicht aktualisiert, nicht überwacht und bei der nächsten Schemaänderung vergessen. Zwei Teams können nicht zwölf Dienste betreiben; sie können zwölf Dienste bauen und danach zwölf Dienste vor sich herschieben.
Wer an dieser Stelle eine Zahl nennen muss, nennt deshalb nicht die Zielarchitektur, sondern den nächsten Schritt: einen Dienst, mit einem der vier Gründe, und danach dieselbe Frage noch einmal.
Wie der schrittweise Umbau technisch abläuft, steht in Strangler Fig Pattern in der Praxis.
Der Weg, wenn die Entscheidung steht
Ist einer der vier Gründe da, folgt der Umbau einer Reihenfolge, die sich nicht umstellen lässt.
Zuerst bekommt der Bereich eine Grenze innerhalb der Anwendung, und zwar geprüft. Dann bekommt er seine eigenen Daten, was der eigentliche Aufwand ist und in Wochen rechnet, nicht in Tagen. Dann wird er an derselben Stelle noch einmal gebaut, hinter einer Umlenkung, mit einem Vergleichslauf gegen das Alte. Und erst dann wird umgeschaltet.
Wer diese Reihenfolge umdreht und mit dem neuen Dienst anfängt, baut ihn gegen ein Modell, das er noch nicht kennt, und merkt in der Mitte, dass die Daten nicht mitkommen.
Was entsteht, wenn die Schnitte falsch liegen, steht in Der verteilte Monolith.
Der Zielzustand, den niemand vorzeigt
Am Ende steht in den Systemen, die ich gesehen habe, immer dasselbe Bild: eine Anwendung mit fünf bis zehn sauber getrennten Modulen, daneben zwei oder drei Dienste mit einem der vier Gründe, und eine gemeinsame Auslieferung für den Kern.
Das ist kein Kompromiss aus Erschöpfung. Es ist der Zustand mit dem besten Verhältnis aus Handlungsfähigkeit und Betriebsaufwand, und er hat einen Vorteil, den eine Landschaft aus zwanzig Diensten nicht hat: Man kann ihn in beide Richtungen verlassen. Ein Modul wird zum Dienst, wenn ein Grund dazukommt. Ein Dienst wird wieder zum Modul, wenn der Grund entfällt.
Der Begriff dafür steht im Glossar, und die Frage, wann ein Dienst neben dem Bestand entsteht, ist eine eigene Leistung.
Dieser Artikel setzt eine Reihe über Systeme fort, die es schon gibt. Der Rückblick über die ersten drei Monate ordnet ihre Artikel nach Anlass.

