Event-Driven Data Modeling: Designing Events, Streams, and State That Rebuilds Itself
Traditional data modeling asks: what is the current state of things? โ and stores a row per customer, per order, per account balance. Event-driven modeling asks a different question: what happened? โ and stores the history of occurrences, from which any state can be derived. Itโs the difference between a bank storing your balance and a bank storing every transaction; the second can always produce the first, but not vice versa. (Real banks, notably, chose events centuries before software existed.)
As architectures shift toward streams โ Kafka topics between services, change-data-capture feeding warehouses, event sourcing inside services โ the event itself becomes a modeled artifact, with design decisions as consequential as any table schema. This post covers how to design events well: naming, granularity, fact-vs-command discipline, the event sourcing and CDC patterns, and the mistakes that show up in every first attempt.
What Makes a Good Event
An event is a record of a fact that occurred โ immutable, past-tense, timestamped. Three design rules follow directly from that definition:
1. Name events as past-tense business facts. OrderPlaced, PaymentCaptured, SubscriptionCancelled. If the name is an imperative โ CreateOrder, ChargeCustomer โ youโve modeled a command (a request that may be refused), not an event (a fact that cannot be). The distinction matters downstream: consumers can always trust an event happened; treating commands as facts is how analytics ends up counting orders that were rejected.
2. Make the event self-contained enough to be useful. The perennial tension: a notification event (OrderPlaced {order_id}) forces every consumer to call back for details โ recreating tight coupling and thundering-herd lookups; a fat event carrying the entire order document bloats topics and leaks internal structure. The working compromise: carry the data most consumers need to act (IDs, amounts, statuses, key foreign identities), link for the rest. When in doubt, ask each intended consumer what theyโd have to look up โ repeated answers belong in the payload.
3. Give every event an envelope. Discipline about metadata pays compound interest:
{ "event_id": "01J9ZK7Q...", // unique โ enables deduplication "event_type": "OrderPlaced", "schema_version": 3, "occurred_at": "2026-07-06T09:14:11Z", // business time "recorded_at": "2026-07-06T09:14:12Z", // system time "correlation_id": "req_8841", // traces a flow across services "payload": { "order_id": "ord_9812", "customer_id": "cus_311", "amount": 300.00, "currency": "USD" }}Note the two timestamps: when it happened vs. when we learned of it differ under retries, offline devices, and backfills โ analytics needs the first, debugging needs both.
Granularity: The Grain Question, Again
Just as fact tables have grain, streams have granularity, and the same discipline applies. Should a checkout emit one OrderPlaced or a burst of ItemAdded ร n + CheckoutStarted + PaymentSubmitted? Work backwards from consumers: fraud detection wants the fine-grained behavioral trail; fulfillment wants one actionable order. Mature designs often layer both โ fine-grained domain events internally, with a coarser summary event published as the integration contract. What kills you is mixing grains in one topic, where consumers canโt tell a micro-step from a completed business fact โ the streaming version of the mixed-grain fact table from the analytical modeling post.
Event Sourcing: When Events Are the Database
Most event-driven systems emit events about state stored elsewhere. Event sourcing goes further: the event log is the system of record, and current state is a computation over it. An account is not a row โ itโs the fold of AccountOpened, FundsDeposited ร12, FundsWithdrawn ร7.
What you gain: a perfect audit trail by construction (regulators love this โ finance and healthcare are event sourcingโs natural habitats); time travel (โwhat did this account look like on March 3?โ); and the ability to build new read models from history โ a query pattern you didnโt anticipate can be served by replaying five years of events into a new shape, something no state-based system can offer.
What it costs: real complexity. Replays get slow without snapshots (periodic materialized state + only the events since). Queries need projections โ precomputed read models per query pattern, usually paired as CQRS (commands append events; queries hit projections). And the hardest part: events live forever, so โwhat did FundsDeposited v1 mean?โ must stay answerable for a decade โ schema evolution (previous post in this series) stops being an operational concern and becomes an archival one. Upcasters โ code that translates old event versions on read โ become permanent residents of your codebase.
The honest guidance: event-source the subdomains where history is the business (ledgers, claims, compliance-heavy workflows); use ordinary state + emitted events elsewhere.
CDC: The Pattern Feeding Every Modern Warehouse
The most widespread event-driven modeling pattern isnโt in application architecture at all โ itโs change data capture: tailing a databaseโs transaction log and publishing every insert/update/delete as an event (Debezium is the standard tooling). This is how operational databases feed warehouses continuously without batch extracts.
The modeling catch: CDC events are row changes, not business facts. orders.status: 2 โ 4 is what the database did; OrderShipped is what the business meant. Landing raw CDC and reconstructing business meaning in transformation (the ELT pattern) works and is common โ but know that youโre reverse-engineering intent from state diffs, and some intent (the why behind a delete) is simply not in the log. Systems that emit deliberate domain events alongside state changes give their analytics a semantic head start.
Events Meet Analytics: A Naturally Happy Marriage
Hereโs a satisfying convergence: an event stream is a fact table arriving in real time. OrderPlaced events land as rows in fct_orders; the eventโs identities (customer, product) join to dimensions; the immutability of events matches the append-only nature of facts. Two impedance mismatches to manage: late-arriving events (window your aggregations on occurred_at, expect stragglers โ same discipline as the time-series post) and duplicates (most streams guarantee at-least-once delivery; that event_id in the envelope is what makes deduplication in the warehouse a QUALIFY row_number() = 1 instead of a forensic project).
The Classic Mistakes
- Commands dressed as events โ
SendEmailon a topic is a job queue pretending to be a fact log. - Entity-shaped events โ publishing the whole current
Orderobject on every change (โevent-carried state transferโ has its uses, but as your only stream it discards the why of each change โ youโve built CDC by hand, minus the tooling). - No envelope discipline โ six months in, nobody can deduplicate, trace, or version anything.
- One mega-topic โ
all-eventswith forty types defeats consumer selectivity, schema governance, and retention tuning in one stroke. - Assuming exactly-once and global order. Design consumers idempotent, assume per-key ordering only (per partition), and life gets dramatically simpler.
Where to Start
Model your next integration as events with the checklist above: past-tense fact names, consumer-informed payloads, versioned envelopes, one grain per topic, registry-enforced schemas. Land them in the warehouse as fact tables and feel how little transformation they need โ thatโs the sign the model matches the meaning. Event-driven modeling is ultimately this seriesโ oldest advice โ store facts, not conclusions โ pushed to its logical end: the facts become the interface, the state becomes a cache, and history stops being something you lose.