The problem
With synchronous calls, the Publish service has to know about and call Search, CDN, Webhooks, Audit and Analytics. Every new consumer changes the producer, and if any one of them is down, publishing fails.
How it works
The producer publishes a fact in the past tense. It doesn't know who's listening, and adding a consumer needs no change to the producer.
The four flavours (Fowler)
| Flavour | What's in the event | Watch out for |
|---|---|---|
| Event notification | Just IDs ("entry 42 changed") | Consumers call back for data → hidden coupling and load |
| Event-carried state transfer | The full new state | Bigger payloads; consumers keep local copies |
| Event sourcing | Events are the source of truth | See Event Sourcing |
| CQRS | Separate write and read models fed by events | See CQRS |
Design checklist
- Schema and versioning: add fields, never repurpose them. Use a schema registry for Kafka.
- Delivery is at-least-once. Consumers must be idempotent.
- Ordering: only guaranteed per partition or key (e.g. key by
entryId). - Reliable publish: use the Transactional Outbox to avoid dual writes.
- Dead-letter queues and replay tooling for poison messages.
- Correlation IDs in every event so you can trace a flow end to end.
- Many consumers react to the same change
- Consumers can lag slightly (search, analytics, notifications)
- You need to absorb traffic spikes by buffering
- The caller needs the result right now to continue
- The team has no tracing or DLQ tooling yet
- A strict global order of operations is required
Trade-offs
- ✅ Loose coupling, independent scaling, natural audit trail, resilience to consumer outages.
- ❌ Eventual consistency: the UI may show stale data briefly.
- ❌ Harder to reason about. "What happens when X is published?" is spread across the code.
- ❌ Duplicates, out-of-order events and poison messages have to be designed for.
In a headless CMS, publishing is inherently event-driven. A publish or release deploy fans out to delivery, CDN and webhooks. The design work was mostly about failure semantics: per-item status, skipping invalid items instead of failing the whole batch, and making redelivery safe.
Cheatsheet
The whole topic on one page. Click to open full screen.
Sources & further learning
Videos, courses, docs and books I recommend for this topic.
Related topics
CQRS (Command Query Responsibility Segregation)
Use one model to change data (commands) and a different, optimised model to read it (queries) — often kept in sync by events.
Event Sourcing
Store every state change as an immutable event; current state is derived by replaying events. The log is the source of truth.
Transactional Outbox
Write the business change and the outgoing event in the same local transaction, then relay the event to the broker — no more "saved to DB but message lost".
Message Queues vs Event Streams
RabbitMQ/SQS-style queues distribute work; Kafka-style logs retain ordered events for many consumers and replay. Know which one your problem needs.
Saga Pattern
Keep data consistent across services without distributed transactions — a sequence of local transactions, each with a compensating action if a later step fails.