State data stores the current value of something — a row you overwrite. Event data stores an immutable record of every change, each timestamped. Model only state and you can describe today; model events and you can explain how you got here — and what's likely to happen next.

Quick answer: State tables show "what's true now" and are cheap to query, but they destroy history on every update. Event tables show "what happened and when," preserving the full sequence — the only place a churn story, an audit trail, or a trend line can actually live.

Every analytics question you'll ever get asked falls into one of two buckets: point-in-time ("how many customers are on the Pro plan right now?") or point-in-sequence ("why do customers downgrade before they cancel?"). Get the modeling fork right and both buckets are trivial queries. Get it wrong, and the second bucket becomes a permanent blind spot — no dashboard, no BI tool, and no clever SQL will conjure back a fact your schema never captured in the first place.

What's the Real Difference Between Event Data and State Data?

State data is a snapshot: one row per entity, updated in place, always reflecting the latest known value. Event data is a ledger: one row per occurrence, never updated, each entry timestamped and immutable. The difference isn't cosmetic — it determines whether your schema can ever answer "what changed, when, and why," or only "what's true right now."

A subscriptions table with a status column is state. A subscription_events table with rows like trial_started, upgraded, payment_failed, and canceled is event data. Same underlying reality, two different modeling decisions — and two very different sets of questions you'll be able to answer a year from now.

DimensionState DataEvent Data
MutabilityMutable — updated in placeImmutable — append-only
Storage shapeOne row per entityOne row per occurrence
Core question answeredWhat is true right now?What happened, and when?
Growth over timeRoughly flat, bounded by entity countGrows with activity, effectively unbounded
Typical columnsstatus, current_plan, updated_atevent_type, occurred_at, payload
Can reconstruct the past?No — the prior value is overwrittenYes — replay the log in order
Example table namesubscriptions, users, accountssubscription_events, user_activity, order_events

Neither is "correct" in isolation. A dashboard showing today's active subscriber count wants state — fast, small, indexable. A churn analysis wants events — slower to aggregate, but complete. The mistake is treating this as a technical implementation detail instead of a product decision made at modeling time, before a line of DDL is written.

If you're still deciding what belongs in your schema before you reach this fork, it sits downstream of two more foundational questions. Our data modeling complete guide covers the full sequence, and what counts as an entity explains how to draw the boundary before deciding how to store its history.

This fork usually gets discovered at the worst possible time: three sprints after launch, when someone in a growth review asks "how many people downgraded before they upgraded again?" and the honest answer is that nobody can know, because the plan column has been overwritten four times since. Deciding state-versus-event upfront, attribute by attribute, is far cheaper than retrofitting an event table onto a system that has already been quietly erasing its own history for a year.

Why a Subscription Table Alone Can't Tell You the Churn Story

A subscriptions table with status = 'canceled' tells you a customer left — nothing about how. It can't show that they downgraded twice, had a failed payment, paused for a month, and reactivated before finally leaving. An event log of the same account tells the whole story, in order, with timestamps.

Here's what nine months on one account looks like as events:

TimestampEvent
2025-01-03trial_started
2025-01-17subscribed (plan: Pro)
2025-03-02payment_failed
2025-03-04payment_retry_succeeded
2025-04-10downgraded (Pro → Basic)
2025-05-01paused
2025-05-29resumed
2025-06-15downgraded (Basic → Free trial tier)
2025-06-30canceled

Now check what the subscriptions table shows for that same account on July 1: status = canceled, plan = free, updated_at = 2025-06-30. One row. Nine events collapsed into a single, final fact. Every leading indicator — the failed payment, the double downgrade, the pause-then-resume cycle — is simply gone.

That collapse costs you exactly where it matters:

  • You can't ask "what percentage of cancellations were preceded by a failed payment in the last 90 days?" — a question that points straight at a billing fix, not a retention campaign.
  • You can't ask "how many customers who paused went on to resume, versus churn?" — the difference between price sensitivity and genuine disengagement.
  • You can't ask "how much lead time is there, on average, between a downgrade and a cancellation?" — the window a save campaign would need to work in.

Each of these is also a jobs-to-be-done question in disguise: a pause usually signals a change in the customer's context, and a downgrade usually signals the plan no longer matches the progress they hired your product to make. Our jobs-to-be-done complete guide walks through reading those push, pull, anxiety, and habit forces — but you can only read them if the underlying events exist to read.

Plot those same nine rows on a timeline and you already have the backbone of a customer journey map — the emotion curve moves through the events, never through a single state snapshot. See our customer journey complete guide for how to build that curve out properly.

The Rule of Thumb: Log Events Even When You Only Display State

The rule of thumb: if a value can change, log the change as an event — even when your product only ever displays the current state. Treat state as a computed view, folded from the event stream, not as the primary record. You can always derive today's snapshot from history; you can never derive history from a snapshot.

This isn't a new idea. Martin Fowler describes event sourcing as capturing every change to application state as a sequence of events, then deriving current state by replaying them in order — a pattern used for years in systems where the "why" matters as much as the "what." Martin Kleppmann makes a related case in Designing Data-Intensive Applications: treat the append-only log as the system of record, and let every queryable "current state" table be a derived, rebuildable cache of that log.

You don't need full event sourcing to get most of the benefit. A lighter, pragmatic version works for most product teams:

  1. Identify every attribute that can change after creation — status, plan, assignee, price, permission level.
  2. Write an event row alongside every state update, ideally in the same transaction, so the two can never quietly drift apart.
  3. Give every event a stable event_type, an occurred_at timestamp, and a payload describing what changed and, where possible, why.
  4. Let the state table be a view or a nightly rebuild off the event table wherever the query pattern allows it.
  5. Never delete from the event table. Append-only storage growth is the price of the option value — it's cheap next to the alternative.

Data warehousing solved a version of this problem long before "event sourcing" was a term anyone used. Ralph Kimball's slowly changing dimension (Type 2) pattern handles exactly this: instead of overwriting a dimension row, you insert a new row with effective and expiration dates, so a historical fact-table join always reflects the state that was true at the time of the transaction — not the state as of today. It's a hybrid: shaped like state, but temporally aware.

Product analytics vendors built entire businesses on the same principle. Tools like Segment and Snowplow exist to collect the event stream once, in a durable and replayable form, and let every downstream system — warehouse, CRM, growth tool — derive its own current-state view from it. Your own schema doesn't need a vendor to make the same choice; it just needs the discipline to write the event row every time.

Why You Can Never Reconstruct History You Didn't Log

You cannot reconstruct history you never recorded — you can only guess at it, and guesses look dangerously like data once they land in a chart. If your subscriptions table only ever stored the current status, no query will tell you how many customers paused before they canceled last quarter. That fact is simply gone.

This is the discipline Pat Helland pointed at in his writing on immutability: accounting systems never erase a transaction, because the historical fact has to remain queryable for audit purposes — they append a correcting entry instead of editing the original. Product event data deserves the same treatment whenever the "transaction" is a plan change, a permission grant, or a support escalation someone will eventually need to explain.

Once something has occurred, the fact of its occurrence should never be deleted — only ever appended to. That's the immutability principle in one line, and it's why event tables don't get UPDATE statements.

The same trap catches e-commerce teams. An orders table that stores status = 'delivered' can't show whether an order was ever flagged for fraud review, partially refunded, or re-routed after a failed delivery attempt — each of those was true for hours or days before the next status overwrote it. If a chargeback dispute lands eight months later asking for a timeline, a state-only schema has nothing to offer but the final word.

For teams retrofitting an already-live state-only table, change data capture (CDC) tools like Debezium or Fivetran can synthesize an event stream from the database's write-ahead log going forward. That gives you row-level INSERT/UPDATE/DELETE history from the moment you turn it on — not a single event from before you flipped the switch. CDC narrows the gap; it doesn't erase it.

Split your backlog of analytics questions into two buckets before you build anything, and the modeling choice becomes obvious:

QuestionAnswerable from state alone?Needs an event log?
How many active subscribers do we have today?YesNo
What's our current MRR?YesNo
What % of cancellations were preceded by a failed payment?NoYes
How long do customers stay paused before resuming or churning?NoYes
Which onboarding step correlates with faster activation?NoYes
Did this customer downgrade before they canceled?NoYes
What plan is this customer on right now?YesNo

Notice the pattern: state answers point-in-time questions about a single entity; anything about sequence, duration, or causality needs the log. Retroactively inferring "they probably paused around March based on the invoice gap" isn't analytics — it's fiction with a confidence interval attached, and no schema migration closes that gap after the fact.

The practical fix is unglamorous: instrument events before you think you'll need them. An unused event_type column costs almost nothing. An unanswerable question from your VP two years from now costs a scramble, a caveat-laden estimate, and a credibility hit you didn't need to take.

Designing State and Event Tables Side by Side

Most schemas need both a state table and an event table for the same entity: the state table for fast current-value lookups, the event table as the durable source of truth. Deciding which attributes get which treatment is a modeling decision, not an afterthought, and belongs in the same session where you draw entities and choose column types.

Work it in this order:

  • Draw the entity and its relationships first, before any storage decisions — see draw the ERD first for why the sequencing matters.
  • Decide, attribute by attribute, whether the value's history is a product requirement or a nice-to-have. Most status, plan, and assignment fields fail this test in favor of event logging once you ask the question directly.
  • Pick types deliberately once you know which table an attribute lives in: an event_type wants a constrained enum or lookup table, a payload often wants jsonb, and an occurred_at should never be nullable. Our guide to choosing column types covers these tradeoffs in depth.
  • Only then write the DDL for both tables, so the state table and the event table are designed as a pair, not two disconnected afterthoughts bolted on at different sprints.

This is the exact design conversation Prodinja's Data Modelling tool is built around: you can lay out a state table and an events table for the same entity side by side, and see, in the generated DDL, precisely what history each schema can and cannot reconstruct — before a single migration has run.

Key Takeaways

  • State data answers "what is true now"; event data answers "what happened and when" — most products need both, chosen deliberately per attribute, not by default.
  • If a value can change, log the change as an event even when your UI only ever displays the current state.
  • Treat state as a computed fold over the event stream, not as the primary record — Fowler's event sourcing pattern and Kleppmann's log-as-source-of-truth argument both rest on this idea.
  • You cannot reconstruct history you didn't log; retroactive imputation is guesswork dressed as data, not analytics.
  • Kimball's slowly changing dimensions (Type 2) and event sourcing solve the same underlying problem from opposite ends of the industry — you don't have to invent this from scratch.
  • A subscription's churn story lives in the sequence of pauses, downgrades, and payment failures leading up to cancellation — never in the final status column alone.
  • Model state and event tables for the same entity side by side so you can see, before you ship, exactly which questions each one will and won't be able to answer later.

Frequently Asked Questions

Should I store both state and event data for the same entity?

Yes — in most production systems, the state table and the event table serve different, non-overlapping jobs. The state table gives you a fast, small, indexable answer to "what is true now"; the event table gives you the durable source of truth you can replay, audit, or aggregate later. Building only one leaves a gap you'll eventually need to fill, usually at the worst possible time.

What's the difference between event sourcing and just logging events?

Event sourcing means the event log is the primary record, and every state table is a derived, rebuildable projection of it. Simply logging events alongside a conventional state table is a lighter pattern: you still update state directly, but you also append an event row so the history isn't lost. Most product teams get most of the practical benefit from the lighter version without adopting full event sourcing architecture.

Does storing event data cost too much?

Storage is rarely the real cost — event tables are cheap to append to and easy to partition by time, and cloud storage pricing has fallen faster than most product decisions account for. The real cost is discipline: writing the event row consistently, in the same transaction as the state update, so the two never drift out of sync with each other.

Can I add event logging later, after I've already shipped with state only?

Yes, starting from the moment you add it — but you cannot backfill events for anything that happened before you began logging. Whatever history existed under the state-only model is gone: canceled subscriptions, closed tickets, or completed orders that changed status before your event table existed have no recoverable sequence, only their final value.