Event sourcing and activity schema
What does keeping events as the record buy, and what do event sourcing and activity schema each pay?
Event modeling
Event sourcing
Event sourcing makes the sequence of state changes the system of record. Current state is a fold over that sequence. (A fold means: start from an empty state and apply each event in order, producing the new state.)
events = [("deposit", 100), ("withdraw", 30), ("deposit", 50)]
balance = 0
for kind, amount in events:
balance += amount if kind == "deposit" else -amount
# balance == 120Event sourcing is the maximal position on two axes at once: compute-on-read and late binding, because you have committed to no question in advance. A projection (a derived view) that you did not anticipate can be built retroactively against the full history. That is the one thing no dimensional model can do. It is also what the product team needs for "customers who returned within 7 days".
The costs are specific:
- Replay time grows with history, so snapshots appear, with a retention policy and a rebuild path. This is the immutable-store law again: append-only substrate, periodic checkpoint, collector. It has the same shape as a write-ahead log plus checkpointing in a database.
- Event schemas become permanently load-bearing. You cannot change what a past event meant. You can only add new event types and translate on read. This makes event versioning strictly harder than table versioning, because there is no migration option, only interpretation.
- The fold is business logic, so "current state" is per projection, and two projections may legitimately disagree. Upstream that is correct. Downstream it means no canonical current state exists unless someone declares one, and the declaration is an organizational act.
- Corrections are compensating events, never edits. A wrong deposit of 100 is fixed by a new event "reverse deposit 100", not by changing the old one. So every downstream aggregate must handle retractions (events that cancel earlier events). A system that cannot express a retraction cannot consume an event-sourced system honestly.
Activity schema
Activity schema takes a narrower position: one table, one grain: one row per activity occurrence per entity, with a small payload. All analysis is expressed as temporal relationships between activities: first ever, last before, next after.
| ts | customer | activity | payload |
|---|---|---|---|
| Mar 1 | C1 | placed_order | order=A-1 |
| Mar 4 | C1 | support_ticket | topic=late |
| Mar 6 | C1 | placed_order | order=A-9 |
-- Customers whose second order came within 7 days of their first
SELECT customer
FROM (
SELECT customer, ts,
ROW_NUMBER() OVER (PARTITION BY customer ORDER BY ts) AS n,
LAG(ts) OVER (PARTITION BY customer ORDER BY ts) AS prev_ts
FROM activity WHERE activity = 'placed_order'
) t
WHERE n = 2 AND ts - prev_ts <= INTERVAL '7' DAY;The derivation is interesting: it dissolves the conformed-dimension problem by having only one dimension and one grain. Cross-process questions become temporal self-joins instead of negotiated agreements (conformed dimensions). New activities are added without schema change or conformance work.
What it pays:
- Every question becomes a temporal self-join, whose cost depends entirely on whether the engine supports as-of and interval joins well.
- Expressiveness is bounded by what can be stated as a temporal relationship between two activities. That excludes a large class of set and allocation questions (like the bridge-table weights).
It is a real trade, not a strict improvement, and its limited adoption is data. Treat it as evidence for the "single grain plus temporal joins" position, not as a product to evaluate.
Both push meaning into the query
Both event-modeled approaches push meaning into the query. That is why a semantic layer stops being optional in an event-modeled stack: the fold has to live somewhere shared, or the "unstated meaning is re-derived" law runs at full strength.