Die Microservices-Architektur trägt zu Skalierbarkeit, Flexibilität und Ausfallsicherheit bei, bringt aber auch Herausforderungen wie Netzwerkausfälle, Datenkonsistenz und Probleme bei der Servicekommunikation mit sich. In dem Beispiel wenden wir uns den Top 10 Microservices-Pattern zu.
Microservices-Entwurfsmuster helfen bei der Lösung dieser Herausforderungen durch:
- Effiziente Behandlung von Fehlern und Timeouts.
- Verbesserung der Datenkonsistenz über mehrere Dienste hinweg.
- Optimierung der Service-to-Service-Kommunikation.
In diesem Beitrag lernen Sie die Top 10 Microservices-Pattern mit realen Anwendungsfällen kennen.
1️⃣ API-Gateway-Pattern
Anwendungsfall aus der Praxis:
Stellen Sie sich vor, Sie kaufen online ein. Die Website kommuniziert mit mehreren Backend-Diensten:
- Benutzerservice (für Benutzerprofile).
- Bestellservice (für die Bestellhistorie).
- Zahlungsdienst (für Transaktionen).
Wenn der Client (Browser oder mobile App) eine direkte Verbindung zu all diesen Diensten herstellt, wird er komplex und ineffizient.
Lösung:
Das API-Gateway fungiert als zentraler Einstiegspunkt und leitet Anfragen an die entsprechenden Microservices weiter.
So funktioniert’s:
1️⃣ Der Client sendet eine Anfrage an das API-Gateway.
2️⃣ Das Gateway leitet die Anfrage an den/die richtige(n) Microservice(s) weiter.
3️⃣ Das Gateway aggregiert bei Bedarf Antworten und gibt eine einzelne Antwort an den Client zurück.
Beispiel: Verwenden von Spring Cloud Gateway
@EnableGateway
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r.path("/users/**")
.uri("lb://USER-SERVICE"))
.route("order-service", r -> r.path("/orders/**")
.uri("lb://ORDER-SERVICE"))
.build();
}
}
✅ Mit diesem Setup kann der Client über ein Gateway mit mehreren Diensten interagieren.
Bewährte Methoden:
✔ Verwenden Sie Caching, um wiederholte Anforderungen zu reduzieren.
✔ Implementieren Sie OAuth2/JWT für die Authentifizierung.
✔ Fügen Sie Leistungsschalter hinzu (siehe unten), um Fehler ordnungsgemäß zu behandeln.
2️⃣ Circuit Breaker Pattern
Anwendungsfall aus der Praxis:
Stellen Sie sich vor, Sie verwenden Netflix und einer ihrer Empfehlungsdienste fällt aus. Ohne Schutz wartet Ihre Netflix-App weiter und stürzt schließlich ab.
Lösung:
Ein Circuit Breaker erkennt, wenn ein Microservice fehlschlägt, und sendet keine Anforderungen mehr an ihn. Stattdessen wird schnell eine Fallbackantwort oder eine Fehlermeldung zurückgegeben.
So funktioniert’s:
1️⃣ Wenn ein Microservice langsam ist oder nicht reagiert, öffnet sich der Schutzschalter und blockiert weitere Anfragen.
2️⃣ Nach einer bestimmten Abkühlphase können einige Anfragen überprüft werden, ob der Dienst wieder verfügbar ist.
3️⃣ Wenn der Dienst wiederhergestellt wird, wird der Schutzschalter geschlossen und der normale Betrieb wieder aufgenommen.
Beispiel: Verwenden des Resilience4j Circuit Breakers
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallbackPayment")
public String processPayment() {
return restTemplate.getForObject("http://payment-service/process", String.class);
}
public String fallbackPayment(Throwable t) {
return "Fallback: Payment service is currently unavailable. Please try later.";
}
✅ Dadurch werden kaskadierende Fehler in einer Microservices-Architektur verhindert.
Bewährte Methoden:
✔ Definieren Sie Schwellenwerte für Fehler (z. B. lösen 5 Fehler hintereinander den Unterbrecher aus).
✔ Verwenden Sie Fallback-Antworten, um die Benutzererfahrung zu verbessern.
✔ Überwachen Sie Leistungsschaltermetriken mit Prometheus/Grafana.
3️⃣ Saga-Pattern(Verwalten verteilter Transaktionen)
Anwendungsfall aus der Praxis:
Sie buchen ein Flugticket und entscheiden sich für die Online-Zahlung. Das System verfügt über mehrere Dienste:
- Buchungsservice (erstellt eine Reservierung).
- Zahlungsdienst (wickelt die Zahlung ab).
- Benachrichtigungsdienst (sendet eine Bestätigungs-E-Mail).
Wenn die Zahlung fehlschlägt, sollte die Buchung storniert werden. Wie stellen wir sicher, dass alle Schritte erfolgreich sind oder rückgängig gemacht werden?
Lösung:
Ein Saga-Muster stellt die letztendliche Konsistenz zwischen Microservices mithilfe von kompensierenden Transaktionen sicher.
So funktioniert’s:
1️⃣ Der Buchungsservice erstellt eine neue Buchung.
2️⃣ Der Zahlungsdienst verarbeitet die Zahlung.
3️⃣ Wenn die Zahlung fehlschlägt, storniert der Buchungsservice die Reservierung.
Beispiel: Verwenden von Choreography Saga mit Kafka Events
@KafkaListener(topics = "payment-events", groupId = "booking-service")
public void handlePaymentEvent(PaymentEvent event) {
if (event.getStatus().equals("FAILED")) {
bookingService.cancelBooking(event.getBookingId());
}
}
✅ Wenn die Zahlung fehlschlägt, wird die Buchung automatisch rückgängig gemacht.
Bewährte Methoden:
✔ Verwenden Sie Choreography Saga für einfache Arbeitsabläufe (ereignisgesteuert).
✔ Verwenden Sie Orchestration Saga mit einem zentralen Controller für komplexe Workflows.
✔ Verwenden Sie Kafka oder RabbitMQ für die ereignisgesteuerte Kommunikation.
4️⃣ CQRS (Command Query Responsibility Segregation) Pattern
Anwendungsfall aus der Praxis:
Stellen Sie sich vor, Sie betreiben Amazon, wo Kunden Bestellungen aufgeben (Schreibvorgänge) und Lieferungen verfolgen (Lesevorgänge). Wenn sowohl Schreib- als auch Lesevorgänge dieselbe Datenbank verwenden, verschlechtert sich die Leistung.
Lösung:
CQRS unterteilt Lese- und Schreibvorgänge in verschiedene Modelle, um die Skalierbarkeit zu verbessern.
So funktioniert’s:
1️⃣ Write Model speichert Daten mit Befehlen (z. B. ).
2️⃣ Das Lesemodell verwendet eine separate Datenbank, die für Abfragen optimiert ist. PlaceOrderCommand
Beispiel: Implementieren von CQRS in Spring Boot
@Query("SELECT new com.example.dto.OrderDTO(o.id, o.status) FROM Order o WHERE o.id = :id")
OrderDTO findOrderById(@Param("id") Long id);
✅ Das Lesemodell (OrderDTO
) ist vom Schreibmodell (Order
) getrennt.
Bewährte Methoden:
✔ Verwenden Sie Event Sourcing (nächstes Muster), um Aktualisierungen nachzuverfolgen.
✔ Optimieren Sie gelesene Datenbanken für schnelle Abfragen (z. B. Elasticsearch).
5️⃣ Event Sourcing Pattern
Anwendungsfall aus der Praxis:
Banken führen eine Transaktionshistorie, anstatt nur ein Guthaben zu speichern. Sie können vergangene Transaktionen wie folgt sehen:
Deposit: +$500
Withdraw: -$200
Transfer: -$100
Anstatt nur den aktuellen Kontostand zu speichern, speichern sie alle Änderungen als Ereignisse.
Lösung:
Event Sourcing speichert alle Zustandsänderungen als Abfolge von Ereignissen, was eine einfache Überwachung und ein Rollback ermöglicht.
So funktioniert’s:
1️⃣ Anstatt eine Datenbank zu aktualisieren, wird jede Aktion als Ereignis gespeichert.
2️⃣ Der aktuelle Zustand wird durch das Abspielen von Ereignissen rekonstruiert.
Beispiel: Speichern von Ereignissen in Kafka
@KafkaListener(topics = "account-events")
public void processEvent(AccountEvent event) {
eventStore.save(event);
}
✅ Dies ermöglicht das Debuggen von Zeitreisen und die Verfolgung von Verlaufen.
Bewährte Methoden:
✔ Verwenden Sie Kafka oder EventStore für die Ereignispersistenz.
✔ Vermeiden Sie das Speichern sensibler Daten in Ereignissen.
6️⃣ Service Discovery Pattern
Anwendungsfall aus der Praxis:
Stellen Sie sich vor, Sie betreiben Uber mit mehreren Microservices für:
- Driver Service (findet verfügbare Treiber).
- Ride Service (bringt Fahrer und Passagiere zusammen).
- Zahlungsdienst (wickelt Transaktionen ab).
Jeder Dienst wird auf mehreren Instanzen auf verschiedenen Servern ausgeführt. Wie findet ein Dienst dynamisch einen anderen?
Problem:
Microservices werden dynamisch gestartet und beendet, sodass es unmöglich ist, Dienststandorte fest zu codieren.
Lösung:
Verwenden Sie die Dienstermittlung, um Dienste dynamisch zu registrieren und zu suchen.
So funktioniert’s:
1️⃣ Jeder Microservice registriert sich selbst bei einer zentralen Service Registry.
2️⃣ Andere Dienste fragen die Registrierung ab, um verfügbare Dienste dynamisch zu finden.
Beispiel: Verwenden von Eureka Service Discovery (Spring Boot)
1. Eureka-Server (Dienst-Registrierung)
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
2. Eureka Client (Microservice registriert sich)
@EnableEurekaClient
@SpringBootApplication
public class RideServiceApplication {
public static void main(String[] args) {
SpringApplication.run(RideServiceApplication.class, args);
}
}
✅ Jetzt kann jeder Microservice „Ride Service“ dynamisch finden.
Bewährte Methoden:
✔ Verwenden Sie Eureka, Consul oder Kubernetes Service Discovery.
✔ Aktivieren Sie Dienstintegritätsprüfungen, um fehlerhafte Instanzen zu entfernen.
✔ Kombinieren Sie es mit Load Balancing, um Anfragen gleichmäßig zu verteilen.
7️⃣ Strangler Fig Pattern
Anwendungsfall aus der Praxis:
Eine Bank betreibt ein altes monolithisches System für Kundenkonten, möchte aber ohne Ausfallzeiten auf Microservices migrieren.
Problem:
Die Migration aller Funktionen auf einmal ist riskant und teuer.
Lösung:
Verwenden Sie das Strangler Fig Pattern, um monolithische Komponenten nach und nach durch Microservices zu ersetzen.
So funktioniert’s:
1️⃣ Identifizieren Sie ein monolithisches Modul (z. B. „Kontoverwaltung“).
2️⃣ Erstellen Sie einen neuen Microservice für dieses Modul.
3️⃣ Leiten Sie Anfragen vom Monolithen über das API-Gateway auf den neuen Microservice um.
4️⃣ Wiederholen Sie den Vorgang für andere Module, bis der Monolith entfernt ist.
Beispiel: Verwenden von API Gateway zur schrittweisen Migration
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("account-service", r -> r.path("/accounts/**")
.uri("lb://NEW-ACCOUNT-SERVICE")) // New microservice
.route("legacy-service", r -> r.path("/legacy/**")
.uri("lb://MONOLITH-SERVICE")) // Old system
.build();
}
✅ Der Datenverkehr wird schrittweise von Monolithen auf Microservices verlagert.
Bewährte Methoden:
✔ Beginnen Sie mit weniger kritischen Modulen (z. B. Berichterstellung, Benachrichtigungen).
✔ Verwenden Sie Funktionsumschalter, um neue Microservices sicher zu aktivieren/deaktivieren.
✔ Behalten Sie die Datensynchronisierung zwischen Monolithen und Microservices bei.
8️⃣ Bulkhead Pattern
Anwendungsfall aus der Praxis:
Sie betreiben einen Essenslieferdienst wie Swiggy/Zomato und kümmern sich um Folgendes:
- Bestellungen Service (hohe Priorität).
- Werbeaktionen Service (niedrige Priorität).
Ein plötzlicher Anstieg des Traffics für Werbeaktionen sollte sich nicht auf die Essensbestellung auswirken.
Problem:
Ein Ausfall in einem Dienst kann alle verfügbaren Ressourcen verbrauchen und zu einem systemweiten Ausfall führen.
Lösung:
Verwenden Sie das Bulkhead-Muster, um kritische Services von nicht kritischen Services zu isolieren.
So funktioniert’s:
1️⃣ Weisen Sie jedem Dienst separate Thread-Pools oder Ressourcen zu.
2️⃣ Wenn ein Dienst überlastet wird, wirkt sich dies nicht auf andere aus.
Beispiel: Verwenden von Resilience4j Bulkhead
@Bulkhead(name = "orderService", type = Bulkhead.Type.THREADPOOL)
public String placeOrder() {
return restTemplate.getForObject("http://order-service/orders", String.class);
}
✅ Auch wenn der Promotions-Service überlastet ist, bleibt der Orders-Service funktionsfähig.
Bewährte Methoden:
✔ Identifizieren Sie Services mit hoher Priorität und weisen Sie dedizierte Ressourcen zu.
✔ Verwenden Sie asynchrones Messaging (Kafka, RabbitMQ), um das Blockieren von Anrufen zu reduzieren.
9️⃣ Sidecar Pattern
Anwendungsfall aus der Praxis:
Netflix möchte seine Microservices um Sicherheit, Überwachung und Protokollierung erweitern, ohne bestehende Dienste zu ändern.
Problem:
Das Hinzufügen von Protokollierung, Sicherheit oder Überwachung zu allen Diensten erhöht die Komplexität.
Lösung:
Verwenden Sie das Sidecar-Muster, um Hilfsdienste neben den wichtigsten Microservices bereitzustellen.
So funktioniert’s:
1️⃣ Jeder Microservice wird mit einem Sidecar-Dienst ausgeführt (z. B. Protokollierung, Überwachung).
2️⃣ Der Sidecar übernimmt Sicherheit, Protokollierung oder Zwischenspeicherung, ohne den Kerndienst zu ändern.
Beispiel: Verwenden von Envoy Proxy als Sidecar
apiVersion: v1
kind: Pod
metadata:
name: user-service
spec:
containers:
- name: user-service
image: user-service:latest
- name: envoy-proxy
image: envoyproxy/envoy
✅ Der Envoy-Proxy verwaltet den Datenverkehr, ohne den Benutzerdienst zu ändern.
Bewährte Methoden:
✔ Verwenden Sie Envoy oder Istio für Sidecar-Proxys.
✔ Bereitstellen von Sidecars in Kubernetes-Pods.
🔟 Database Per Microservice Pattern
Anwendungsfall aus der Praxis:
Eine E-Commerce-Plattform bietet Dienstleistungen für:
- Benutzer (MySQL).
- Bestellungen (PostgreSQL).
- Inventar (MongoDB).
Jeder Dienst hat unterschiedliche Datenanforderungen und sollte eine eigene Datenbank verwenden.
Problem:
Eine einzige Datenbank für alle Microservices führt zu Engpässen und engen Kopplungen.
Lösung:
Jeder Microservice verfügt über eine eigene Datenbank, was die Skalierbarkeit und Unabhängigkeit verbessert.
So funktioniert’s:
1️⃣ Benutzerservice → MySQL
2️⃣ Bestellservice → PostgreSQL
3️⃣ Inventarservice → MongoDB
✅ Microservices bleiben unabhängig und skalierbar.
Bewährte Methoden:
✔ Verwenden Sie polyglotte Persistenz (wählen Sie die beste Datenbank für jeden Dienst aus).
✔ Vermeiden Sie dienstübergreifende Verknüpfungen – verwenden Sie stattdessen APIs.
Fazit
Durch die Implementierung dieser Top 10 Microservices-Pattern können Sie skalierbare, ausfallsichere und effiziente Systeme erstellen.
Eine Übersicht weiterer Pattern finden Sie hier: Liste aller Design-Pattern