Messaging: JMS, Kafka & Event-Driven Architecture

Asynchronität, Events, Commands, DLQ, Idempotenz und Outbox.

Enterprise JavaBeispieleArchitekturOffline HTML

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.
OrderPlaced wird publiziert; Billing, Shipping und Analytics reagieren unabhängig.
NachrichtentypBedeutungBeispiel
EventEtwas ist fachlich passiertOrderPlaced, InvoiceCreated, PaymentReceived
CommandEin System soll etwas tunCreateInvoice, ReserveStock
Document MessageZustand wird übertragenCustomerSnapshotUpdated
Integration EventÖffentlicher Vertrag zwischen SystemenOrderPlacedV2 mit stabilen Feldern

JMS und Kafka vergleichen

KriteriumJMSKafka
GrundideeQueue/Topic Messaging in Enterprise MiddlewareDistributed Event Log / Streaming Plattform
Typische StärkeTransaktionale Enterprise-Integration, klassische App-Server, IBM MQ/ArtemisHoher Durchsatz, Replay, Event Streaming, Consumer Groups
NachrichtenmodellMessage wird konsumiert und bestätigtRecord bleibt im Log, Offset steuert Fortschritt
FehlerstrategieRedelivery, DLQ, Broker-KonfigurationRetry Topics, DLQ, Offset-Management, Idempotenz
ArchitekturfrageWer 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))
    ));
}
⌂ Cockpit