Blog
Teil 4 von 13 Backend Patterns

Der Distributed Monolith

4 min Lesezeit
architecture microservices patterns

“Wir sind jetzt Microservices!” Slack-Nachricht vom Teamlead, 47 Reactions. Der Monolith wurde in sechs Services aufgeteilt, jeder mit eigenem Repo, eigener CI/CD-Pipeline. Architekturdiagramm an der Wand, sieht aus wie aus einem AWS-Blogpost.

Sechs Monate später: jedes Deployment braucht ein Meeting mit vier Teams. Integration Tests laufen 45 Minuten und brechen regelmäßig. Freitags-Deploys sind verboten. Es gibt einen Release Train, alle zwei Wochen deployen alle Services gleichzeitig.

Du hast keinen Service rausgelöst. Du hast den Monolithen verteilt.

Deploy den Order-Service und schau, was in beiden Welten passiert:

Microservices
API Gateway
IDLE
Users
IDLE
Orders
IDLE
Products
IDLE
Payments
IDLE
DEPLOY
0.0s
AFFECTED
0
Distributed Monolith
API Gateway
IDLE
Users
IDLE
Orders
IDLE
Products
IDLE
Payments
IDLE
DEPLOY
0.0s
AFFECTED
0

Links: ein Service deployt, fertig. 1.5 Sekunden, kein anderer Service weiß davon. Rechts: du wolltest einen Service deployen, und am Ende müssen vier mit. Plus ein Rollback. Und das ist die optimistische Variante; in der Realität sind es Stunden, weil Menschen koordinieren müssen.

Der Begriff “Distributed Monolith” ist keine offizielle Architekturkategorie. Er beschreibt den Zustand, in dem du die Kosten von Microservices zahlst (Netzwerk, Deploymentkomplexität, Observability) ohne die Vorteile zu kriegen (unabhängige Deploys, Team-Autonomie, Isolation).

Drei Arten von Kopplung

Ein Distributed Monolith entsteht nicht durch eine Entscheidung. Er entsteht durch hundert kleine. Jede für sich “pragmatisch.” Zusammen: ein System das du nicht unabhängig deployen kannst.

Shared Database. Der häufigste Fehler. Zwei Services lesen aus derselben Tabelle. Einer ändert das Schema, der andere bricht. In echten Microservices besitzt jeder Service seine Daten. Shared DB ist das Gegenteil davon.

Synchrone HTTP Calls. Service A ruft Service B synchron auf. Wenn B down ist, ist A down. Wenn B langsam ist, ist A langsam. Du hast keine Isolation, sondern einen verteilten Funktionsaufruf mit Netzwerk-Overhead.

Shared Libraries. Ein gemeinsames NPM-Package mit Domain-Models. Klingt harmlos. Aber wenn du UserProfile änderst, müssen alle Consumer redeployed werden. Das ist ein versteckter Monolith-Release-Zyklus.

Klick auf einen Service und schau, wie viele andere betroffen sind:

Blast Radius
Klick auf einen Service

In echten Microservices würde ein Klick maximal einen anderen Service markieren. Wenn jeder Klick die Hälfte des Systems aufleuchten lässt, hast du deinen Distributed Monolith gefunden.

Fünf Symptome

  1. Du kannst keinen Service alleine deployen. Jedes Deployment triggert Changes in mindestens zwei anderen Services.
  2. Integration Tests sind der Bottleneck. Weil die Services so eng gekoppelt sind, dass Unit Tests nicht reichen.
  3. Es gibt einen Release Train. Alle Services deployen gleichzeitig, nach festem Zeitplan.
  4. Schema-Änderungen sind Projektarbeit. Eine Migration in Service A erfordert Code-Changes in B, C und D.
  5. Shared Libraries haben ständig Major Bumps, und jeder muss sofort updaten.

Wenn drei von fünf zutreffen: Glückwunsch.

Was tun

Unpopular take Nr. 1: Geh zurück zum Monolithen. Ernsthaft. Ein gut strukturierter Monolith ist besser als ein schlecht strukturierter Distributed Monolith. Du sparst dir Netzwerk-Latenz, verteiltes Tracing, Service Discovery, und das Meeting am Dienstag wo vier Teams koordinieren wer zuerst deployt.

Wenn zurück keine Option ist:

Shared DB eliminieren. Jeder Service bekommt seine eigene Datenbank. Daten die mehrere Services brauchen, werden über Events repliziert. Schmerzhaft, aber der wichtigste Schritt.

Sync auf Async umbauen. HTTP-Calls zwischen Services durch Events ersetzen. Service A publiziert OrderCreated, Service B reagiert wenn es kann. Keine Runtime-Kopplung mehr.

Shared Libraries aufbrechen. Domain-Models gehören in den Service der die Daten besitzt. Andere Services definieren ihre eigene Sicht auf die Daten.

// shared-models/src/UserProfile.ts — von Users, Orders, Notifications importiert
export interface UserProfile {
  id: string;
  name: string;
  email: string;
  shippingAddress: Address;
  paymentMethods: PaymentMethod[];
  notificationPreferences: NotificationPrefs;
}
// users-service/src/models.ts
export interface User {
  id: string;
  name: string;
  email: string;
}

// orders-service/src/models.ts
export interface OrderCustomer {
  userId: string;
  shippingAddress: Address;
}

// notifications-service/src/models.ts
export interface NotificationRecipient {
  userId: string;
  email: string;
  preferences: NotificationPrefs;
}

Drei Interfaces statt einem. Jeder Service weiß nur was er braucht. Keine Kopplung. Kein koordiniertes Deployment. Ja, das bedeutet Duplikation. Duplikation ist billiger als Kopplung.

Das ist im Grunde das “Bounded Context”-Konzept aus Domain-Driven Design. Jeder Service definiert sein eigenes Modell der Welt. Dass User in drei Services unterschiedlich aussieht, ist kein Bug. Es ist der Punkt.

Unpopular take Nr. 2: Wenn dein Team weniger als 30 Leute hat und ihr nicht Netflix seid, fangt gar nicht erst mit Microservices an. Ein Monolith mit klaren Modul-Grenzen bringt euch weiter als sechs Services die sich eine Datenbank teilen.