Event-Driven Architecture
An event is a record that something happened: customer 4471 placed order 8812 at 14:02:17, a thermostat read 21.5 degrees, a driver accepted a trip. The record names the thing, when it happened and what changed, and it never changes afterwards, because the past does not change. A system built on events writes each record to a shared log once, and every other system that cares reads the log at its own pace.
Request-driven versus event-driven
In a request-driven design, the checkout service finishes an order by calling the inventory service, then the email service, then the analytics service, and waits for each reply. Adding a fraud check means changing checkout again. When the email service is down, the order either fails or checkout grows a retry branch for that one call. In an event-driven design, checkout writes one order_placed event to the log and returns. Inventory, email, analytics and fraud each subscribe to the log and react when they are ready. Checkout holds no list of who reads its events, so a new consumer costs the producer nothing.
- Checkout service (source, producer)
- Order event log (queue, append-only topic)
- Inventory service (consumer, reserves stock)
- Email service (consumer, sends receipt)
- Analytics loader (transform, hourly batch)
- Warehouse (storage, order_events table)
- Checkout service -> Order event log
- Order event log -> Inventory service
- Order event log -> Email service
- Order event log -> Analytics loader
- Analytics loader -> Warehouse
- Request-driven
- Producer calls each consumer and waits for a reply
- Producer code changes when a consumer is added
- A down consumer blocks or fails the request
- History lives in server logs nobody queries
- Event-driven
- Producer writes one event and returns
- Consumers are added without touching the producer
- A down consumer catches up from its last offset
- The log is a queryable record of everything that happened
The log sits between producer and consumers, which is why these systems are called decoupled: the 2 sides share a contract about the event, and nothing else.
The shape of an event
Every event carries an envelope, the fields that are the same for every event type, and a payload, the fields specific to this type. The envelope is what routing, deduplication and loading rely on, so it is strict. The payload is what the business reads, so it changes as the business does, and a schema_version in the envelope records which shape the payload has.
| Field | Part | Example | Why it is there |
|---|---|---|---|
| event_id | envelope | evt_01HX4K9Q7M | Dedupe retries, trace one event end to end |
| event_type | envelope | order_placed | Route to the consumers that care |
| event_time | envelope | 2026-10-20T14:02:17Z | When it happened, stamped by the producer |
| entity_id | envelope | order_8812 | Group every event about one thing |
| producer | envelope | checkout-v12 | Trace a malformed event to its source |
| schema_version | envelope | 3 | Read old payloads with the right code |
| payload | payload | items, total, currency | The facts specific to this type |
{
"event_id": "evt_01HX4K9Q7M",
"event_type": "order_placed",
"event_time": "2026-10-20T14:02:17Z",
"entity_id": "order_8812",
"producer": "checkout-v12",
"schema_version": 3,
"payload": {
"customer_id": "cust_4471",
"items": [
{"sku": "SKU-220", "qty": 2, "unit_price": 19.99}
],
"total": 39.98,
"currency": "EUR"
}
}Querying an event table
Once the loader lands events in the warehouse, the table has one row per event, and most questions become a count grouped by type and time. This query shows what happened in each hour of one day.
SELECT
date_trunc('hour', event_time) AS event_hour,
event_type,
count(*) AS events
FROM order_events
WHERE event_time >= TIMESTAMP '2026-10-20 00:00:00'
AND event_time < TIMESTAMP '2026-10-21 00:00:00'
GROUP BY
date_trunc('hour', event_time),
event_type
ORDER BY
event_hour,
event_typeA payment service must tell inventory, email, fraud and analytics that a refund went through. Which record belongs in the shared log?
- A refund_pending event with refund_id and a note that the amount will follow in a later message
- An update_order_status event with order_id and new_status set to refunded
- A refund_succeeded event with refund_id, order_id, amount, currency and event_time
- A direct write that sets orders.refunded to true in the orders table
About this lesson
This section is part of the Event Streams lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.