Read in English · Leer en español · Lire en français
Abschnitt 5.8 von 5.8 · Mittelstufe · 24 Min. Lesezeit
Wann ein Monolith die richtige Antwort ist
Videolektion
5 Min. · auf Englisch gesprochen, mit UntertitelnNur anhören (Audio)
Transkript lesen
Bevor Kapitel sechs das Netzwerk überquert, lohnt sich die direkte Frage: Wann ist ein Monolith die richtige Antwort? Sie sehen die Argumente, wie weit ein Monolith skalieren kann, und die Belege dafür, dass es Zeit für eine Aufteilung ist.
Ein neues Restaurant beginnt meist mit einer Küche, in der jeder Koch jeden anderen sehen und ihm helfen kann. Am ersten Tag fünf getrennte Küchen in der Stadt zu eröffnen, jede mit eigenem Personal, eigenen Lieferungen und eigener Leitung, würde weit mehr kosten und jede Änderung an der Speisekarte zur Verhandlung machen. Erst wenn die eine Küche wirklich überlastet ist oder ein Gericht besondere Ausstattung braucht, ist eine weitere sinnvoll. Bei Software ist es ähnlich. Und denken Sie daran: Monolith beschreibt das Deployment, nicht die Qualität.
2015 skizzierte Martin Fowler die Microservice Premium. Die Skizze dient nur der Veranschaulichung, ohne echte Daten. Unten ist die Komplexität aufgetragen, an der Seite die Produktivität. Microservices bringen eigene Kosten mit: viele Einheiten deployen, Monitoring, Teilausfälle, versionierte APIs. Unterhalb einer gewissen Komplexität ist das eine Prämie, die nichts bringt. Ein Monolith wird mit wachsender Größe tatsächlich langsamer. Erst jenseits der Kreuzung zahlen sich Microservices aus. Fowler beobachtete außerdem, dass fast alle erfolgreichen Microservice-Systeme als zu groß gewordener Monolith begannen und fast alle von Grund auf als Microservices gebauten in ernste Schwierigkeiten gerieten. Gute Grenzen sind schwer zu finden, bevor Sie die Domäne verstehen.
David Heinemeier Hansson, der Schöpfer von Ruby on Rails, sprach sich in The Majestic Monolith für kleine Teams aus: Verteilen Sie Ihre Rechenarbeit nicht, wenn Sie es nicht wirklich müssen. Stefan Tilkov vertrat die Gegenseite: Einen Monolithen später aufzuteilen ist oft schwieriger als erwartet, weil seine Teile eng zusammenwachsen. Die beiden Positionen liegen näher beieinander, als es klingt. Beide sagen, dass die Grenzen das Schwierige sind, und ein Monolith, dessen Module vom ersten Tag an durchgesetzt werden, beantwortet den Großteil von Tilkovs Bedenken.
Wann ist ein Monolith also richtig? Bei einem kleinen Team: Die Kommunikationskosten, die Microservices beseitigen, sind gering, ihre Betriebskosten aber nicht. Bei einer unsicheren Domäne oder einem frühen Produkt. Eine Grenze zwischen Modulen zu verschieben ist ein Refactoring; zwischen Services ist es eine Migration. Wenn Sie hohe Konsistenz brauchen. Eine Datenbanktransaktion über Bestellungen und Zahlungen ist in einem Monolithen einfach und korrekt. Wenn die Betriebskapazität begrenzt ist, denn jeder Service braucht seine eigene Pipeline, sein Monitoring und Bereitschaftswissen. Und wenn Teile der Domäne viel miteinander kommunizieren. Prozessinterne Aufrufe dauern Nanosekunden bis Mikrosekunden; Netzwerkaufrufe dauern Millisekunden und können fehlschlagen. In jedem Fall sollte es ein modularer Monolith sein.
Eine verbreitete Annahme lautet, Monolithen könnten nicht skalieren. Sie skalieren weiter, als die meisten Systeme es je brauchen. Betreiben Sie viele identische Kopien hinter einem Load Balancer, was nur funktioniert, wenn die Anwendung zustandslos ist. Sessions und Caches liegen also in einem gemeinsamen Speicher, und jedes Replikat kann jede Anfrage bedienen. Alle Schreibzugriffe gehen an die Primärdatenbank, während Read Replicas, leicht verzögert, Bestellhistorie und Berichte bedienen. Und langsame Arbeit, etwa E-Mails und Auszahlungen, kommt in eine Queue. Worker-Prozesse übernehmen sie von dort. Sie führen denselben Code mit einem anderen Startbefehl aus, sodass Web- und Worker-Rolle getrennt skalieren.
Prüfen wir das mit Beispielzahlen. Ein Essenslieferungs-Start-up mit sieben Engineers erwartet innerhalb von zwei Jahren eine Spitze von zwölfhundert Anfragen pro Sekunde. Ein Replikat bewältigt hundertachtzig innerhalb seines Antwortzeitziels. Zwölfhundert geteilt durch hundertachtzig ist etwa sechs Komma sieben, also sieben Replikate. Mit etwa fünfzig Prozent Reserve, genug, um den Ausfall einer Availability Zone zu überstehen, sind es zur Spitzenzeit etwa elf. Etwa fünfzehn Prozent der Anfragen schreiben in die Datenbank, jede mit etwa drei Schreibvorgängen. Das sind etwa fünfhundertvierzig Schreibvorgänge pro Sekunde, gut machbar für ein einzelnes PostgreSQL-Primärsystem. Ein Cache für Speisekarten fängt die meisten Lesezugriffe ab, und zwei Read Replicas übernehmen Bestellhistorie und Berichte. Eine Codebasis, betrieben als etwa elf Web-Replikate plus Worker, erreicht also das Ziel. Das Architekturbudget fließt in Modulgrenzen und Zustandslosigkeit, nicht in die Aufteilung.
Wann ist es Zeit für eine Aufteilung? Wenn es Belege gibt, dass eine Deployment-Einheit die Einschränkung ist: Teams warten aufeinander, und kleine Korrekturen stehen tagelang hinter fremder Arbeit an. Oder ein Modul, etwa die Echtzeit-Kurierverfolgung, braucht zur Spitzenzeit das Fünfzigfache der Kapazität des Rests oder muss laufen, wenn alles andere ausgefallen ist. Oder ein Teil hat andere Anforderungen, etwa Kartendaten, die eine separate, streng kontrollierte Einheit rechtfertigen. Langsamer Code ist kein Grund; eine Aufteilung behebt ihn nicht. Dann teilen Sie schrittweise auf, ein Modul nach dem anderen, entlang von Bounded Contexts, beginnend mit dem Modul mit der klarsten Grenze und dem stärksten Grund zum Herauslösen.
Damit ist Kapitel fünf abgeschlossen. Der vollständige Abschnitt enthält eine Entscheidungshilfe, einen Vergleich aller Stile des Kapitels und eine Checkliste für einen gesunden Monolithen. Wenn Sie bereit sind, machen Sie den optionalen Kapiteltest und gehen Sie dann weiter zu Kapitel sechs, wo wir diese Fragen über ein Netzwerk tragen. Gut gemacht.
Was Sie wissen müssen
- Ein Monolith ist eine Deployment-Entscheidung (eine Einheit, die zusammen gebaut und ausgeliefert wird), kein Urteil über die innere Qualität; ein gut modularisierter Monolith ist eine respektable, oft überlegene Architektur.
- Martin Fowlers Microservice Premium und MonolithFirst (2015) argumentieren, dass sich Microservices erst oberhalb eines gewissen Komplexitätsniveaus auszahlen und dass die meisten erfolgreichen Microservice-Systeme als Monolithen begonnen haben.
- David Heinemeier Hanssons Majestic Monolith (2016) spricht sich für kleine Teams aus; Stefan Tilkov (2015) warnt, dass nicht modularisierte Monolithen schwer aufzuteilen sind, setzen Sie Modulgrenzen daher von Anfang an durch.
- Ein Monolith ist meist richtig bei kleinen Teams, unsicheren Domänen, frühen Produkten, hohen Konsistenzanforderungen und begrenzter Betriebskapazität.
- Monolithen skalieren über zustandslose Replikate hinter einem Load Balancer, Read Replicas, Caching, Background Worker und bei Bedarf eine Partitionierung nach Mandanten.
- Teilen Sie auf, wenn es Belege gibt: Konkurrenz zwischen Teams, Deployment-Warteschlangen, stark abweichende Skalierungs- oder Compliance-Anforderungen; teilen Sie schrittweise entlang von Bounded Contexts auf.
Die Frage hinter dem Kapitel
Die Stile in diesem Kapitel werden alle als eine Einheit deployt. Kapitel 6 trägt dieselben Fragen über ein Netzwerk, wo jeder Aufruf fehlschlagen, langsam sein oder doppelt ankommen kann. Bevor wir diese Linie überschreiten, lohnt es sich, direkt zu fragen: Wann ist ein Monolith die richtige Antwort? Einen großen Teil der 2010er-Jahre lautete die Standardantwort der Branche „selten“, und viele Teams teilten junge Systeme in Microservices auf, weil große, berühmte Unternehmen es so gemacht hatten. Ein Jahrzehnt Erfahrung hat zu einer ausgewogeneren Sicht geführt, und dieser Abschnitt legt sie dar.
Das Wort Monolith erfordert Sorgfalt. Wie Abschnitt 5.1 erklärt hat, beschreibt es das Deployment (eine Einheit, die zusammen gebaut, getestet und ausgeliefert wird), nicht die innere Qualität. Ein Monolith kann ein gut strukturiertes modulares System oder ein Big Ball of Mud sein, und dasselbe gilt für eine Menge von Services. Viele der Probleme, die man Monolithen anlastet, sind in Wahrheit Probleme mangelnder Modularität, und wer ein schlecht modularisiertes System in Services aufteilt, erhält einen verteilten Big Ball of Mud, und der ist schlimmer.
MonolithFirst und die Microservice Premium
Im Mai 2015 beschrieb Martin Fowler die Microservice Premium: Microservices bringen eigene, reale Kosten mit sich (automatisiertes Deployment vieler Einheiten, Monitoring, Umgang mit Teilausfällen, Eventual Consistency, versionierte APIs zwischen Teams), und diese Kosten bremsen ein Team, solange das System nicht komplex genug ist, damit Microservices sie wieder einspielen. Sein Rat lautete, standardmäßig auf einen einzigen, gut modularisierten Monolithen zu setzen und erst dann zu Microservices zu wechseln, wenn die Komplexität es verlangt.
Einen Monat später, in einem Artikel mit dem Titel MonolithFirst (Juni 2015), ergänzte er zwei Beobachtungen aus den Projekten, die er gesehen hatte: Fast alle erfolgreichen Microservice-Systeme hatten als Monolith begonnen, der zu groß wurde und dann aufgeteilt wurde, und fast alle von Grund auf als Microservices gebauten Systeme waren in ernste Schwierigkeiten geraten. Ein Grund ist, dass gute Servicegrenzen schwer zu finden sind, bevor man die Domäne versteht, und eine falsche Grenze zwischen Services weit teurer zu verschieben ist als eine falsche Grenze zwischen Modulen. Er beschrieb mehrere Wege: einen Monolithen bauen und später Services an seinen Rändern abtrennen; einen bewusst wegwerfbaren Monolithen bauen; oder mit wenigen grobgranularen Services beginnen und sie weiter aufteilen, sobald sich die Grenzen gefestigt haben.
Die Microservice Premium
Der Majestic Monolith und die Gegenposition
Im Februar 2016 veröffentlichte David Heinemeier Hansson, der Schöpfer von Ruby on Rails, „The Majestic Monolith“ im Blog Signal v. Noise. Er beschrieb Basecamp, damals von einem Team aus etwa einem Dutzend Programmierern gebaut, als eine integrierte Anwendung, die Web-, Mobil-, Desktop- und E-Mail-Clients bedient, und argumentierte, kleine Teams sollten auf einen gut gemachten Monolithen setzen, statt die Architektur von Organisationen mit Tausenden Engineers zu kopieren. Sein Kernsatz lautete Don't distribute your computing!
, es sei denn, man muss es wirklich.
Nicht alle Fachleute sind sich einig. Ebenfalls im Juni 2015 argumentierte Stefan Tilkov auf Fowlers Website in „Don't start with a monolith“, dass es oft schwieriger als erwartet ist, einen Monolithen später aufzuteilen, weil seine Teile über gemeinsame Bibliotheken, Domänenobjekte und die Datenbank eng zusammenwachsen. Ist das System eindeutig groß genug, um getrennte Teile zu rechtfertigen, und ist die Domäne gut verstanden, empfahl er, von Anfang an getrennte Subsysteme zu entwerfen. Die beiden Positionen liegen weniger weit auseinander, als es klingt: Beide sagen, dass die Grenzen das Schwierige sind, und ein Monolith, dessen Module vom ersten Tag an durchgesetzt werden (Abschnitt 5.3), beantwortet den Großteil von Tilkovs Bedenken.
Reale Fälle weisen in beide Richtungen, haben aber eine gemeinsame Lehre. Segment beschrieb 2018, wie es mehr als 140 kleine Services wieder zu einem einzigen Service zusammengelegt hatte, nachdem Betriebsaufwand und auseinanderlaufende gemeinsame Bibliotheken die Auslieferung gebremst hatten; Amazon Prime Video verlagerte 2023 eine Monitoring-Workload von verteilten Serverless-Komponenten in einen einzigen Prozess (siehe Abschnitt 1.3). Ebenso gewinnen Organisationen mit Hunderten von Teams, wie die Großfälle aus Abschnitt 6.3, durch Services echte Unabhängigkeit. Die Lehre heißt Passung: Der Deployment-Stil muss zur Organisation, zur Reife der Domäne und zu den geforderten Qualitäten passen.
Wann ein Monolith die richtige Antwort ist
Ein kleines Team. Bei einem bis wenigen Teams sind die Kommunikationskosten, die Microservices beseitigen, gering, ihre Betriebskosten aber dieselben wie für eine große Organisation. Ein Monolith erlaubt es allen, das ganze System zu verstehen und zu ändern.
Eine unsichere Domäne oder ein frühes Produkt. Solange Sie noch herausfinden, was das Produkt ist, werden sich Grenzen verschieben. Eine Grenze zwischen Modulen zu verschieben ist ein Refactoring; sie zwischen Services zu verschieben ist eine Migration mit Daten, APIs und Deployments.
Hohe Konsistenzanforderungen. Eine einzige Datenbanktransaktion über Bestellungen, Zahlungen und Lagerbestand ist in einem Monolithen einfach und korrekt. Über Services hinweg braucht sie Sagas, Outboxes und Kompensation (Abschnitt 6.7).
Begrenzte Betriebskapazität. Jeder Service braucht seine eigene Pipeline, sein Monitoring, sein Alerting, seine Skalierungsregeln und Bereitschaftswissen. Ein Team ohne Plattform dafür verbringt seine Zeit mit Klempnerarbeit.
Performancekritische Interaktionen. Prozessinterne Aufrufe dauern Nanosekunden bis Mikrosekunden; Netzwerkaufrufe dauern Millisekunden und können fehlschlagen. Gesprächige Interaktionen zwischen Teilen der Domäne sind innerhalb eines Prozesses weit günstiger.
In all diesen Fällen ist die empfohlene Form ein modularer Monolith, kein unstrukturierter. Er hält die Option offen, Services herauszulösen, wie Abschnitt 5.3 erklärt.
Einen Monolithen skalieren
Eine verbreitete Annahme lautet, Monolithen könnten nicht skalieren. Sie skalieren weiter, als die meisten Systeme es je brauchen, mithilfe der X-Achse des von Martin Abbott und Michael Fisher beschriebenen Scale Cube (viele identische Kopien betreiben) zusammen mit einigen Standardtaktiken. Kapitel 9 behandelt jede davon ausführlich (Abschnitt 9.1).
Horizontale Replikate hinter einem Load Balancer. Betreiben Sie viele identische Kopien der Anwendung und verteilen Sie die Anfragen auf sie. Das funktioniert nur, wenn die Anwendung zustandslos ist: Sessions, hochgeladene Dateien und Caches müssen in gemeinsamen Speichern liegen, damit jedes Replikat jede Anfrage bedienen kann (Abschnitt 9.2). Es bringt außerdem Redundanz: Fällt ein Replikat aus, steht der Dienst nicht mehr still.
Read Replicas. Die meisten Systeme lesen weit mehr, als sie schreiben. Datenbank-Read Replicas kopieren die Änderungen des Primärsystems und bedienen Lesezugriffe wie Bestellhistorie und Berichte, um den Preis der Replikationsverzögerung: Ein Replikat kann einen Sekundenbruchteil hinterherhinken (Abschnitt 7.4).
Caching. Speisekarten, Produktseiten und Konfiguration ändern sich selten und werden ständig gelesen; sie im Arbeitsspeicher oder in einem gemeinsamen Cache vorzuhalten nimmt der Datenbank den Großteil der Last ab (Abschnitt 7.6). Statische Assets kommen in ein CDN.
Background Worker. Langsame oder stoßweise Arbeit (E-Mails, PDF-Rechnungen, Importe, Auszahlungen) kommt in eine Queue und wird von Worker-Prozessen abgearbeitet. Worker laufen meist mit derselben Codebasis und einem anderen Startbefehl, sodass das System eine Codebasis bleibt, die Web- und die Worker-Rolle aber getrennt skalieren (Abschnitt 9.3).
Größere Maschinen und Partitionierung. Vertikale Skalierung der Datenbank ist oft der einfachste nächste Schritt. Bei sehr großen Systemen lassen sich die Daten nach Mandant oder Region partitionieren, mit einer vollständigen Kopie des Monolithen pro Partition (die Z-Achse des Scale Cube). Stack Overflow ist ein bekanntes Beispiel dafür, wie weit ein weitgehend monolithisches Design reichen kann: Laut dem eigenen Bericht seiner Engineers verarbeiteten seine Load Balancer an einem Tag im Februar 2016 etwa 209 Millionen HTTP-Anfragen, wobei die zentrale Q&A-Anwendung (eine mandantenfähige Anwendung für alle Q&A-Sites) auf neun primären Webservern lief und vier SQL-Server-Maschinen das gesamte Netzwerk bedienten.
Ein horizontal skalierter Monolith
Ausgearbeitetes Beispiel
Ausgearbeitetes Beispiel: Wie weit trägt ein Monolith?
Ein neues Essenslieferungs-Start-up hat 7 Engineers und erwartet bis Ende des zweiten Jahres eine Spitze von 1200 HTTP-Anfragen pro Sekunde zur Abendessenszeit am Freitag. Ein Lasttest des modularen Monolithen zeigt, dass ein Replikat (4 vCPU) 180 Anfragen pro Sekunde innerhalb des p95-Ziels von 300 ms bewältigt. Etwa 85 % der Anfragen sind Lesezugriffe (Speisekarten, Bestellverfolgung). Die Zahlen dienen der Veranschaulichung.
| Replikate für die Spitze | 1200 / 180 = 6,7, also 7 Replikate zur Spitzenzeit |
|---|---|
| Kapazitätsreserve (Headroom) | Die Auslastung deutlich unter der Sättigung halten (Abschnitt 2.4) und den Ausfall einer Availability Zone überstehen: etwa 50 % Kapazitätsreserve einplanen, also 7 x 1,5 = etwa 11 Replikate zur Spitzenzeit, außerhalb der Spitze herunterskaliert |
| Schreibzugriffe auf die Datenbank | 15 % von 1200 = 180 Anfragen pro Sekunde mit je etwa 3 Schreibvorgängen: etwa 540 Schreibvorgänge pro Sekunde, gut machbar für ein einzelnes verwaltetes PostgreSQL-Primärsystem auf einer mittelgroßen Instanz |
| Lesezugriffe auf die Datenbank | 1020 Leseanfragen pro Sekunde; ein Cache für Speisekarten (die sich selten ändern) fängt die meisten ab, und zwei Read Replicas übernehmen Bestellhistorie und Berichte, wobei etwa eine Sekunde Replikationsverzögerung akzeptiert wird |
| Hintergrundarbeit | Belegmails, Kurierauszahlungen und Restaurantberichte wandern in einen Worker-Pool, gespeist von einer Queue, damit ein langsamer E-Mail-Anbieter den Bezahlvorgang nicht bremsen kann |
| Fazit | Eine Codebasis, als eine Einheit deployt und als etwa 11 Web-Replikate plus Worker betrieben, erreicht das Zweijahresziel. Das Architekturbudget fließt in Modulgrenzen und Zustandslosigkeit, nicht in die Aufteilung. |
Die Grenze, die zuerst erreicht wird, ist selten die CPU. Häufiger ist es die Teamkoordination, wenn die Engineering-Gruppe wächst, oder ein einzelnes Modul (etwa die Disposition) mit ganz anderen Skalierungs- oder Release-Anforderungen.
Jede Zahl hier sollte durch Messungen aus Ihren eigenen Lasttests ersetzt werden; die Kapazitätsplanung behandelt Abschnitt 9.7.
Anzeichen, dass es Zeit für eine Aufteilung ist
Eine Aufteilung sollte eine Reaktion auf Belege sein, nicht auf Moden. Die Anzeichen, dass eine Deployment-Einheit zur Einschränkung geworden ist, sind überwiegend organisatorischer und betrieblicher Art:
Konkurrenz zwischen Teams. Mehrere Teams ändern denselben Code und warten aufeinander; Merge-Konflikte, Abstimmungsmeetings und von anderen Teams verursachte kaputte Builds nehmen zu, wenn die Organisation wächst. Sind die Module bereits gut abgegrenzt und hält die Konkurrenz an, fehlt womöglich die Unabhängigkeit im Deployment.
Deployment-Warteschlangen. Releases werden zu Release-Zügen gebündelt, weil jede Änderung einen erneuten Test des ganzen Systems erfordert; eine kleine Korrektur wartet tagelang hinter fremder Arbeit; die fehlgeschlagene Änderung eines Teams blockiert das Release aller.
Stark abweichende Skalierungs- oder Verfügbarkeitsanforderungen. Ein Modul (etwa die Echtzeit-Kurierverfolgung) braucht zur Spitzenzeit das Fünfzigfache der Kapazität des Rests oder muss laufen, wenn alles andere ausgefallen ist. Den ganzen Monolithen für die Anforderungen eines Moduls zu skalieren wird teuer.
Anderer Sicherheits- oder Compliance-Umfang. Kartendaten oder Gesundheitsakten können eine separate, streng kontrollierte Einheit rechtfertigen, um den Rest des Systems aus dem Prüfungsumfang herauszuhalten.
Andere Technologieanforderungen. Etwa ein Machine-Learning-Modell, das sich am besten aus Python bereitstellen lässt, innerhalb eines Java-Monolithen.
Manche Gründe sind für sich genommen keine guten Gründe: „Monolithen sind Legacy“, der Wunsch, eine neue Technologie einzusetzen, oder langsamer Code (ein Designproblem, das eine Aufteilung nicht behebt und durch zusätzliche Netzwerkaufrufe sogar verschlimmern kann).
Wie man aufteilt, wenn es so weit ist
Teilen Sie schrittweise auf, ein Modul nach dem anderen, beginnend mit dem Modul, das sowohl die klarste Grenze als auch den stärksten Grund zum Herauslösen hat. Modularisieren Sie zuerst, wenn der Code noch nicht modular ist: Wer einen Service aus einem Knäuel herauslöst, zieht das Knäuel mit. Lösen Sie dann entlang von Bounded Contexts heraus (Abschnitt 5.7): Setzen Sie eine Netzwerk-API vor die bestehende API des Moduls, verlagern Sie seine Daten in eine eigene Datenbank, ersetzen Sie prozessinterne Ereignisse durch einen Message Broker und gemeinsame Transaktionen durch explizite Sagas. Leiten Sie den Verkehr schrittweise auf den neuen Service um und behalten Sie den alten Weg, bis sich der neue bewährt hat.
Das ist der Strangler-Fig-Ansatz samt seinen Verwandten, behandelt in Abschnitt 11.6. Das Ziel ist oft nicht „alles Microservices“, sondern ein Monolith mit einigen herausgelösten Services dort, wo die Belege es verlangt haben; die servicebasierte Architektur aus Abschnitt 6.2 ist ein verbreiteter, pragmatischer Endzustand.
Die Stile aus Kapitel 5 im Vergleich
| Merkmal | Schichten | Modularer Monolith | Pipes and Filters | Microkernel | Hexagonal / Clean |
|---|---|---|---|---|---|
| Hauptpartitionierung | Technisch | Fachlich | Verarbeitungsschritte | Kern plus Plug-ins | Innen gegenüber außen |
| Einfachheit | Hoch | Mittel bis hoch | Hoch | Mittel | Niedrig bis mittel |
| Gesamtkosten | Günstig | Günstig | Günstig | Günstig | Günstig bis mittel |
| Änderbarkeit bei fachlichen Änderungen | Niedrig bis mittel | Hoch | Hoch innerhalb des Flusses | Hoch in geplanten Richtungen | Hoch für Infrastruktur |
| Testbarkeit | Mittel | Mittel bis hoch | Hoch | Mittel bis hoch | Hoch |
| Deploybarkeit | Niedrig | Niedrig bis mittel | Niedrig bis mittel | Mittel | Durch den Deployment-Stil bestimmt |
| Skalierbarkeit | Niedrig | Niedrig bis mittel | Niedrig bis mittel | Niedrig | Durch den Deployment-Stil bestimmt |
| Fehlertoleranz | Niedrig | Niedrig | Niedrig bis mittel | Niedrig bis mittel | Durch den Deployment-Stil bestimmt |
| Typischer Einsatz | Kleine bis mittlere Apps, ein Team | Wachsende Produkte, mehrere Teams | ETL, Medien, Logs, Compiler | Produkte mit vielen Varianten | Logik, die Frameworks überdauern muss |
Die Bewertungen fassen die Urteile aus den einzelnen Abschnitten zusammen und gehen von typischen Implementierungen aus. Domain-Driven Design erscheint nicht als Spalte, weil es eine Methode zum Finden von Grenzen ist, die jeder dieser Stile nutzen kann. Reale Systeme kombinieren Stile: Ein modularer Monolith, dessen Module innen hexagonal sind, mit einem Pipes-and-Filters-Importjob und einem Microkernel für Steuerregeln, ist völlig normal.
Entscheidungshilfe
Entscheidungshilfe: zwischen den Stilen aus Kapitel 5 wählen
Nutzen Sie sie für ein neues System oder eine größere Umstrukturierung, die als eine Deployment-Einheit laufen wird. Gehen Sie die Fragen von oben nach unten durch; mehrere Stile können auf verschiedenen Ebenen zutreffen.
- Ist das System hauptsächlich ein Datenfluss durch eine Reihe von Transformationen (importieren, bereinigen, anreichern, exportieren) statt Benutzer, die mit gemeinsamem Zustand interagieren?JaVerwenden Sie Pipes and Filters für diesen Fluss (Abschnitt 5.4). Halten Sie jeden Filter zustandslos und das Datenformat zwischen den Filtern explizit.NeinWeiter zur nächsten Frage.
- Hat das Produkt einen stabilen Kernablauf, aber viele Varianten bestimmter Schritte (pro Land, Kunde, Anbieter oder Dateiformat), oder müssen Dritte es erweitern?JaVerwenden Sie einen Microkernel (Abschnitt 5.5) für diese Variationspunkte, idealerweise nachdem zwei oder drei Varianten gebaut sind, damit der Vertrag auf echten Fällen beruht.NeinWeiter zur nächsten Frage.
- Ist es klein (ein Team, ein paar Dutzend Bildschirmmasken), mit einfachen Regeln, und wird es voraussichtlich kaum wachsen?JaVerwenden Sie eine Schichtenarchitektur (Abschnitt 5.2) mit geschlossenen Schichten, durchgesetzt durch einen Architekturtest. Fügen Sie nicht mehr Struktur hinzu, als das Problem braucht.NeinWeiter zur nächsten Frage.
- Werden mehrere Teams daran arbeiten, oder umfasst die Domäne mehrere Bereiche mit eigener Sprache (Speisekarten, Bestellungen, Disposition)?JaVerwenden Sie einen modularen Monolithen (Abschnitt 5.3) mit Modulen entlang von Bounded Contexts (Abschnitt 5.7), von denen jedes seine Daten und seine API besitzt.NeinEine Schichten- oder eine modulare Struktur funktionieren beide; bevorzugen Sie Module nach Domäne, wenn Wachstum wahrscheinlich ist, denn eine spätere Umstrukturierung ist teuer.
- Ist die Geschäftslogik in jedem Modul oder jeder Anwendung umfangreich, oder hängt sie von externen Diensten ab, die sich wahrscheinlich ändern?JaStrukturieren Sie das Innere mit Ports and Adapters (Abschnitt 5.6): die Domäne im Zentrum, Adapter für Web, Datenbank und Anbieter.NeinBehalten Sie im Modul die Standardstruktur des Frameworks bei; fügen Sie Ports nur an Grenzen hinzu, die sich wahrscheinlich ändern.
Checkliste: ein Monolith, der gesund und aufteilbar bleibt
Das meiste davon kostet wenig, wenn es früh geschieht, und sehr viel, wenn es nachgerüstet wird.
- Module folgen fachlichen DomänenDie Pakete auf oberster Ebene sind nach Bounded Contexts benannt (orders, dispatch, payments), nicht nach technischen Schichten.
- Jedes Modul hat eine kleine öffentliche APIAlles andere ist intern; ein Architekturtest lässt den Build bei einem verbotenen Import fehlschlagen.
- Jede Tabelle gehört genau einem ModulGetrennte Schemas oder Namenskonventionen machen die Zuständigkeit sichtbar; kein Modul liest die Tabellen eines anderen.
- Die Anwendung ist zustandslosSessions, Uploads und Caches liegen in gemeinsamen Speichern, sodass jedes Replikat jede Anfrage bedienen kann und sich Replikate beliebig hinzufügen lassen.
- Langsame Arbeit läuft in Background WorkernE-Mails, Berichte, Auszahlungen und Importe laufen über eine Queue, damit die Web-Replikate schnell bleiben und beide getrennt skaliert werden können.
- Build und Tests bleiben schnellVerfolgen Sie Build- und Testzeiten; halten Sie die Haupt-Pipeline unter etwa 10 bis 15 Minuten, indem Sie Module getrennt und parallel testen.
- Das Deployment ist automatisiert und häufigKleine, häufige Releases mit automatisiertem Rollback; Feature-Flags entkoppeln das Deployment vom Release.
- Observability pro ModulLogs, Metriken und Traces sind mit dem Modul gekennzeichnet, sodass Sie sehen, welches Modul langsam ist, fehlschlägt oder teuer ist.
- Anzeichen von Überlastung werden gemessenDurchlaufzeit, Länge der Deployment-Warteschlange, Merge-Konflikte pro Team und Ressourcenverbrauch pro Modul werden regelmäßig überprüft, damit die Entscheidung zur Aufteilung auf Belegen beruht.
Prüfen Sie Ihr Verständnis
Warum ist eine falsche Grenze zwischen Services teurer als eine falsche Grenze zwischen Modulen?
Eine Modulgrenze zu verschieben ist ein Refactoring innerhalb einer Codebasis und einer Datenbanktransaktion; eine Servicegrenze zu verschieben ändert Netzwerk-APIs, Datenspeicher, Deployments und oft die Teamzuständigkeit und muss ohne Ausfallzeit geschehen.
Was muss für einen Monolithen gelten, bevor Sie ihn durch zusätzliche Replikate skalieren können?
Er muss zustandslos sein: Sessions, Dateien und Caches müssen in gemeinsamen Speichern liegen, damit jedes Replikat jede Anfrage bedienen kann.
Nennen Sie zwei Anzeichen, dass ein Monolith zur Einschränkung geworden ist, und einen Grund für eine Aufteilung, der für sich allein nicht ausreicht.
Anzeichen: anhaltende Konkurrenz zwischen Teams und Deployment-Warteschlangen oder ein Modul mit ganz anderen Skalierungs- oder Compliance-Anforderungen. Für sich allein nicht ausreichend: Mode, der Wunsch nach einer neuen Technologie oder langsamer Code.
Worin überschneiden sich die Positionen MonolithFirst und „Don't start with a monolith“?
Beide sagen, dass das schwierige Problem darin liegt, gute Grenzen zu finden und zu halten; ein Monolith mit durchgesetzten Modulgrenzen geht auf Tilkovs Bedenken ein und behält Fowlers geringere Kosten bei.
Quellen und weiterführende Literatur
Haben Sie eine Frage zu diesem Abschnitt? ArchitectBot, der KI-Lernassistent des Kurses, antwortet zuerst aus dem Kurs.
Fragen: „Wir sind fünf Entwickler, die ein neues B2B-Planungsprodukt bauen, und Investoren fragen ständig nach Microservices. Wie erkläre ich, warum wir mit einem modularen Monolithen beginnen, und was uns zu einem Wechsel bewegen würde?“