Skip to main content

An Event Is an Immutable Fact About the Past

What This Concept Is

An event is a record that something already happened. It has three non-negotiable properties:

  • Past tense. The name describes a thing that is already true in the world. OrderPlaced, PaymentCaptured, EmailAddressChanged. Never PlaceOrder, CapturePayment, ChangeEmailAddress -- those are commands, not events.
  • Immutable. Once emitted, an event is not edited, deleted, or retracted. If the world later changes, you emit a new event (OrderCancelled, EmailAddressChanged again). The old event is still true: it happened.
  • Authored by the system that knows it happened. The producer is the system of record for that fact. Nobody else gets to publish PaymentCaptured except the payment service.

A well-formed event carries:

  • an event ID (unique, for dedup)
  • a timestamp (when it happened)
  • a subject ID (what it is about -- order_id, user_id)
  • a payload (the facts, enough for a consumer to act without asking back)
  • optionally a version (schema evolution) and a correlation ID (tracing across a workflow)

Why It Matters Here

Every downstream idea in this module -- pub-sub, outbox, log-based brokers, sagas, event sourcing -- assumes you actually know what an event is. If you smuggle commands or queries into your "event" stream, every pattern that follows degrades into an ad hoc RPC protocol with worse latency, weaker guarantees, and more operational cost than the HTTP call you were trying to replace.

The single most common reason event-driven systems "don't work" in practice is that the events are not events. They are commands disguised as events, or CRUD rows pushed through a broker.

Concrete Example

A correct event from a checkout service:

{
"event_id": "01HX8...Z2",
"event_type": "OrderPlaced",
"occurred_at": "2026-04-22T14:11:03Z",
"order_id": "ord_9f2a",
"customer_id": "cus_4b8c",
"total_cents": 4299,
"currency": "USD",
"items": [
{"sku": "SKU-42", "qty": 2, "unit_cents": 1999}
],
"schema_version": 3
}

Things that make this an event:

  • the name is past tense and describes something that is now true
  • there is no instruction ("send email", "charge card"); consumers decide what to do
  • the payload is self-contained enough to be acted on without a callback to checkout
  • the producer is checkout, the system that knows the order was placed

Counterexample: A Command Disguised as an Event

{
"event_type": "SendOrderConfirmationEmail",
"order_id": "ord_9f2a",
"customer_email": "a@b.com",
"template": "order_confirmation_v2"
}

This is not an event. It is a remote command wearing an event's clothes. Giveaways:

  • the name is an imperative ("Send...")
  • it names a single intended action ("email") and a specific handler implementation ("template")
  • there is only one plausible consumer (the email service); pub-sub here is theater
  • if tomorrow you also want to send an SMS, you will add SendOrderConfirmationSMS, then PushNotifyOrderConfirmation, and the checkout service has quietly become the orchestrator of the entire post-order world

The fact is OrderPlaced. Emailing is one reaction to that fact. The email service subscribes to OrderPlaced and decides, on its own, how (or whether) to notify. Commands go point-to-point; events go past-tense and public.

Common Confusion / Misconception

"An event is any message on a message bus." No. A message on a bus can be an event, a command, or a reply. The broker does not know and does not care. Intent lives in your naming and design discipline, not in the transport.

"If the event is mutable, we can just fix the bug by republishing it corrected." That is exactly the habit event-driven design is meant to kill. Republishing under the same ID is worse than wrong: consumers that already handled it won't re-process, and consumers that dedupe will drop your "correction." Emit a new event (OrderCorrected, OrderCancelled, etc.).

"Events should be small -- just the ID -- so consumers can query back for details." That is a legitimate design choice (event notification), but it is not a rule. See Concept 05 for when you want the payload to carry enough state to make the consumer self-sufficient.

How To Use It

When you draft an event, apply this checklist:

  1. Is the name past tense?
  2. Does the name describe a fact, not an instruction or a desire?
  3. Could at least two independent consumers plausibly care about it? (If not, reconsider whether this is a command.)
  4. Is the producer the authoritative source for this fact?
  5. Is the payload enough to be meaningful without a callback? (Can be relaxed -- see notification style.)
  6. Is there an event_id for dedup and an occurred_at timestamp?
  7. Does the name belong to the producer's domain vocabulary, or is it borrowed from a consumer? Borrowed names (ReadyForBilling, ReadyForEmail) are a strong tell the "event" is actually a command addressed to a specific downstream.

If any answer is "no," the event is not yet well-formed. A common field-tested rule: if you cannot write the event's Wikipedia entry -- a one-line past-tense statement that an informed outsider could understand -- the event is still named from a process view, not a fact view.

Check Yourself

  1. Which of these names is an event and which is a command: UserSignedUp, SendWelcomeEmail, InvoicePaid, RetryFailedPayment?
  2. Why does a "correction" to an event emerge as a new event, not a mutation of the old one?
  3. Name the minimum three fields every event must carry and why.

Mini Drill or Application

Take a CRUD system you know (a blog, a to-do app, a banking ledger). In 15 minutes:

  1. List 10 operations as they exist today (createPost, updateUser, deleteComment, etc.).
  2. For each, either write the past-tense event it produces (PostPublished, UserEmailChanged, CommentRemoved) or mark it as a command with no interesting event.
  3. For any event where you had to invent the past-tense name, write one sentence on what the consumers could do with it. If you cannot name two plausible consumers, reconsider.

Transfer to Adjacent Domains

  • Domain-Driven Design (S7M3). A well-formed event is a Domain Event in the DDD sense -- a fact published by an aggregate when an invariant-preserving transition commits. If your bounded context already emits domain events inside the process boundary, the hard work of Concept 01 is half done; you only have to decide what crosses the context wall.
  • Messaging patterns (next cluster). Everything in Cluster 2 assumes the events are real -- pub-sub (04), notification vs ECST (05), outbox (06) are all "what do we do with properly-shaped events." Misnamed events break every pattern downstream.
  • Analytics / lakehouse pipelines. The same past-tense-fact discipline is what separates a clean event lake (one fact per row, immutable, keyed) from a tangle of CDC rows and command traces that analysts have to detangle after the fact.
  • Regulatory / audit. Immutability-by-construction is the shortest path to "we can reconstruct what the system knew at time T." That property is not an afterthought; it must be in the event contract from day one.

Read This Only If Stuck