Description
What is event-driven architecture?
Event-driven architecture (EDA) is a way of building software in which services communicate by publishing events (immutable facts such as "message received", "status delivered" or "order placed") to a broker, and any number of other services subscribe and react in their own time. The producer does not know who consumes its events or wait for them to finish, which is the opposite of a request-driven design where service A calls B, which calls C, and the whole chain fails if any link is slow.
WhatsApp platforms are a natural fit because the platform itself is event-driven: Meta delivers every inbound message, delivery status and template update as a webhook. A request-driven receiver would verify the payload, then synchronously update the CRM, call the bot, notify the inbox and record analytics before answering. In an event-driven design the receiver verifies the signature, publishes one event per message or status, returns 200 and is done; the inbox, bot, CRM sync and analytics each consume the events independently.
Several patterns live under the EDA umbrella. Event notification sends a small fact and lets consumers fetch details; event-carried state transfer puts everything consumers need in the event; event sourcing stores the events themselves as the system of record. Workflows can be choreographed (each service reacts to events) or orchestrated (one coordinator issues commands). Most WhatsApp backends mix them: choreography for fan-out of inbound events, orchestration for multi-step journeys such as order-to-delivery notifications.
EDA is not the same as a message queue. A queue is a building block that hands each job to one worker; EDA is the architectural decision to let events drive the system, usually built from topics, subscriptions and queues together. Webhook fan-out is one concrete use of it, splitting Meta's single webhook stream to many internal consumers.
Key takeaways
- Event-driven architecture (EDA) connects services through published events rather than direct calls, so producers never wait on consumers.
- A WhatsApp integration is event-driven at its edge already: every inbound message and status arrives as a webhook event.
- Each consumer gets its own subscription, retries and dead-letter queue, so a failing CRM sync cannot block the inbox or the bot.
- Brokers and Meta's webhooks deliver at least once and not always in order: consumers must be idempotent and partition by conversation.
- A message queue is one building block of EDA; the architecture is the decision to let events, not call chains, drive the system.
Why it matters
Why does event-driven architecture matter?
Request chains turn every dependency's bad day into your outage: if the CRM takes eight seconds, the webhook handler takes eight seconds, Meta retries, and duplicates pile up. Events decouple the timing. The receiver acknowledges in milliseconds, slow consumers fall behind without slowing anyone else, new consumers (a fraud check, a data warehouse feed) subscribe without touching existing code, and each consumer can apply horizontal scaling independently. The price is new failure modes, such as duplicates, reordering and eventual consistency, that must be designed for up front.
Mechanics
How does event-driven architecture work?
step 1 — Name events as facts with a standard envelope
Use past-tense types such as
whatsapp.message.received,whatsapp.status.deliveredandorder.placed, wrapped in a consistent envelope (CloudEvents 1.0 is a common choice:id,source,type,time,data).step 2 — Publish once, at the edge
The webhook receiver verifies
X-Hub-Signature-256, publishes one event per message or status, and returns 200 only after the broker confirms. If publishing fails, return an error so Meta retries. For events born in your database (an order confirmed), use a transactional outbox.step 3 — Give every consumer its own subscription
The inbox, bot, CRM sync and analytics each read from their own subscription or consumer group, filtered to the event types they care about, with their own retry policy and dead-letter queue.
step 4 — Design consumers for duplicates and disorder
Brokers and Meta's webhooks offer at-least-once delivery, so use the event
id(a wamid, or wamid plus status) as an idempotency key. Webhook event ordering is not guaranteed either: partition by the customer'swa_idand never move a status backwards whenreadarrives beforedelivered.step 5 — Observe the flow, not just the services
Carry trace context in event headers, and watch consumer lag per subscription: a growing backlog on one consumer is the early sign of trouble that a request-driven system would have shown as timeouts.
Usage
Event-driven architecture in code
// Edge: verify, turn each message/status into one event, publish, then acknowledge.app.post("/webhooks/whatsapp", verifySignature, async (req, res) => {const events = [];for (const entry of req.body.entry ?? []) {for (const change of entry.changes ?? []) {const v = change.value;const pnid = v.metadata?.phone_number_id;for (const m of v.messages ?? [])events.push(toEvent("whatsapp.message.received", m.id, pnid, m.from, m));for (const s of v.statuses ?? [])events.push(toEvent(`whatsapp.status.${s.status}`, `${s.id}:${s.status}`, pnid, s.recipient_id, s));}}try {// broker = your client for Kafka, RabbitMQ, a cloud pub/sub service...await broker.publishBatch("whatsapp-events", events, { partitionKey: (e) => e.subject });res.sendStatus(200);} catch {res.sendStatus(503); // not published: let Meta retry rather than lose the event}});function toEvent(type, id, phoneNumberId, waId, data) {return {specversion: "1.0",id, // dedupe key for every consumertype,source: `/whatsapp/${phoneNumberId}`,subject: waId, // partition key: keeps one conversation in ordertime: new Date().toISOString(),datacontenttype: "application/json",data,};}// Consumer: its own subscription, its own retries, idempotent.broker.subscribe("whatsapp-events", { group: "inbox", types: ["whatsapp.message.received"] }, async (event) => {if (await processed.has("inbox", event.id)) return; // at-least-once: skip repeatsawait inbox.append(event.subject, event.data);await processed.add("inbox", event.id);});
The receiver only verifies, publishes and acknowledges; everything else happens in consumers that can fail and retry on their own.
Properties
Event-driven architecture: key facts and limits
- Core idea
- Producers publish facts; consumers subscribe and react
- Common envelope
- CloudEvents 1.0: id, source, specversion, type required
- Meta webhook delivery
- At least once, retried for up to 7 days
- Meta webhook order
- Not guaranteed, so design for reordering
- Webhook payload size
- Up to 3 MB per notification
- Per-conversation order
- Partition events by the customer's wa_id
Caveats
Common mistakes with event-driven architecture
Acknowledging before the event is safe
Returning 200 to Meta and then failing to publish loses the message for good, because Meta will not retry a delivery you acknowledged. Publish first (or write to a durable outbox), then acknowledge.
Assuming exactly-once and in-order delivery
Brokers and Meta deliver at least once, and statuses can arrive out of order. Make every consumer idempotent by event ID, partition by conversation, and make status updates monotonic.
Events that are really remote procedure calls
An event named
sendWelcomeMessageNowthat only one service can handle is a command in disguise and couples the services again. Publish what happened (customer.opted_in) and let consumers decide what to do.Changing event schemas without versioning
Renaming a field breaks every consumer you forgot about. Add fields rather than change them, version event types (
v2) when you must break compatibility, and keep a schema registry or a documented contract.
Example
Example: One webhook, four independent reactions
A Chennai electronics retailer receives a customer reply, "Is my order shipped?". The webhook receiver publishes whatsapp.message.received keyed by the customer's wa_id and answers Meta in 30 ms. The bot service classifies the intent and publishes order.status.requested; the orders service answers with an event the bot turns into a reply. Meanwhile the inbox shows the thread to agents, and analytics counts the intent. When the CRM sync goes down for an hour that afternoon, its subscription simply builds a backlog and catches up later; customers never notice, and the webhook endpoint never returns an error.
Questions
EDA FAQs
- Is a webhook an example of event-driven architecture?
- A webhook is an event notification over HTTP, so it is event-driven at the boundary. Whether your system is event-driven depends on what happens next: publishing the webhook as events for independent consumers is EDA, while a handler that calls five services in a row is a request chain.
- What is the difference between event-driven architecture and a message queue?
- A message queue is a component; event-driven architecture is a design style. Queues usually hand each job to one worker, while EDA typically uses topics or exchanges so one event reaches many independent subscribers, each with its own queue.
- Do I need Kafka for event-driven architecture?
- No. Any broker with durable topics or subscriptions works, including RabbitMQ, a cloud pub/sub service, Redis Streams or a database outbox for small systems. Choose based on volume, ordering needs, retention and what your team can operate.
- How do I keep WhatsApp events in order in an event-driven system?
- Partition by conversation and make consumers tolerant of disorder. Use the customer's
wa_idas the partition key so one conversation is processed in sequence, and compare timestamps or status rank so a latedeliverednever overwritesread. - When is event-driven architecture overkill?
- When one service does all the work and a single queue would do. A small bot with one consumer gains little from topics and schemas; adopt EDA when several teams or services need to react to the same events.
Notes
Best practices for event-driven architecture
- Keep the webhook receiver boring: verify, publish, acknowledge. Put business logic in consumers.
- Give each consumer its own dead-letter queue so one poison event does not stall the others.
- Store raw webhook payloads for a few days; replaying them into a new consumer is the easiest backfill there is.
- Start with a few well-named event types; splitting a vague event later is easier than merging dozens.