Microservices-Architekturmuster, die wirklich funktionieren
Praktische Lektionen aus Entwurf und Betrieb von Microservices im großen Maßstab: Event-Kommunikation, Servicegrenzen und bewährte Muster.

Der Realitätscheck für Microservices
Nach Jahren im Aufbau und Betrieb von Microservices habe ich gelernt: Der schwierigste Teil ist nicht die Aufspaltung eines Monolithen, sondern das Wissen, wo man schneidet und wie man dafür sorgt, dass die Teile zuverlässig miteinander sprechen.
Dieser Beitrag stellt die Muster vor, die sich meiner Erfahrung nach im Produktivbetrieb bewährt haben.
Servicegrenzen finden
Der häufigste Fehler? Die Aufteilung nach technischer Schicht statt nach Fachdomäne.
// ❌ Technical split — leads to tight coupling
// services/user-api/
// services/user-database/
// services/user-cache/
// ✅ Domain split — each service owns its full stack
// services/identity/ → auth, profiles, permissions
// services/catalog/ → products, categories, search
// services/orders/ → checkout, fulfillment, returnsDomain-Driven Design liefert dafür das passende Vokabular: Bounded Contexts. Jeder Service sollte einem Bounded Context entsprechen, mit klarer Verantwortung für seine eigenen Daten und sein Verhalten.
Ereignisgesteuerte Kommunikation
Synchrone HTTP-Aufrufe zwischen Services erzeugen brüchige Ketten. Wenn Service A Service B aufruft und dieser wiederum Service C, reicht ein einziger Fehler irgendwo in der Kette, um alles zum Einsturz zu bringen.
Ereignisgesteuerte Kommunikation entkoppelt Services:
// Order service — publishes event after checkout
interface OrderCreatedEvent {
type: "order.created";
data: {
orderId: string;
customerId: string;
items: Array<{ productId: string; quantity: number }>;
total: number;
};
metadata: {
timestamp: string;
correlationId: string;
};
}
async function createOrder(input: CreateOrderInput): Promise<Order> {
const order = await db.orders.create(input);
await eventBus.publish({
type: "order.created",
data: {
orderId: order.id,
customerId: order.customerId,
items: order.items,
total: order.total,
},
metadata: {
timestamp: new Date().toISOString(),
correlationId: generateId(),
},
});
return order;
}Die Services für Lagerbestand, Benachrichtigungen und Analytics abonnieren jeweils order.created und reagieren unabhängig voneinander.
Das Saga-Muster für verteilte Transaktionen
Wenn ein Geschäftsprozess mehrere Services umspannt, lässt sich dafür keine klassische Datenbanktransaktion verwenden. Sagas koordinieren stattdessen den Ablauf:
| Schritt | Service | Aktion | Kompensation |
|---|---|---|---|
| 1 | Bestellungen | Bestellung anlegen | Bestellung stornieren |
| 2 | Zahlung | Karte belasten | Zahlung erstatten |
| 3 | Lager | Bestand reservieren | Bestand freigeben |
| 4 | Versand | Lieferung planen | Sendung stornieren |
Schlägt Schritt 3 fehl, laufen die Kompensationen in umgekehrter Reihenfolge ab: erst wird die Zahlung erstattet, dann die Bestellung storniert. Man spricht von Choreografie, wenn die Services auf Ereignisse reagieren, und von Orchestrierung, wenn ein zentraler Koordinator den Ablauf steuert.
Das API-Gateway-Muster
Ein API-Gateway sitzt zwischen den Clients und den Services und übernimmt:
- Routing — Anfragen an den richtigen Service weiterleiten
- Authentifizierung — Tokens einmal zentral prüfen, statt in jedem einzelnen Service
- Rate Limiting — Services vor Lastspitzen schützen
- Antwortaggregation — Daten aus mehreren Services zu einer Antwort zusammenführen
// Simplified gateway route example
async function getProductPage(productId: string) {
const [product, reviews, recommendations] = await Promise.all([
catalogService.getProduct(productId),
reviewService.getReviews(productId),
recommendationService.getSimilar(productId),
]);
return { product, reviews, recommendations };
}Das Circuit-Breaker-Muster
Fällt ein nachgelagerter Service aus, sorgt beharrliches Weiteraufrufen dafür, dass sich der Fehler kaskadenartig durch das gesamte System fortpflanzt. Genau das verhindert das Circuit-Breaker-Muster:
class CircuitBreaker {
private failures = 0;
private lastFailure = 0;
private state: "closed" | "open" | "half-open" = "closed";
constructor(
private threshold: number = 5,
private resetTimeout: number = 30000,
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === "open") {
if (Date.now() - this.lastFailure > this.resetTimeout) {
this.state = "half-open";
} else {
throw new Error("Circuit is open");
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = "closed";
}
private onFailure() {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) {
this.state = "open";
}
}
}Observability ist keine Option, sondern Pflicht
In einem verteilten System treten zwangsläufig Probleme auf, die mehrere Services betreffen. Ohne vernünftige Observability wird die Fehlersuche unmöglich.
Die drei Säulen:
- Strukturiertes Logging — JSON-Logs mit Korrelations-IDs, die eine Anfrage über alle Services hinweg nachverfolgbar machen
- Distributed Tracing — Tools wie OpenTelemetry zur Visualisierung von Anfrageflüssen
- Metriken — RED-Metriken (Rate, Errors, Duration) pro Service
Die Korrelations-ID ist dabei entscheidend — sie sollte durch jeden Service-Aufruf und jeden Log-Eintrag weitergereicht werden, damit sich der gesamte Weg einer Anfrage im Nachhinein rekonstruieren lässt.
Die wichtigsten Erkenntnisse
- Nach Domäne teilen, nicht nach Schicht — Bounded Contexts ergeben natürliche Servicegrenzen
- Standardmäßig asynchron — Ereignisgesteuerte Kommunikation verhindert kaskadierende Ausfälle
- Für den Fehlerfall planen — Circuit Breaker, Retries mit Backoff und Sagas für verteilte Transaktionen
- Alles beobachten — Was man nicht sieht, kann man nicht reparieren
- Mit einem Monolithen beginnen — Services erst extrahieren, wenn klar wird, wo die Grenzen wirklich verlaufen sollten
Die beste Architektur ist die einfachste, die den eigenen Anforderungen genügt. Microservices sollte man nicht einführen, weil es gerade angesagt ist — sondern dann, wenn Team und Domänenkomplexität es wirklich erfordern.


