Messaging: JMS, Kafka & Event-Driven Architecture
Asynchronität, Events, Commands, DLQ, Idempotenz und Outbox.
Enterprise JavaBeispieleArchitekturOffline HTML
In dieser Datei
Warum Messaging nicht nur Technik ist
Messaging wird oft eingeführt, um Systeme zu entkoppeln. Ohne fachliche Ereignisse, stabile Verträge und Fehlerstrategie entsteht aber nur verteiltes Chaos. Ein Event beschreibt eine fachlich abgeschlossene Tatsache. Ein Command fordert eine Aktion an. Diese Unterscheidung ist entscheidend für Ownership und Verantwortung.
OrderPlaced wird publiziert; Billing, Shipping und Analytics reagieren unabhängig.
| Nachrichtentyp | Bedeutung | Beispiel |
|---|---|---|
| Event | Etwas ist fachlich passiert | OrderPlaced, InvoiceCreated, PaymentReceived |
| Command | Ein System soll etwas tun | CreateInvoice, ReserveStock |
| Document Message | Zustand wird übertragen | CustomerSnapshotUpdated |
| Integration Event | Öffentlicher Vertrag zwischen Systemen | OrderPlacedV2 mit stabilen Feldern |
JMS und Kafka vergleichen
| Kriterium | JMS | Kafka |
|---|---|---|
| Grundidee | Queue/Topic Messaging in Enterprise Middleware | Distributed Event Log / Streaming Plattform |
| Typische Stärke | Transaktionale Enterprise-Integration, klassische App-Server, IBM MQ/Artemis | Hoher Durchsatz, Replay, Event Streaming, Consumer Groups |
| Nachrichtenmodell | Message wird konsumiert und bestätigt | Record bleibt im Log, Offset steuert Fortschritt |
| Fehlerstrategie | Redelivery, DLQ, Broker-Konfiguration | Retry Topics, DLQ, Offset-Management, Idempotenz |
| Architekturfrage | Wer besitzt Queue und Vertrag? | Wer besitzt Topic, Schema und Partition-Key? |
Idempotenz und DLQ
Kafka Consumer mit bewusster Commit-Strategie
public class OrderPlacedConsumer {
private final KafkaConsumer<String, OrderPlacedEvent> consumer;
private final BillingService billing;
public void pollLoop() {
while (true) {
ConsumerRecords<String, OrderPlacedEvent> records = consumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, OrderPlacedEvent> record : records) {
try {
billing.createInvoice(record.value());
consumer.commitSync(Map.of(record.topicPartition(), new OffsetAndMetadata(record.offset() + 1)));
} catch (RecoverableBillingException ex) {
// Pattern: Retry / Dead Letter - nicht endlos denselben Datensatz blockieren.
retryOrSendToDlq(record, ex);
}
}
}
}
}
Gefährlich: Automatisches Commit vor erfolgreicher Verarbeitung kann Datenverlust erzeugen. Endloses Retry ohne DLQ kann Partitionen blockieren.
Outbox Pattern
Das Outbox Pattern löst das klassische Problem: Eine Datenbankänderung und ein Event müssen zuverlässig zusammengehören, aber Datenbank und Broker bilden keine einfache gemeinsame Transaktion. Deshalb schreibt der Use Case zuerst Fachdaten und Outbox-Eintrag in derselben DB-Transaktion. Ein separater Publisher versendet Outbox-Einträge an den Broker.
Transactional Outbox
@Transactional
public void placeOrder(PlaceOrderCommand command) {
Order order = orderFactory.create(command);
order.place();
orderRepository.save(order);
// Pattern: Transactional Outbox - Event wird in derselben DB-Transaktion festgehalten.
outboxRepository.append(new OutboxMessage(
"OrderPlaced",
order.id().value().toString(),
json.serialize(OrderPlaced.from(order))
));
}