Warum wir ERPs zuerst als modulare Monolithen bauen
Die Aufteilung passiert meist zu früh, aus dem falschen Grund
Das erste Architekturmeeting bei einem ERP-Projekt läuft fast immer gleich ab: Lager, Aufträge, Rechnungsstellung und Kundenverwaltung werden als getrennte Services gezeichnet, weil das in Konferenzvorträgen so aussieht. Sechs Monate später debuggt dasselbe Team eine Transaktion über drei Services hinweg, nur um Bestand zu verringern, einen Auftrag anzulegen und eine Rechnungszeile zu buchen. Diese Fehlersuche dauert länger, als das Feature als eine einzige Anwendung zu bauen gedauert hätte. Die meisten ERP-Projekte scheitern aus architektonischen Gründen, nicht aus technischen, und die Schwierigkeiten beginnen meist schon in dieser ersten Woche.
Wir bauen ERPs zunächst als modulare Monolithen. Diese Entscheidung haben wir bei keinem einzigen Projekt bereut. Es geht nicht darum, Komplexität zu vermeiden. Es geht darum, die Architektur an das anzupassen, was tatsächlich zusammen geändert werden muss und was tatsächlich konsistent bleiben muss.
Wo die Modulgrenze verläuft
Ein modularer Monolith ist keine einzige undifferenzierte Codebasis. Bei einem typischen Java/Spring-Boot-ERP ziehen wir die Grenze nach Geschäftsfähigkeit: Lager, Verkaufsaufträge, Einkauf, Rechnungsstellung, Kundendaten. Jedes Modul besitzt sein eigenes Package und seinen eigenen Satz Tabellen, oft ein eigenes Schema innerhalb derselben PostgreSQL-Datenbank. Ein Modul erreicht ein anderes ausschließlich über eine explizite Schnittstelle, nie durch direkte Abfrage der Tabellen eines anderen Moduls.
- Lager besitzt Bestandsmengen, Reservierungen und Bewegungen.
- Verkauf besitzt Aufträge, Preisfindung und Erfüllungsstatus.
- Rechnungsstellung besitzt Abrechnungsbelege und Zahlungsstatus.
- Kundendaten besitzen Kontaktdaten, Zahlungsbedingungen und Korrespondenzverlauf.
Im Code-Review gilt eine Regel: Ein Modul darf die Service-Schnittstelle eines anderen Moduls aufrufen, aber nicht dessen Tabellen abfragen. Genau diese Regel macht eine spätere Auslagerung möglich, weil die Grenze von Tag eins an schon wie ein Netzwerkaufruf geformt ist, noch bevor es überhaupt einen Netzwerkaufruf gibt.
Was in einer Transaktion bleiben muss
Die Vorgänge, die in einem ERP atomar sein müssen, sind genau die, die über Services hinweg schwer atomar zu halten sind: Bestand reservieren und einen Auftrag bestätigen, eine Rechnung buchen und den Auftragsstatus aktualisieren, eine Zahlung verbuchen und eine Kreditsperre aufheben. In einem modularen Monolithen laufen diese innerhalb einer Datenbanktransaktion mit den gewohnten ACID-Garantien. In einem zu früh aufgeteilten System braucht derselbe Vorgang eine Saga, ausgleichende Schritte und ein Zeitfenster, in dem das System inkonsistent ist. Die meisten Support-Tickets entstehen genau in diesem Fenster.
Wir nutzen zwischen Modulen durchaus einen Ereignismechanismus, geschrieben in derselben Transaktion in eine Outbox-Tabelle und danach asynchron verarbeitet, aber nur für Effekte, die eine Verzögerung vertragen: eine Benachrichtigung versenden, eine Reporting-Projektion aktualisieren, eine Dokumentenerzeugung anstoßen. Alles, was der Nutzer auf dem nächsten Bildschirm sehen muss, bleibt innerhalb der Transaktionsgrenze.
Wann wir tatsächlich aufteilen
Zwei Gründe rechtfertigen, ein Modul in einen eigenen Service auszulagern: ein wirklich anderes Skalierungsprofil oder ein wirklich anderes verantwortliches Team. Ein Reporting-Modul, das schwere Aggregationen gegen eine Read Replica statt gegen den transaktionalen Kern fahren muss, skaliert anders, deshalb ist es meist das erste Stück, das wir auslagern. Eine kundenseitige Storefront auf demselben ERP-Katalog ist eine weitere natürliche Trennung, weil sie ihren eigenen Release-Rhythmus und ihr eigenes Verfügbarkeitsziel braucht, und keines von beiden sollte von einem Batch-Import-Job im Backoffice abhängen.
Weil die Grenzen bereits sauber waren, wird die Auslagerung zur mechanischen Übung: Die interne Schnittstelle wird zu einem HTTP- oder Messaging-Vertrag, das Modul bekommt eine eigene Deployment-Pipeline, und der Rest des Systems ruft weiterhin dieselbe Schnittstelle wie zuvor auf. Wir haben das an laufenden Kundensystemen umgesetzt, ohne neu zu schreiben.
Unsere Empfehlung
Starten Sie jedes ERP als modularen Monolithen mit strikten internen Grenzen und einer Datenbank. Halten Sie den transaktionalen Kern, den Teil, der jetzt wirklich konsistent bleiben muss, so lange innerhalb dieser Grenze, wie es nötig ist. Lagern Sie ein Modul erst dann in einen eigenen Service aus, wenn Sie einen konkreten Skalierungs- oder Zuständigkeitsgrund haben, nicht weil es ein Diagramm so vorsieht. Die Grenzarbeit von Tag eins macht aus der späteren Aufteilung einen günstigen Schritt statt einer Neuentwicklung.