Skip to main content

The Shift from CRUD to Events

What This Concept Is

A CRUD system stores current state and exposes Create, Read, Update, Delete. An event-driven system stores (or publishes) the history of changes -- the events -- and treats the current state as a projection of that history.

The pivotal move is this:

The database row is not the truth. The row is the latest answer produced by the sequence of facts.

Once you internalize that, several things stop being magic:

  • audit trail is free; it was the log all along
  • UPDATE users SET email = ? quietly erases the fact EmailAddressChanged, which many downstream systems actually wanted
  • time travel ("what did this customer look like on March 1?") is a query over history, not a debate with the DBA
  • multiple consumers can materialize their own views of the same history instead of synchronously reading the same row

You do not have to go full event sourcing (Concept 13) to benefit. Even without rewriting your database, publishing events on every state change lets other services react without polling your tables.

Why It Matters Here

Event-driven architecture is, at root, a bet that change is a first-class thing worth naming. CRUD treats change as invisible: the new value overwrites the old, nobody is told, and if anyone else needs to know they have to poll, subscribe via CDC, or be told out-of-band.

Once you make change visible -- by emitting an event for every meaningful transition -- the rest of this module falls into place:

  • pub-sub (Cluster 2) has something meaningful to publish
  • brokers (Cluster 3) have something worth retaining
  • workflows (Cluster 4) can be driven by event progression instead of by a request graph
  • sourcing and CQRS (Cluster 5) become available design choices

Concrete Example

CRUD view

users table
+---------+----------+----------+-------------------+
| user_id | name | email | updated_at |
+---------+----------+----------+-------------------+
| u_123 | Priya K | p@b.com | 2026-04-22 14:02 |

A week later:

| u_123   | Priya K  | p@c.com  | 2026-04-29 09:17  |

Questions the CRUD system cannot answer:

  • what was the email on 2026-04-23?
  • why did it change?
  • who needs to be told that it changed?
  • did it change once, or did someone change it to p@x.com and then back?

Event-driven view

events stream for user u_123
+-------------------+-------------------+-------------------------+
| occurred_at | event_type | payload |
+-------------------+-------------------+-------------------------+
| 2026-04-22 14:02 | UserRegistered | {name, email: p@b.com} |
| 2026-04-26 10:44 | EmailChangeReq. | {new_email: p@x.com} |
| 2026-04-26 10:47 | EmailChangeReq. | {new_email: p@c.com} |
| 2026-04-29 09:17 | EmailAddressCh. | {new_email: p@c.com} |

Now:

  • the current email is the last EmailAddressChanged: p@c.com
  • on 2026-04-23 it was p@b.com -- read forward until that date
  • billing service subscribes to EmailAddressChanged and updates its own mailing list without polling your DB
  • the analytics team can detect the "user tried p@x.com but landed on p@c.com" pattern

The CRUD row is still fine as a view. It is no longer the truth.

What This Is Not

This concept is not "use events instead of a database." Most systems keep a database -- the event publication is additional, and (if you use the outbox pattern, Concept 06) it is published atomically with the DB write. This concept is also not "event sourcing." Event sourcing (Concept 13) is the strong form: the log is the only source of truth and the DB, if any, is derived. You can adopt the mental shift (events are first-class) long before you adopt the strong form.

Common Confusion / Misconception

"CRUD is wrong." CRUD is fine for simple masters-and-details data. The shift matters where change is interesting -- domain events like orders, payments, shipments, support cases, policy updates. A lookup table of country codes does not need an event.

"We emit an event after every UPDATE, so we are event-driven now." Emitting an event after the fact leaves the dual-write bug (Concept 06). Also, not every UPDATE is a domain event: bumping updated_at is not EmailAddressChanged. Name events by the domain transition, not by SQL.

"Change data capture (CDC) is the same thing." CDC publishes row-level changes directly from the database log. It is useful, but it leaks your schema to consumers and names events by tables and columns (users.email updated), not by domain (EmailAddressChanged). CDC is a tool; the shift in this concept is semantic.

"We will just keep history in an audit table." You can, and in many shops that is the starting point. The limitation: audit tables are read-only historical records used by humans. Events are consumed by other systems and drive their behavior. Different use, different shape.

How To Use It

When you add or redesign a service, apply this exercise before any schema work:

  1. List the domain transitions (state changes) that are interesting to other services or people.
  2. Name each in past tense.
  3. For each, ask: if my DB did not exist and someone could only read this stream of events, could they reconstruct what they need?
  4. Decide per transition: event-only, DB-only, or both (via outbox).

Check Yourself

  1. Why is "the current row" a derived view and not the source of truth in event-driven thinking?
  2. Name three questions an event log answers that a CRUD table alone cannot.
  3. When is CRUD still the right default -- and why?

Mini Drill or Application

Pick a boring CRUD operation in a system you know (editing a user profile, reassigning a ticket, updating a product price). In 20 minutes:

  1. Write the SQL that would execute today (the UPDATE statement).
  2. Write the domain event(s) that should accompany that update. Often the right answer is 1; sometimes there are 2 or 3 distinct transitions hiding in one UPDATE.
  3. For each event, list two services or analyses that would benefit from consuming it.

Transfer to Adjacent Domains

  • Event sourcing (Concept 13). The strong form of this shift: the log is the only source of truth, and every DB is derived. Adopt the mental model now even if you never adopt the strong form; the shift is linguistic before it is architectural.
  • Outbox pattern (Concept 06). The specific technique that lets you publish events atomically with DB writes -- it is how the CRUD-to-events shift becomes reliable without giving up your relational store.
  • Data platform / lakehouse (S6). A domain-event stream is already the correct input for a lakehouse. Teams that still pipe raw CDC rows into Snowflake/Databricks and re-derive domain meaning downstream are paying the CRUD-shaped tax twice.
  • CDC and DEBEZIUM. Change data capture is a useful transport for this shift, but (as the misconception notes) CDC on its own leaks table shape. Pair CDC with an outbox table to keep the event contract clean.
  • Analytics maturity. Teams that can answer "what did we know on date X?" have usually already made this shift -- explicitly or by accident. If that question routinely requires a database snapshot restore, you are still CRUD-shaped.

Read This Only If Stuck