Enterprise Integration Patterns

Endpunkte

Enterprise Integration Patterns · Gruppe 7 von 8

Lesestand

Endpunkte sind die Stellen, an denen eine Anwendung mit dem Messaging-System in Berührung kommt. Gruppe 2 hat mit dem Message Endpoint das Grundprinzip gezeigt. Diese Gruppe beschreibt, wie Endpunkte im Detail gebaut werden. Einige Muster halten den Messaging-Code aus der Geschäftslogik heraus, andere regeln, wie ein Empfänger an Nachrichten kommt, wie mehrere Empfänger sich die Last teilen oder ihre Nachrichten auswählen, und wie Empfänger Ausfälle und Duplikate unbeschadet überstehen.

  • Anwendung
  • Integrationskomponente
  • Kanal
  • Nachricht
  • Datenbank / Speicher

47. Messaging Gateway · Messaging-Zugang

ShopGeschäftslogiklager.reserviere(4711)MessagingGatewayLagersieht aus wie ein normaler Methodenaufrufdahinter: Nachrichten, Kanäle, Broker
Die Geschäftslogik ruft eine gewöhnliche Methode auf. Dass dahinter Nachrichten verschickt werden, weiß nur das Gateway.

Die Idee

Ein Messaging Gateway verbirgt das Messaging vollständig hinter einer normalen, fachlichen Schnittstelle. Die Geschäftslogik ruft eine Methode wie lager.reserviere(artikel) auf und weiß nicht, dass dahinter eine Nachricht erzeugt, in einen Kanal gelegt und eine Antwort abgewartet wird. Das Gateway ist die konkrete Ausprägung des Message Endpoint aus Gruppe 2.

Beispiel

Im Shop gibt es ein Interface Lager mit den Methoden reserviere() und bestand(). Die Implementierung ist ein Messaging Gateway, das daraus Command Messages bzw. Request-Reply-Anfragen macht. Für Tests wird das Gateway durch eine einfache Attrappe ersetzt, die ohne Broker auskommt. Wechselt die Firma später zu einem anderen Messaging-System, bleibt die Geschäftslogik unverändert.

Worauf achten

Das Gateway kann synchron sein, die Methode wartet also auf die Antwort, oder asynchron, dann liefert sie sofort ein CompletableFuture oder nimmt einen Rückruf entgegen. Die synchrone Variante ist bequemer, verdeckt aber, dass ein Aufruf langsam sein oder ausfallen kann. Messaging-typische Fehler wie Zeitüberschreitungen sollte das Gateway in fachliche Ausnahmen übersetzen, statt Broker-Ausnahmen bis in die Geschäftslogik durchzureichen.

In der Praxis

@MessagingGateway in Spring Integration, das aus einem Interface automatisch ein Gateway erzeugt, ProducerTemplate und Proxys über @Produce in Camel. Das Gateway verbindet die GoF-Muster Facade und Proxy mit der Messaging-Welt.

48. Messaging Mapper · Nachrichtenabbildung

Objekte im SpeicherBestellungKundePosition 1Position 2MessagingMapperLager{ "nr": 4711, "kunde": 815,"positionen": [ ... ] }flache Nachricht ohne Objektverweise
Der Mapper wandelt das Objektgeflecht aus Bestellung, Kunde und Positionen in eine flache Nachricht um.

Die Idee

Im Programm sind Daten als Objekte organisiert, die aufeinander verweisen: Eine Bestellung kennt ihren Kunden und ihre Positionen. Eine Nachricht dagegen ist ein flaches, in sich abgeschlossenes Dokument. Der Messaging Mapper übersetzt zwischen beiden Welten: Er baut aus Domänenobjekten eine Nachricht und aus einer empfangenen Nachricht wieder Objekte. Die Domänenklassen selbst wissen nichts von Nachrichten.

Beispiel

Die Klasse Bestellung hat eine Referenz auf ein Kunde-Objekt und eine Liste von Position-Objekten. Der Mapper entscheidet, was in die Nachricht gehört: vom Kunden nur die Nummer, von den Positionen Artikel und Menge. Daraus entsteht ein JSON-Dokument. Auf der Empfängerseite baut ein Mapper daraus wieder passende Objekte.

Worauf achten

Das Muster ist eng verwandt mit dem Data Mapper aus Fowlers PoEAA, der dasselbe für Datenbanken leistet. Die Kernfragen sind ähnlich: Wie tief folgt man Verweisen? Was passiert mit Zyklen? Was wird mitgeschickt, was nur als ID? Wichtig ist, die Nachricht als eigenen Vertrag zu behandeln. Wer interne Klassen einfach automatisch serialisiert, macht jede Änderung an einer internen Klasse zu einer Änderung der Schnittstelle nach außen.

In der Praxis

Jackson und JSON-B in Java, MapStruct für die Abbildung zwischen Domänenobjekten und eigenen Nachrichtenklassen, Protobuf und Avro mit generierten Nachrichtenklassen. In Java 21 sind Records ideal als eigene, unveränderliche Nachrichtenklassen, getrennt von den Domänenklassen.

49. Transactional Client · Transaktionaler Client

eine Transaktion: alles oder nichtseingang1. empfangenLagerDB2. Bestand buchen3. sendenausgangFehler bei Schritt 3? Dann gilt auch 1 und 2 als nicht geschehen.
Nachricht empfangen, Bestand buchen und Bestätigung senden gelingen entweder gemeinsam oder gar nicht.

Die Idee

Ein Transactional Client fasst mehrere Messaging-Schritte, und oft auch Datenbankzugriffe, zu einer Transaktion zusammen. Entweder gelingen alle Schritte, oder keiner gilt als ausgeführt. Eine empfangene Nachricht wird erst dann endgültig aus dem Kanal entfernt, wenn die Verarbeitung vollständig erfolgreich war.

Beispiel

Das Lager empfängt eine Bestellung, bucht den Bestand in seiner Datenbank und schickt eine Bestätigung. Stürzt das Programm nach dem Buchen ab, aber vor dem Senden, wäre ohne Transaktion der Bestand verringert, aber niemand erführe davon. Mit Transaktion wird alles zurückgerollt: Die Buchung wird verworfen, die Bestellung liegt wieder im Kanal und wird erneut zugestellt.

Worauf achten

Eine Transaktion nur über Messaging-Schritte ist meist einfach, weil der Broker sie unterstützt. Schwierig wird es, wenn Broker und Datenbank gemeinsam in einer Transaktion stehen sollen. Verteilte Transaktionen (XA, Zwei-Phasen-Commit) sind langsam und fehleranfällig und werden heute meist vermieden. Verbreitete Alternativen: das Outbox-Muster, bei dem die ausgehende Nachricht zunächst in derselben Datenbanktransaktion gespeichert und danach verschickt wird, und idempotente Empfänger, die doppelte Zustellung verkraften (Muster 56).

In der Praxis

Transaktionale Sessions in JMS (Session.SESSION_TRANSACTED), @Transactional zusammen mit JmsTransactionManager in Spring, Transaktionen und „exactly once“-Semantik in Kafka. Für die Kombination mit der Datenbank in der Praxis meist das Outbox-Muster, etwa mit Debezium.

50. Polling Consumer · Abfragender Empfänger

ShopLagerreceive()alle 5 Sek.:„Gibt esNeues?“Der Empfänger bestimmt selbst, wann und wie viele Nachrichten er abholt
Das Lager holt sich in festen Abständen ab, was im Kanal liegt, so viel, wie es gerade bewältigen kann.

Die Idee

Ein Polling Consumer holt Nachrichten aktiv ab: Er fragt den Kanal, ob etwas da ist, und nimmt sich die nächste Nachricht, wenn er dazu bereit ist. Wie jemand, der ab und zu in den Briefkasten schaut. Der Empfänger steuert damit selbst sein Tempo.

Beispiel

Das Lager fragt alle fünf Sekunden nach neuen Bestellungen und nimmt sich jeweils bis zu 50 auf einmal. In der Mittagspause, wenn wenig Personal da ist, wird das Intervall verlängert. Nachts verarbeitet ein Stapellauf alle Rückstände in einem Rutsch. Der Shop merkt davon nichts: Er legt Bestellungen einfach in den Kanal.

Worauf achten

Der Vorteil ist die Kontrolle: Der Empfänger wird nie mit mehr Arbeit überschüttet, als er bewältigen kann. Man spricht auch von Backpressure. Der Nachteil: Häufiges Nachfragen kostet Ressourcen, auch wenn nichts da ist, und seltenes Nachfragen verzögert die Verarbeitung. Abhilfe schafft Long Polling: Der Empfänger fragt und wartet eine gewisse Zeit, bis etwas kommt. Das Gegenstück ist der Event-Driven Consumer (Muster 51).

In der Praxis

MessageConsumer.receive(timeout) in JMS, KafkaConsumer.poll(), das Kafka grundsätzlich nach diesem Prinzip arbeitet, ReceiveMessage mit Long Polling in Amazon SQS. In Java ist BlockingQueue.poll(timeout, unit) das Gleiche im Kleinen.

51. Event-Driven Consumer · Ereignisgesteuerter Empfänger

ShopLagerListeneronMessage(m)wartet passivDie Nachricht wird zugestellt, sobald sie da ist, ganz ohne Nachfragen
Der Listener wird automatisch aufgerufen, sobald eine Bestellung eintrifft.

Die Idee

Ein Event-Driven Consumer wartet passiv. Er meldet beim Messaging-System eine Rückrufmethode an, und sobald eine Nachricht eintrifft, ruft das System diese Methode auf. Wie eine Türklingel statt regelmäßig in den Briefkasten zu schauen.

Beispiel

Das Lager registriert einen Listener für den Kanal bestellungen. Trifft eine Bestellung ein, wird onMessage() sofort aufgerufen, und die Kommissionierung beginnt ohne Verzögerung. Ist nichts los, verbraucht der Listener keine Rechenzeit.

Worauf achten

Der Vorteil ist die sofortige Reaktion ohne unnötige Abfragen. Der Nachteil: Der Empfänger hat weniger Kontrolle über sein Tempo. Kommen plötzlich tausend Nachrichten auf einmal, muss die Infrastruktur dafür sorgen, dass er nicht überlastet wird, etwa über eine begrenzte Zahl gleichzeitiger Aufrufe oder einen Puffer. In der Praxis verwischt die Grenze zum Polling Consumer: Viele Listener-Frameworks fragen im Hintergrund selbst ab und rufen dann die Anwendung auf.

In der Praxis

MessageListener.onMessage() in JMS, @JmsListener, @KafkaListener und @RabbitListener in Spring, Message-Driven Beans in Jakarta EE, Cloud-Funktionen wie AWS Lambda, die durch Nachrichten ausgelöst werden.

52. Competing Consumers · Konkurrierende Empfänger

ShopQueue "bestellungen"Worker 1Worker 2Worker 3Worker 4bei Last zuschaltenwer gerade frei ist,nimmt die nächste
Drei Worker teilen sich die Bestellungen. Steigt die Last, wird einfach ein vierter gestartet.

Die Idee

Mehrere gleichartige Empfänger lesen aus demselben Point-to-Point Channel und konkurrieren um die Nachrichten. Jede Nachricht bekommt genau einer von ihnen, nämlich der, der gerade frei ist. So verteilt sich die Last automatisch, und die Verarbeitung lässt sich einfach durch weitere Empfänger beschleunigen.

Beispiel

Im Weihnachtsgeschäft kommen zehnmal mehr Bestellungen als sonst. Statt das Lagersystem umzubauen, startet der Betrieb zusätzliche Worker, die alle aus derselben Queue lesen. Im Januar werden sie wieder abgeschaltet. Fällt ein Worker aus, übernehmen die anderen seine Arbeit, denn eine Nachricht, deren Verarbeitung nicht bestätigt wurde, wird erneut zugestellt.

Worauf achten

Competing Consumers sind die einfachste Form der Skalierung im Messaging, mit zwei typischen Nebenwirkungen. Erstens geht die Reihenfolge verloren: Worker 2 kann vor Worker 1 fertig werden. Ist die Reihenfolge wichtig, etwa für alle Nachrichten eines Kunden, müssen zusammengehörige Nachrichten immer beim selben Worker landen. Kafka erreicht das über Partitionen. Zweitens müssen die Worker ohne gemeinsamen Zustand im Speicher auskommen, weil jede Nachricht bei einem anderen Worker landen kann.

In der Praxis

Mehrere Listener auf einer JMS- oder RabbitMQ-Queue, eine Kafka-Consumer-Group mit mehreren Mitgliedern, automatische Skalierung von Containern anhand der Queue-Länge, etwa mit KEDA in Kubernetes.

53. Message Dispatcher · Nachrichtenverteiler

ShopLageranwendungDispatcherBearbeiter BücherBearbeiter TechnikBearbeiter Frischeein Kanal-Empfänger
Die Lageranwendung hat genau einen Empfänger am Kanal. Intern verteilt er jede Bestellung an den passenden Bearbeiter.

Die Idee

Ein Message Dispatcher ist ein einzelner Empfänger am Kanal, der Nachrichten innerhalb einer Anwendung an mehrere Bearbeiter weiterreicht. Für das Messaging-System gibt es nur einen Konsumenten. Intern entscheidet der Dispatcher, welcher Bearbeiter, etwa welcher Thread oder welche Klasse, eine Nachricht übernimmt.

Beispiel

Die Lageranwendung liest aus einem Kanal und hat drei interne Bearbeiter für Bücher, Technik und Frischware. Der Dispatcher holt jede Bestellung und übergibt sie dem passenden Bearbeiter, jeweils in einem eigenen Thread. So lassen sich die Bearbeiter getrennt entwickeln und unterschiedlich viele Threads für jeden einplanen.

Worauf achten

Der Unterschied zu Competing Consumers: Dort konkurrieren mehrere Empfänger direkt am Kanal, und das Messaging-System verteilt. Beim Dispatcher verteilt die Anwendung selbst. Das gibt mehr Kontrolle, etwa über die Zahl der Threads oder die Zuordnung nach Inhalt, und belastet den Broker mit nur einer Verbindung. Der Unterschied zum Content-Based Router: Der Dispatcher verteilt innerhalb einer Anwendung an Objekte, der Router zwischen Kanälen.

In der Praxis

Listener-Container in Spring mit einem Thread-Pool (concurrency), ein ExecutorService, an den ein Empfänger Aufgaben weitergibt, und in Java 21 besonders einfach mit virtuellen Threads: ein virtueller Thread pro empfangener Nachricht.

54. Selective Consumer · Wählerischer Empfänger

ShopLager Kölnselector: land = 'DE'Lager Wienselector: land = 'AT'jeder nimmt nur, was zuseinem Selektor passt
Zwei Lager lesen aus demselben Kanal, jedes mit einem Selektor. Köln bekommt die deutschen, Wien die österreichischen Bestellungen.

Die Idee

Ein Selective Consumer gibt beim Empfang ein Auswahlkriterium an, einen Selektor. Er erhält nur die Nachrichten aus dem Kanal, die dazu passen. Mehrere Empfänger mit unterschiedlichen Selektoren können sich so einen Kanal teilen, ohne sich gegenseitig Nachrichten wegzunehmen, die nicht für sie bestimmt sind.

Beispiel

Die Lager in Köln und Wien lesen aus einem gemeinsamen Bestellkanal. Köln gibt den Selektor land = 'DE' an, Wien land = 'AT'. Das Messaging-System wertet den Selektor aus und stellt jedem Lager nur seine Bestellungen zu.

Worauf achten

Selektoren arbeiten meist nur auf Header-Feldern, nicht auf dem Inhalt, weil der Broker den Body nicht versteht. Das ist ein Grund, routingrelevante Angaben in den Header zu schreiben. Vorsicht bei Nachrichten, auf die kein Selektor passt: Sie bleiben im Kanal liegen und können ihn verstopfen. Oft sind getrennte Kanäle (Datatype Channel) oder ein Content-Based Router die übersichtlichere Lösung. Der Selective Consumer verlagert dieselbe Entscheidung in den Empfänger.

In der Praxis

Message Selectors in JMS, ein SQL-ähnlicher Ausdruck wie land = 'DE' AND betrag > 100 beim Anlegen des Consumers, Subscription-Filter in Azure Service Bus, Filter-Policies für SQS-Abonnements bei Amazon SNS.

55. Durable Subscriber · Dauerhafter Abonnent

ShopTopic "preise"Shop-Frontendläuft: bekommt sofortgespeichert für ihnPreisvergleichgerade offlineNachholen nach dem Neustart
Der Preisvergleich ist kurz offline. Seine Nachrichten werden aufbewahrt und nach dem Neustart zugestellt.

Die Idee

Bei einem normalen Publish-Subscribe Channel erhält ein Abonnent nur Nachrichten, die eintreffen, während er verbunden ist. Was in seiner Abwesenheit verschickt wurde, verpasst er. Ein Durable Subscriber dagegen hat ein dauerhaftes Abonnement: Das Messaging-System bewahrt seine Nachrichten auf, solange er offline ist, und stellt sie zu, sobald er sich wieder verbindet.

Beispiel

Ein Preisvergleichsdienst abonniert das Topic preise. Während eines Updates ist er eine halbe Stunde offline. In dieser Zeit ändern sich 300 Preise. Als dauerhafter Abonnent erhält er nach dem Neustart alle 300 Änderungen und ist wieder auf dem neuesten Stand. Das Shop-Frontend, das immer läuft, hat sie in der Zwischenzeit direkt bekommen.

Worauf achten

Dauerhafte Abonnements kosten Speicher: Meldet sich ein Abonnent nie wieder, sammeln sich seine Nachrichten unbegrenzt an. Abonnements müssen deshalb gepflegt und nicht mehr benötigte gelöscht werden, oder die Nachrichten erhalten eine Ablaufzeit. Ein Abonnent braucht eine feste Kennung, damit das System ihn nach dem Neustart wiedererkennt. Für Daten, bei denen nur der aktuelle Stand zählt, wie bei Preisen, ist es manchmal einfacher, nach dem Start den aktuellen Zustand neu abzufragen.

In der Praxis

createDurableSubscriber() bzw. createSharedDurableConsumer() in JMS, durable Queues pro Abonnent hinter einem Fanout-Exchange in RabbitMQ. In Kafka ist jeder Konsument im Grunde dauerhaft: Nachrichten werden ohnehin für eine einstellbare Zeit aufbewahrt, und jede Consumer Group merkt sich, wie weit sie gelesen hat.

56. Idempotent Receiver · Idempotenter Empfänger

Shop171817Buchhaltungschon verarbeitet:17, 1817 erneut? ignorierenRechnung nur einmalDuplikat verworfenNachricht 17 doppelt, z. B. nacheinem VerbindungsabbruchNachrichten-ID im Header
Nachricht 17 kommt zweimal an. Die Buchhaltung erkennt das Duplikat und erstellt die Rechnung nur einmal.

Die Idee

In verteilten Systemen kann eine Nachricht mehrfach ankommen, zum Beispiel wenn der Sender nach einem Verbindungsabbruch nicht weiß, ob sie angekommen ist, und sie zur Sicherheit noch einmal schickt. Ein Idempotent Receiver ist so gebaut, dass eine wiederholte Nachricht keinen zusätzlichen Effekt hat. Ob er sie einmal oder fünfmal erhält, das Ergebnis ist dasselbe.

Beispiel

Die Buchhaltung erstellt für jede Bestellung eine Rechnung. Bestellung 17 kommt wegen eines Netzwerkfehlers zweimal an. Die Buchhaltung merkt sich die IDs aller verarbeiteten Nachrichten, erkennt die 17 wieder und verwirft das Duplikat. Der Kunde bekommt nur eine Rechnung.

Worauf achten

Es gibt zwei Wege zur Idempotenz. Erstens: Duplikate erkennen, indem man sich verarbeitete Nachrichten-IDs merkt. Das braucht einen dauerhaften Speicher und eine Regel, wann alte IDs gelöscht werden. Zweitens: Operationen von Natur aus idempotent gestalten. „Setze den Bestand auf 12“ ist idempotent, „verringere den Bestand um 2“ nicht. „Markiere Bestellung 17 als versandt“ ist idempotent, „lege eine Rechnung an“ nicht. Da Guaranteed Delivery in der Praxis fast immer „mindestens einmal“ bedeutet, ist Idempotenz eine der wichtigsten Eigenschaften robuster Empfänger.

In der Praxis

idempotentConsumer() in Camel mit einem Speicher für bereits gesehene IDs (im Speicher, JDBC, Redis), IdempotentReceiverInterceptor in Spring Integration, Datenbank-Constraints auf der Nachrichten-ID, Idempotency Keys in Zahlungs-APIs wie bei Stripe.

57. Service Activator · Dienstaktivierer

ShopServiceActivatorruft aufLagerdienstbestandPruefen(art)normale Methode,weiß nichts von MessagingWeb-Oberflächederselbe Dienst, direkt aufgerufenübersetzt Nachricht in Aufrufund Ergebnis in Antwort
Der Lagerdienst ist eine ganz normale Methode. Der Service Activator macht sie per Nachricht erreichbar, die Web-Oberfläche ruft sie direkt auf.

Die Idee

Ein Service Activator verbindet einen gewöhnlichen Dienst, also eine Methode oder Klasse ohne jede Messaging-Logik, mit einem Kanal. Er empfängt eine Nachricht, liest die Parameter heraus, ruft den Dienst auf und schickt das Ergebnis bei Bedarf als Antwortnachricht zurück. Der Dienst selbst bleibt unverändert und kann weiterhin auch direkt aufgerufen werden.

Beispiel

Das Lager hat eine Methode bestandPruefen(artikel), die von der Web-Oberfläche direkt aufgerufen wird. Damit auch der Shop sie per Messaging nutzen kann, wird ein Service Activator davorgesetzt. Er liest die Artikelnummer aus der Anfrage, ruft die Methode auf und schickt den Bestand als Antwort zurück. Am Lagerdienst ändert sich nichts.

Worauf achten

Der Service Activator ist das Gegenstück zum Messaging Gateway (Muster 47): Das Gateway macht aus einem Methodenaufruf eine Nachricht, auf der Seite des Senders. Der Service Activator macht aus einer Nachricht einen Methodenaufruf, auf der Seite des Empfängers. Mit beiden zusammen kann die Geschäftslogik auf beiden Seiten komplett frei von Messaging-Code bleiben. Fehler des Dienstes muss der Activator sinnvoll übersetzen, etwa in eine Fehlerantwort oder durch Verschieben in den Invalid Message Channel.

In der Praxis

@ServiceActivator in Spring Integration, bean() und to("bean:...") in Camel. Auch @JmsListener oder @KafkaListener an einer Methode, die nur einen fachlichen Dienst aufruft, ist im Kern ein Service Activator.

Die Endpunkte im Überblick

FrageMusterKurz gesagt
Wie bleibt die Geschäftslogik sauber?Messaging GatewaySenderseite: Methodenaufruf wird zur Nachricht
Service ActivatorEmpfängerseite: Nachricht wird zum Methodenaufruf
Messaging MapperObjekte werden zu Nachrichten und zurück
Wie bleibt alles konsistent?Transactional ClientEmpfangen, Verarbeiten und Senden als eine Einheit
Wie kommt die Nachricht zum Empfänger?Polling ConsumerEmpfänger holt ab, im eigenen Tempo
Event-Driven ConsumerSystem ruft den Empfänger auf, sofort
Wie wird die Last verteilt?Competing Consumersmehrere Empfänger am selben Kanal
Message Dispatcherein Empfänger verteilt intern
Wer bekommt was?Selective ConsumerEmpfänger wählt per Selektor aus
Wie übersteht man Ausfälle?Durable SubscriberAbonnent verpasst nichts, wenn er offline ist
Idempotent Receiverdoppelte Nachrichten richten keinen Schaden an