← Zurück

Kapitelkompass

Events sind Verträge in Bewegung

Messaging entkoppelt Zeit und Verfügbarkeit, verlangt dafür klare Event-Semantik, Partitionierung und Schema-Evolution.

ProducerAusgangspunktTopicEinordnenPartitionAbsichernConsumerErgebnis
Der Themenweg zeigt die fachliche Leserichtung dieses Kapitels.

Das nimmst du mit

  • Event und Command trennen
  • Keys sinnvoll wählen
  • Reihenfolge einordnen
  • Schemas evolvieren

Praxisfall

OrderCreated muss pro Bestellung geordnet, aber über Bestellungen parallel verarbeitet werden.

Entscheidung

Partitioniere nach der fachlichen Einheit, für die Reihenfolge tatsächlich garantiert sein muss.

!

Typisches Risiko

Ein Event mit Datenbanktabellen-Semantik verteilt internes Design als dauerhaften Vertrag.

08. Messaging, Kafka/JMS und Event Design

Messaging Deep Dive: Kafka/JMS, Event Design, Commands vs Events, Topics, Partitionen, Consumer Groups und Schema-Versionierung.

1. Warum Messaging ein Architekturthema ist

Messaging wird in Enterprise-Projekten oft zu spät ernst genommen. Ein Team beginnt mit sendEvent(order) und merkt erst im Betrieb, dass ein Event nicht nur eine technische Nachricht ist. Ein Event ist ein Vertrag zwischen fachlichem Modell, Laufzeitverhalten und Betrieb. Sobald ein Order-Service einen Billing-Service, ein Reporting-System, ein Audit-System oder eine Legacy-Schnittstelle asynchron beliefert, entstehen neue Designfragen.

Die wichtigste Erkenntnis ist: Messaging entfernt Kopplung nicht, sondern verschiebt sie. Ein synchroner REST-Call koppelt den Aufrufer an Antwortzeit und Verfügbarkeit. Ein asynchrones Event koppelt Publisher und Consumer an Event-Schema, fachliche Bedeutung, Reihenfolge, Wiederholbarkeit und Reprocessing-Strategie.

Synchrone und asynchrone Integration

Schlechte Enterprise-Lösung:

JAVA
public void placeOrder(PlaceOrderCommand command) {
    Order order = repository.save(command.toOrder());
    kafka.send("order-events", order.toJson());
    // keine Version, keine Idempotenz, kein Trace
}

Diese Lösung sieht kurz aus, aber sie verschweigt fast alles, was im Betrieb wichtig wird: Was passiert, wenn der Broker down ist? Ist toJson() ein stabiler Vertrag? Welcher Consumer darf welche Felder verwenden? Kann das Event erneut verarbeitet werden? Wie wird eine alte Event-Version gelesen? Ein Deep-Dive-Design muss diese Fragen vor dem ersten produktiven Release beantworten.

2. Synchron, asynchron und fachliche Grenzen

Synchron bedeutet nicht automatisch schlecht und asynchron bedeutet nicht automatisch modern. Eine Bestellung anzulegen ist für den Benutzer oft ein synchroner Vorgang: Der Client will wissen, ob die Order angenommen wurde. Die spätere Rechnungserzeugung, das Reporting oder eine Benachrichtigung können dagegen asynchron laufen.

Eine robuste Faustregel lautet: Die unmittelbare fachliche Entscheidung bleibt im Application Service. Nebenwirkungen, Projektionen und nachgelagerte Reaktionen können über Events laufen. Wenn aber ein Consumer sofort antworten muss, ist Messaging meist das falsche Werkzeug oder braucht ein explizites Request-Reply-Protokoll.

ASCII-Sicht:

TEXT
Client
  |
  | POST /orders
  v
Order Application Service  -- lokale Entscheidung --> Order accepted/rejected
  |
  | OrderPlaced Event
  v
Broker/Log
  |------------------> Billing Projection
  |------------------> Reporting
  |------------------> Audit

Produktionshinweis: Wenn ein Fachbereich sagt, „Billing muss garantiert danach passieren“, bedeutet das nicht automatisch synchrone Kopplung. Es bedeutet, dass Fehlerpfad, Retry, Monitoring und Eskalation sauber sein müssen.

3. Command, Event und Query wirklich unterscheiden

Commands, Events und Queries sind keine Namenskonventionen, sondern unterschiedliche fachliche Aussagen.

Command Event Query

Ein Command ist eine Aufforderung. Es hat einen intendierten Empfänger und kann abgelehnt werden. Ein Event ist eine Tatsache, die bereits passiert ist. Eine Query liest Daten und sollte keine fachliche Änderung auslösen.

Schlechtes Beispiel:

JAVA
// klingt wie Tatsache, ist aber eigentlich eine Anweisung an Billing
public record InvoiceShouldBeGeneratedEvent(String orderId) {
}

Besser:

JAVA
public record GenerateInvoiceCommand(String orderId) {
}
public record InvoiceGenerated(String invoiceId, String orderId) {
}

Warum das wichtig ist: Wenn ein Consumer InvoiceShouldBeGeneratedEvent liest, weiß er nicht, ob eine Rechnung schon existiert, ob er die Aktion ausführen soll oder ob es nur eine Zustandsinformation ist. Unklare Nachrichtentypen erzeugen unklare Verantwortlichkeiten.

4. Event-Namen und fachliche Granularität

Ein häufiges Anti-Pattern ist das Event OrderChanged. Es klingt flexibel, ist aber für Consumer schlecht. Der Consumer muss herausfinden, was sich geändert hat und ob diese Änderung für ihn relevant ist. Dadurch wandert Fachlogik aus dem Publisher in viele Consumer.

Besser sind konkrete fachliche Ereignisse:

JAVA
public record OrderPlaced(String orderId, String customerId, long amountInCent, String currency) {
}
public record OrderCancelled(String orderId, String reason) {
}
public record PaymentAuthorized(String orderId, String authorizationId) {
}
public record InvoiceGenerated(String invoiceId, String orderId) {
}

Zu fein ist aber ebenfalls gefährlich. OrderLineQuantityFieldChanged ist meistens zu nah an interner Datenstruktur. Ein gutes Event beschreibt eine relevante fachliche Tatsache, nicht eine UI- oder Tabellenänderung.

Checkfrage: Kann ein Fachbereich den Eventnamen verstehen? Falls nein, ist das Event vermutlich zu technisch. Falls ein Consumer aus dem Namen nicht erkennen kann, warum er reagieren sollte, ist es vermutlich zu grob.

5. JMS vs Kafka: verschiedene Denkmodelle

JMS und Kafka werden oft als austauschbare Messaging-Technologien betrachtet. Das ist zu oberflächlich. JMS ist historisch stark auf Nachrichtenzustellung, Queues, Topics, Acknowledgement und Enterprise-Integration ausgerichtet. Kafka ist ein verteiltes append-only Log, in dem Consumer ihre Lesepositionen über Offsets verwalten und Eventströme erneut gelesen werden können.

JMS und Kafka Mentalmodell

JMS passt oft gut für Arbeitsaufträge, Punkt-zu-Punkt-Verarbeitung und klassische Enterprise-Messaging-Szenarien. Kafka passt gut für Event Streaming, Reprocessing, mehrere unabhängige Consumer Groups und hohe Durchsatz-Szenarien.

Eine Migration von JMS nach Kafka ist deshalb keine reine Dependency-Änderung. Das Team muss klären: Wer besitzt Offsets? Wie lange bleiben Events lesbar? Können Consumer neu aufbauen? Wie wird Reihenfolge garantiert? Welche Semantik hat ein Commit?

6. Topic Design und Event-Kategorien

Topic Design ist Architekturdesign. Zu wenige Topics führen zu unklaren Eventströmen. Zu viele Topics erzeugen Betriebskomplexität. Ein Topic sollte fachlich und betrieblich zusammenpassen.

Mögliche Varianten:

Variante Beispiel Vorteil Risiko
Domänen-Topic order-events fachlich verständlich viele Eventtypen in einem Strom
Eventtyp-Topic order-placed leicht gezielt zu abonnieren viele Topics
Kontext-Topic billing-events bounded context nah Grenzfälle bei übergreifenden Flows

Für eine Order/Billing-Plattform ist order-events ein guter Start, wenn Partition Key, Schema-Strategie und Consumer-Vertrag sauber beschrieben sind. Für hochvolumige oder sicherheitskritische Events kann ein eigenes Topic sinnvoll sein.

7. Partitionen, Offsets und Consumer Groups

Kafka-artige Systeme skalieren über Partitionen. Innerhalb einer Partition ist die Reihenfolge stabil. Zwischen Partitionen gibt es keine globale Reihenfolge. Consumer Groups erlauben parallele Verarbeitung: Jede Partition wird innerhalb einer Group typischerweise nur von einem Consumer gleichzeitig gelesen.

Topic Partition Offset

Das wichtigste Missverständnis: Ein Offset ist keine fachliche Verarbeitungsgarantie. Wenn ein Consumer den Offset zu früh committed, kann eine Nachricht technisch als gelesen gelten, obwohl die fachliche Wirkung nicht dauerhaft gespeichert wurde.

Gute Regel: Offset-Commit erst nach idempotenter und dauerhafter Verarbeitung. Bei Consumer-Projektionen heißt das: Projektion speichern, Idempotenz markieren, dann Offset committen.

8. Partition Key: Ordnung gegen Parallelität

Der Partition Key entscheidet, welche Events gemeinsam geordnet werden. Wenn alle Order-Events mit orderId partitioniert werden, sieht ein Consumer für dieselbe Order die Reihenfolge OrderPlaced -> PaymentAuthorized -> InvoiceGenerated innerhalb derselben Partition.

Partition Key Ordering

Schlecht:

JAVA
String partitionKey = UUID.randomUUID().toString();

Besser:

JAVA
String partitionKey = event.aggregateId();
// z.B. orderId

Trade-off: Ein sehr grober Key wie tenantId kann Hot Partitions erzeugen. Ein zu zufälliger Key zerstört fachliche Ordnung. Der richtige Key hängt vom Aggregat und vom wichtigsten Reihenfolgeversprechen ab.

9. Event Envelope: Metadaten sind kein Luxus

Ein Event ohne Envelope ist im Betrieb schwer zu verstehen. Der Envelope trennt fachliche Payload von technischen und organisatorischen Metadaten.

Event Envelope

Wichtige Felder:

JAVA
public record EventEnvelope(
String eventId,
String traceId,
String aggregateType,
String aggregateId,
String eventType,
int schemaVersion,
Instant occurredAt,
String partitionKey,
Map<String, String> headers,
Object payload
) {
}

eventId ermöglicht Idempotenz. traceId verbindet Logs über Services. schemaVersion macht Evolution explizit. partitionKey dokumentiert die Ordnungsentscheidung. Ohne diese Felder wird jeder spätere Fehlerfall manuelle Detektivarbeit.

10. Schema Evolution und Consumer-Kompatibilität

Events sind langlebige Verträge. Ein Consumer kann heute ein Event lesen, das gestern, letzte Woche oder vor einer Migration geschrieben wurde. Deshalb darf ein Publisher ein Event-Schema nicht wie eine interne DTO-Klasse behandeln.

Schema Evolution

Kompatible Änderung:

JAVA
// V1
public record OrderPlacedV1(String orderId, long amountInCent) {
}
// V2: neues Feld mit Default möglich
public record OrderPlacedV2(String orderId, long amountInCent, String currency) {
}

Brechende Änderung:

JAVA
// amount wird plötzlich decimal string statt cent long
public record OrderPlacedV3(String orderId, String amount) {
}

Deep-Dive-Regel: Neue Consumer sollten mehrere Versionen lesen können. Neue Publisher sollten nicht sofort alte Consumer brechen. Ereignisse brauchen ein Evolutionsfenster.

11. Delivery Semantics: at-most-once, at-least-once, exactly-once

Delivery Semantics werden häufig missverstanden. At-most-once vermeidet Duplikate, kann aber Nachrichten verlieren. At-least-once vermeidet Verlust, kann aber Duplikate erzeugen. Exactly-once ist in der Praxis nur innerhalb enger technischer Grenzen gültig und löst nicht automatisch fachliche Idempotenz.

Für Enterprise-Consumer ist at-least-once + idempotenter Consumer meist die ehrlichste Annahme.

JAVA
if (idempotencyStore.alreadyProcessed(event.eventId())) {
    return ProcessingResult.duplicateIgnored(event.eventId());
}
applyBusinessEffect(event);
idempotencyStore.markProcessed(event.eventId());

Wichtig: Idempotenz muss dieselbe dauerhafte Grenze haben wie die fachliche Wirkung. Wenn Projektion und Idempotenz in getrennten Speichern ohne Transaktion liegen, bleibt ein Crash-Fenster.

12. Consumer-Verarbeitung als State Machine

Ein Consumer ist kein forEach(event -> handle(event)). Er ist eine kleine State Machine mit technischen und fachlichen Zuständen.

Consumer State Machine

Typischer Ablauf:

TEXT
Received
  -> validate envelope
  -> validate schema version
  -> check idempotency
  -> apply business effect
  -> mark idempotent
  -> commit offset

Fehler können an jeder Stelle auftreten. Deshalb muss der Code unterscheiden: kaputtes Event, temporärer DB-Fehler, unbekannte Version, Duplikat, fachlich nicht mehr relevante Nachricht.

13. Code-Walkthrough: InMemoryKafkaBroker

Das Code-Lab enthält keinen echten Kafka-Client, sondern ein bewusst kleines Modell, das die Konzepte sichtbar macht: Topics, Partitionen, Offsets und Consumer Groups. Dadurch kann man die Logik mit reinem JDK 21 kompilieren und testen.

Ausschnitt:

JAVA
public List<ConsumerRecord> poll(String topicName, String groupId, int maxRecords) {
    Topic topic = topics.get(topicName);
    ConsumerGroupState group = groups.computeIfAbsent(groupId, ConsumerGroupState::new);
    List<ConsumerRecord> records = new ArrayList<>();
    for (Partition partition : topic.partitions()) {
        long nextOffset = group.nextOffset(topicName, partition.id());
        records.addAll(partition.readFrom(nextOffset, maxRecords - records.size()));
    }
return records;
}

Der Code zeigt bewusst: Consumer Groups besitzen eigene Offsets. Zwei Gruppen können dieselben Events unabhängig lesen.

14. Code-Walkthrough: EventSchemaRegistry

Die Registry im Lab ist klein, aber fachlich wichtig. Sie verhindert, dass ein Consumer ungeprüft unbekannte Versionen verarbeitet.

JAVA
public CompatibilityCheck validate(EventEnvelope event) {
    Set<Integer> versions = acceptedVersions.get(event.eventType());
    if (versions == null) return CompatibilityCheck.unknownType(event.eventType());
    if (!versions.contains(event.schemaVersion())) {
        return CompatibilityCheck.rejectedVersion(event.eventType(), event.schemaVersion());
    }
return CompatibilityCheck.accepted(event.eventType(), event.schemaVersion());
}

In echten Projekten kann dahinter Avro, JSON Schema, Protobuf oder ein Schema Registry Service stehen. Das Mentalmodell bleibt gleich: Consumer entscheiden bewusst, welche Verträge sie akzeptieren.

15. Code-Walkthrough: Idempotenter Projection Consumer

Der BillingProjectionConsumer baut aus OrderPlaced-Events eine eigene Sicht. Er verarbeitet Duplikate nicht erneut, lehnt unbekannte Versionen ab und schreibt kaputte Events in eine Dead Letter Queue.

JAVA
public ProcessingResult handle(EventEnvelope event) {
    if (idempotencyStore.alreadyProcessed(event.eventId())) {
        return ProcessingResult.duplicateIgnored(event.eventId());
    }
CompatibilityCheck check = schemaRegistry.validate(event);
if (!check.accepted()) {
    deadLetterQueue.add(event, check.reason());
    return ProcessingResult.rejected(event.eventId(), check.reason());
}
projection.apply(event);
idempotencyStore.markProcessed(event.eventId());
return ProcessingResult.processed(event.eventId());
}

Das ist kein vollständiges Produktions-Framework, aber es zeigt die richtige Denkweise: Validierung vor Wirkung, Idempotenz vor erneuter Wirkung, Dead Letter statt stiller Datenkorruption.

16. Anti-Patterns im Messaging

Typische Fehler in Enterprise-Projekten:

Schlechtes Beispiel:

JAVA
public record DbOrderRowChanged(Map<String, Object> row) {
}

Besser:

JAVA
public record OrderPlaced(String orderId, String customerId, long amountInCent, String currency) {
}

Der Unterschied ist nicht syntaktisch, sondern fachlich: Das gute Event beschreibt eine Tatsache im Geschäftsprozess.

17. Tests im Messaging-Design

Messaging muss mit Szenarien getestet werden, nicht nur mit einzelnen Methoden. Das Lab enthält Tests für Consumer-Groups, Idempotenz und Schema-Abweisung.

Wichtige Testfälle:

TEXT
1. Billing und Reporting lesen denselben Eventstrom unabhängig.
2. Duplikat mit gleicher eventId erzeugt keine zweite Projektion.
3. Unbekannte Schema-Version landet in DLQ.
4. Partition Key hält Events einer Order zusammen.
5. Consumer committen erst nach Verarbeitung.

Diese Tests sind Architekturtests im Kleinen. Sie prüfen nicht nur Java-Methoden, sondern die Annahmen des Integrationsdesigns.

18. Produktionscheckliste für Kapitel

Vor einem produktiven Messaging-Design sollte diese Liste beantwortet sein:

Wenn diese Fragen offen sind, ist das System nicht tief verstanden, auch wenn der Code bereits Nachrichten sendet.

19. Durchgehender Order-to-Invoice Messaging-Flow

Der Order-to-Invoice-Flow ist ein gutes Beispiel, weil mehrere fachliche Verantwortungen beteiligt sind. Der Order-Service entscheidet, ob eine Bestellung angenommen wird. Inventory reserviert Bestand. Payment autorisiert Zahlung. Billing erzeugt eine Rechnung. Reporting und Audit hören mit, ohne den Kernfluss zu blockieren.

Ein schlechter Fluss wäre ein einziger synchroner Ablauf:

JAVA
orderRepository.save(order);
inventoryClient.reserve(order);
paymentClient.authorize(order);
billingClient.generateInvoice(order);
reportingClient.update(order);

Dieser Code ist einfach zu lesen, aber operativ gefährlich. Jeder Downstream-Ausfall blockiert die Bestellung. Timeouts werden zu fachlichen Abbrüchen. Wiederholungen können Payment oder Billing doppelt ausführen. Außerdem ist nicht klar, welche Teilschritte schon dauerhaft passiert sind.

Eine bessere Choreografie trennt lokale Entscheidung und nachgelagerte Reaktionen:

TEXT
OrderApplicationService
  -> Order speichern
  -> Outbox(OrderPlaced) schreiben
  -> Commit

OutboxPublisher
  -> order-events publizieren

InventoryConsumer
  -> Bestand reservieren
  -> InventoryReserved publizieren

BillingConsumer
  -> nach PaymentAuthorized Rechnung erzeugen

Deep-Dive-Punkt: Choreografie bedeutet nicht, dass niemand verantwortlich ist. Der fachliche Prozess braucht weiterhin Monitoring, Prozesszustände und Eskalationsregeln.

20. Topic-Design-Alternativen im Vergleich

Eine Enterprise-Plattform kann Topics nach Domäne, Eventtyp, Sicherheitszone oder Betriebsprofil schneiden. Das ist keine reine Geschmacksfrage.

Entscheidung Vorteil Risiko Geeignet wenn
order-events einfache fachliche Landkarte viele Eventtypen Order-Kontext hat moderate Eventmenge
payment-events Sicherheit/Compliance getrennt mehr Topic-Verwaltung Payment braucht eigene Retention/ACL
order-placed Consumer können gezielt abonnieren Topic-Wildwuchs sehr hohe Last pro Eventtyp
audit-events Audit getrennt betreibbar Doppelpublikation möglich Audit ist eigener Bounded Context

Ein guter Architekt dokumentiert nicht nur den gewählten Topic-Namen, sondern auch die verworfenen Alternativen. Später versteht man sonst nicht, warum das System so geschnitten wurde.

21. Event-Versionierung mit Migrationsfenster

Ein Event-Schema muss über ein Migrationsfenster betrachtet werden. Publisher und Consumer werden selten exakt gleichzeitig deployt. Deshalb braucht man Phasen:

TEXT
Phase 1: Consumer kann V1 lesen
Phase 2: Consumer kann V1 und V2 lesen
Phase 3: Publisher sendet V2
Phase 4: alte V1-Events laufen aus Retention/Archiv
Phase 5: Consumer-Unterstützung für V1 wird entfernt

Schlecht:

JAVA
// Publisher wechselt sofort von V1 zu V2, Consumer versteht nur V1.
broker.publish(new OrderPlacedV2(orderId, amount, currency, taxRate));

Besser:

JAVA
// Consumer akzeptiert vor Publisher-Umstellung beide Versionen.
registry.register("OrderPlaced", 1, 2);

Produktionshinweis: Die Retention des Brokers ist Teil der Schema-Strategie. Wenn Events 14 Tage im Topic bleiben, muss ein Consumer in dieser Zeit alte Versionen lesen können.

22. Consumer-Group-Skalierung und Rebalancing

Consumer Groups skalieren nicht beliebig. Die maximale parallele Verarbeitung innerhalb einer Group ist durch die Anzahl der Partitionen begrenzt. Zehn Consumer auf drei Partitionen bedeuten, dass sieben Consumer keine Partition aktiv bearbeiten.

Rebalancing ist ein Betriebsereignis. Wenn Consumer starten, sterben oder langsam werden, werden Partitionen neu verteilt. Währenddessen können Pausen entstehen. Consumer-Code muss deshalb damit umgehen, dass Verarbeitung unterbrochen und später fortgesetzt wird.

Wichtige Regel:

TEXT
Mehr Consumer erhöhen nur dann Durchsatz,
wenn genügend Partitionen existieren und die Downstream-Systeme mithalten.

Ein Billing-Consumer kann Kafka schnell lesen, aber die Datenbank oder der Payment-Adapter kann trotzdem der Engpass sein. Dann hilft mehr Kafka-Parallelität nicht, sondern erzeugt Druck auf abhängige Systeme.

23. Offset-Commit-Fehlerfälle

Offset-Commit ist einer der wichtigsten Punkte im Consumer-Design.

Schlecht:

JAVA
record = poll();
commitOffset(record);
// zu früh
projection.apply(record);
// kann danach fehlschlagen

Wenn projection.apply fehlschlägt, ist das Event technisch als gelesen markiert, aber fachlich nicht verarbeitet. Das ist Datenverlust.

Besser:

JAVA
record = poll();
projection.apply(record);
idempotency.markProcessed(record.eventId());
commitOffset(record);

Jetzt kann ein Crash vor dem Commit zu erneuter Lieferung führen. Deshalb muss die Projektion idempotent sein. Das ist der ehrliche Preis von at-least-once.

24. JMS-Queue vs Kafka-Log an einem Beispiel

Bei einer JMS-Queue wird ein Arbeitsauftrag typischerweise von einem Consumer abgeholt und nach Acknowledgement aus Sicht der Queue erledigt. Das passt gut zu Aufgaben wie GenerateInvoiceCommand.

Bei Kafka bleibt das Event im Log, und jede Consumer Group verwaltet ihre eigene Position. Das passt gut zu Tatsachen wie OrderPlaced, weil Billing, Reporting und Audit unabhängig lesen können.

Nicht gut:

TEXT
OrderPlaced als JMS-Arbeitsauftrag behandeln,
obwohl mehrere unabhängige Systeme dieselbe Tatsache brauchen.

Nicht gut:

TEXT
GenerateInvoiceCommand als Kafka-Domänen-Event publizieren,
obwohl es einen klaren Empfänger und eine Ausführungsabsicht hat.

25. Event-Namenskatalog für die Fallstudie

Gute Eventnamen für die Order/Billing/Payment-Domäne:

TEXT
OrderPlaced
OrderAccepted
OrderRejected
InventoryReserved
InventoryReservationFailed
PaymentAuthorized
PaymentDeclined
InvoiceGenerated
InvoiceGenerationFailed
OrderCancelled
RefundRequested
RefundCompleted

Vorsichtige oder schlechte Namen:

TEXT
OrderChanged
OrderUpdated
ProcessOrderEvent
DoBillingEvent
PaymentStatusChanged
DataSynced

Warum? Die schlechten Namen beschreiben entweder zu wenig Fachlichkeit oder eine technische Aktivität statt einer fachlichen Tatsache.

26. Event-Payload: Minimal, aber ausreichend

Ein Event sollte genug enthalten, damit Consumer ihre Aufgabe erfüllen können, aber nicht das komplette interne Aggregat dumpen.

Zu wenig:

JAVA
public record OrderPlaced(String orderId) {
}

Jetzt müssen Consumer eventuell den Order-Service synchron nachladen. Dadurch entsteht neue Kopplung.

Zu viel:

JAVA
public record OrderPlaced(OrderEntityJpaDump dump) {
}

Jetzt wird die interne Datenstruktur zum öffentlichen Vertrag.

Besser:

JAVA
public record OrderPlaced(
String orderId,
String customerId,
long grossAmountInCent,
String currency,
List<OrderLineSnapshot> lines
) {
}

Snapshot bedeutet: fachlich relevante Daten zum Zeitpunkt des Events, nicht Live-Entity.

27. Testkatalog für Messaging-Architektur

Ein echter Messaging-Testkatalog enthält mehr als einen Happy Path.

TEXT
- Event wird einmal publiziert und einmal verarbeitet.
- Dasselbe Event wird zweimal geliefert und nur einmal angewendet.
- Unbekannte Version wird abgelehnt.
- Alter Consumer liest alte Version weiter.
- Neue optionale Felder brechen alte Consumer nicht.
- Partition Key hält Order-Reihenfolge.
- Zwei Consumer Groups lesen unabhängig.
- Consumer-Lag wird erkennbar.
- DLQ-Eintrag enthält Ursache und Trace-ID.

Diese Tests sind nicht nur technische Tests. Sie sichern Architekturentscheidungen.

28. Kapitel-6A-Produktionscheckliste

Vor Produktion müssen mindestens diese Punkte geklärt sein:

TEXT
Event-Katalog dokumentiert
Topic-Katalog dokumentiert
Partition-Key-Strategie dokumentiert
Schema-Versionierungsregel dokumentiert
Consumer-Groups bekannt
Offset-Commit-Regel festgelegt
Idempotenzspeicher definiert
DLQ-Prozess definiert
Trace-ID und Correlation-ID definiert
Retention und Reprocessing geklärt
ACL/Security für Topics geklärt
Monitoring und Alerts geplant

Wenn ein Team nur den Producer-Code zeigen kann, aber diese Liste nicht beantworten kann, ist das Messaging-Design noch nicht deep-dive-reif.

Code-Anhang - Kurzüberblick

Der vollständige Code liegt unter code/messaging-Kapitel-rebuild. Das PDF hält Code bewusst kurz; die HTML- und ZIP-Fassung enthalten den vollständigen Code.

⌂ Cockpit