Integration patterns
Once a call leaves the process, everything changes: it can be slow, it can be lost, it can arrive twice, and it can succeed while the caller believes it failed. Integration patterns are the vocabulary for dealing with that, and they are only comprehensible against the failures they exist to absorb.
Start with the fallacies
Section titled “Start with the fallacies”The eight fallacies of distributed computing are the reason this catalogue exists. The network is not reliable, latency is not zero, bandwidth is not infinite, the network is not secure, topology does change, there is not one administrator, transport cost is not zero, and the network is not homogeneous.
Every pattern below is a response to one of those.
Message construction and channels
Section titled “Message construction and channels”Document message, command message, event message — three different intents, and confusing them is how a system ends up with events that are secretly commands, coupling the publisher to a consumer it should not know about.
Point-to-point vs publish-subscribe — whether one consumer handles a message or all of them do. A decision that is very hard to change once consumers exist.
Dead letter channel — where a message goes when it cannot be processed. Without one, failure is silent, and silent failure in a queue is the worst failure mode in this catalogue.
Routing and transformation
Section titled “Routing and transformation”Content-based router, splitter, aggregator, scatter-gather — the routing vocabulary. Each has a state and timeout problem that is easy to miss when drawn on a whiteboard.
Message translator and canonical data model — the transformation vocabulary. A canonical model shared by every system is a coupling point disguised as a decoupling one; per-boundary translation is usually the safer choice.
Reliability
Section titled “Reliability”Idempotent receiver. The single most important pattern here. At-least-once delivery is what you get in practice, so every consumer must tolerate seeing the same message twice.
Transactional outbox. Writing to the database and publishing a message are not one atomic act. The outbox makes them one, and its absence is the most common source of “the data says one thing and the downstream system says another”.
Saga. A business transaction across services, with a compensating action per step because there is no distributed rollback. Expensive and sometimes unavoidable — the cost is that every step needs a defined way to be undone.
Circuit breaker, retry with backoff, bulkhead, timeout. The resilience set. A retry without a timeout and a backoff is an amplifier for an outage, not a mitigation.
Worked example — the two that are always needed
Section titled “Worked example — the two that are always needed”Idempotent receiver. At-least-once delivery is what the broker actually gives you, so the consumer decides whether a message has been seen — and it decides it in the same transaction as the work, or the decision is a race:
@Transactionalpublic void on(PaymentTaken event) { if (!processedMessages.claim(event.messageId())) // INSERT ... ON CONFLICT DO NOTHING return; // a duplicate: already handled
Order order = orders.withId(event.orderId()).orElseThrow(); order.markPaid(event.paidAt()); orders.save(order);}Checking processedMessages first and inserting afterwards is the version
everyone writes and it is wrong: two deliveries in flight both read “not seen”
and both charge the customer. The claim has to be the atomic act.
Transactional outbox. Writing to the database and publishing a message are two systems, so they cannot both succeed:
@Transactionalpublic void ship(OrderId id, TrackingNumber tracking) { Order order = orders.withId(id).orElseThrow(); order.ship(tracking); orders.save(order); outbox.append(order.releaseEvents()); // same transaction, same database}A separate relay reads the outbox and publishes, marking rows as sent. It will occasionally publish twice — a crash between publish and mark — which is exactly why the receiver above is idempotent. The two patterns are one design: the sender promises at-least-once, the receiver absorbs it. Systems that implement only one of the two fail in the field rather than in the test suite.
Note what the outbox holds: the domain events the aggregate raised, which is why raising them inside the aggregate rather than in the service layer matters — an event emitted next to the save can be emitted without the save.
Where the contracts live
Section titled “Where the contracts live”Every one of these boundaries is a contract. Message schemas belong in the event catalogue; synchronous interfaces belong in the API catalogue. An integration whose message shape exists only in code is an integration nobody can change safely.
Status
Section titled “Status”Outline, with the reliability pair worked through because they are the two no message-driven system gets to skip. The per-pattern pages, each with a rendering per stack, are not written yet.