Data Engineering  /  Data Modeling

๐Ÿงฑ Data Modeling 20 guides ยท updated 2026

From ER diagrams to data mesh โ€” relational, dimensional, NoSQL, and governance-driven modeling for building data platforms people can trust.

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.

Order service

transaction log

domain events

(OrderPlaced, OrderShipped)

Orders DB

CDC (Debezium)

Kafka: business facts

Kafka: row changes

Warehouse / lake:

events land as fact tables

Other services

(fulfillment, fraud)

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

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.