EventsGENERALPROTOCOL-SPECIFIC

Naming Events

Past-tense domain facts decouple; procedural names smuggle the consumer's behaviour into the producer.

What actually happensHow to build it

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind has a problem.

The question

Why is OrderCreated a better event name than ProcessOrderEvent, and what breaks when I get this wrong?

The requirement

We are introducing a topic for orders. Three teams will consume it, and two of them do not exist yet.

The obvious build

Name it after what should happen next, because that is the reason we are publishing it. ProcessOrderEvent, SendOrderConfirmation, UpdateInventoryEvent. Everyone knows what it means.

Why it breaks

ProcessOrderEvent names the consumer's behaviour, so the producer has now asserted what should happen. The producer has taken back the coupling that publishing an event was supposed to remove.

How it breaks in production
  • ProcessOrderEvent names the consumer's behaviour, so the producer has now asserted what should happen. The producer has taken back the coupling that publishing an event was supposed to remove.
  • A second consumer appears — analytics — and the name is now wrong for it. Nobody is "processing an order" for analytics; it is just recording that one exists.
  • "Processing" changes meaning. The word once meant charge and reserve; now it means charge, reserve, tax and fraud-check. The name is unchanged and no longer describes anything specific.
  • A message named SendOrderConfirmation is really a command. When a second subscriber attaches to it, confirmations go out twice (Commands vs Events).
  • OrderUpdatedEvent as a catch-all forces every consumer to diff old and new state to work out whether it cares — so every consumer contains a copy of the producer's business rules.
RequirementAPI ContractApplication LogicData AccessExternal DepsConcurrencyFailureSecurityObservabilityDeploymentScale

What is actually happening

  • An event name is a contract with unknown future consumers. It is read by people who cannot ask the producer what it meant, and it will be read years after the person who chose it has left.
  • A past-tense domain fact — OrderPlaced, PaymentCaptured, SubscriptionCancelled — is a statement about the world that stays true regardless of who reacts. Nothing about it needs to change when the set of reactions changes.
  • A procedural name — ProcessOrderEvent, HandleSignupEvent — is an instruction wearing an event's clothes. It encodes the reaction, which means the producer must be edited whenever the reaction changes, and it means "who owns this?" has no clean answer.
  • Names also carry a granularity decision. OrderUpdated is one name for twenty different facts; OrderShipped, OrderCancelled and OrderAddressChanged are three names a consumer can subscribe to selectively.
  • The payload is part of the name's contract. A thin event carries identifiers and lets the consumer fetch what it needs; a fat event carries the state, which removes the fetch and freezes a snapshot of your internal model into a published schema (Schema Leakage).

The name tells you who owns the decision

Read the two names below as if you were a new engineer joining the consuming team. The first tells you what you are supposed to do, which means someone else decided. The second tells you what is true, which means you decide.

That is the entire argument, and it has a practical consequence: a procedural name makes every future change a two-team conversation, because the producer holds an opinion about a behaviour it does not implement.

Same message, two contracts
Procedural
broker.publish('ProcessOrderEvent', {
  orderId,
  shouldSendEmail: true,
  skipInventory: false,
})
// the producer is telling consumers what to do,
// and now owns a flag per consumer behaviour
Domain fact
broker.publish('orders.OrderPlaced', {
  eventId,          // dedupe key
  version: 2,       // payload schema version
  occurredAt,       // when the fact became true
  orderId,
  customerId,
  orderVersion: 7,  // entity version, for ordering
})
// consumers decide what, if anything, to do

The shouldSendEmail flag is the tell: the producer is now configuring a consumer it does not own, so every change to email behaviour is a change to checkout. The fact version carries no behaviour at all, which is why a consumer added next year needs no producer change.

Names that age well, and names that do not

The failure of a bad event name is rarely immediate. It shows up eighteen months later, when the name is describing something it no longer describes and three consumers have each built a different workaround.

Grade a candidate name against two questions: does it still make sense if the current consumers are deleted, and does it still make sense if two unrelated consumers are added?

NameVerdictWhy it ages the way it does
orders.OrderPlacedGoodA fact in the producer's domain. Stays accurate no matter who consumes it.
billing.PaymentCapturedGoodNames the specific financial event, distinguishable from authorised or settled.
ProcessOrderEventBadNames a behaviour. Wrong for the second consumer; meaningless once "process" changes.
SendWelcomeEmailCommandImperative, one legitimate handler. Route it as a job, not a topic (Job Queues).
OrderUpdatedToo coarseOne name for twenty facts. Every consumer reimplements the producer's diff logic.
UserEventMeaninglessType discriminated inside the payload, so subscription filtering and schema checks are both impossible.
OrderPlacedV2Version in the nameSplits the topic and doubles subscriptions. Version belongs in the envelope, not the type.
InventoryReservedGoodA fact owned by inventory. Checkout can consume it without either side knowing the other's rules.

Thin or fat: what goes in the payload

Having chosen a name, you choose how much of the world travels with it. This is a real decision with no default answer, and the criteria differ by consumer count, payload sensitivity and whether the consumer can tolerate fetching.

The frequently-missed constraint is auditability. A consumer that fetches current state cannot reconstruct what was true at the time of the event, which matters enormously for anything financial or historical.

How much state should the event carry?

Can the consumer fetch what it needs, and does it need the state as it was at the time?

Thin — ids only

when Few consumers, low fan-out, consumers already have API access to the producer, payload would contain sensitive fields.

cost Every consumer fetches, so the producer's availability is back in the path and N consumers means N reads per event.

Fat — full state snapshot

when High fan-out, consumers in other trust domains, or the consumer must know what was true at the time (audit, pricing, ledger).

cost Your internal model becomes a published schema; payload size multiplies with consumers; sensitive fields are broadcast (Three Models, Not One).

Hybrid — ids plus the fields that made the fact

when The common case. Carry what changed and what a consumer needs to decide whether to care.

cost Requires judgement per event and drifts over time unless the catalogue is maintained.

Full event sourcing — the event is the state

when The domain genuinely needs a replayable history as the source of truth: ledgers, compliance.

cost A different architecture, not a payload choice. Schema evolution, replay and projections all become core concerns (Event-Driven Backends).

How to build it

Most important first.

  • Use <Entity><PastTenseVerb> in the producer's domain language: OrderPlaced, InvoiceIssued, UserDeactivated. If the name reads as an instruction, it is a command and belongs in a queue with one owner.
  • Name the fact, never the reaction. The test: could a consumer that does something completely unrelated still find this name accurate? If no, the name is too specific to one consumer.
  • Prefer several precise events to one Updated event. Consumers that only care about cancellations should be able to say so in their subscription rather than in an if.
  • Namespace by owning context — orders.OrderPlaced, billing.InvoiceIssued — so the name says who is authoritative for the fact.
  • Carry a version in the envelope and treat payload changes as API changes: additive fields are safe, removing or retyping a field is a breaking release that needs a migration plan (Running Two API Versions in One Service).
  • Put stable metadata in a fixed envelope: event id, type, version, occurred-at, aggregate id, correlation id. Every consumer needs those and none of them are domain-specific.

What can go wrong

Failure modes
  • A name that outlives its meaning: UserSignedUp is still published when a user is imported in bulk, and every consumer that assumed "a human just did something" sends 40,000 welcome emails.
  • A field a consumer depends on quietly stops being populated. No compile error anywhere — the consumer just starts seeing undefined and writing nulls (Writing Event Consumers).
  • occurred-at set at publish time rather than at fact time, so consumers computing durations get the relay's delay baked into their numbers.
  • Fat events that mirror the database row, so a routine internal column rename becomes a coordinated multi-team release.
  • Two producers publishing the same event type with slightly different payloads, discovered by a consumer crashing on one of them.
What can race
  • Two events for the same entity, published in one order and consumed in another, so an older payload overwrites a newer one — which is why events carry an entity version and consumers compare it (Keeping a Search Index in Sync).
Security
  • A fat event is a broadcast of internal state to every current and future consumer. Fields such as internal notes, cost prices or email addresses are disclosed to consumers that never needed them (Schema Leakage).
  • Thin events with ids make authorization possible at fetch time — the consumer asks for the entity and the check runs then. Fat events skip that check entirely.
  • Personal data in event payloads persists in broker retention and consumer stores, so a naming and payload decision is also a data-retention decision (Sensitive Data Classification is the security domain's treatment).
Misreads
  • "Past tense is a style rule." It is a coupling rule. The tense is how you can tell whether the producer or the consumer owns the decision about what happens next.
  • "OrderUpdated covers everything, so it is flexible." It pushes the producer's business logic into every consumer, which each implements slightly differently and none of which stays in sync.
  • "Fat events remove coupling because the consumer needs nothing else." They convert a fetch-time coupling into a schema-time coupling, which is harder to change and harder to see.
  • "We can just add a field." Adding is usually safe. What is not safe is a consumer treating a newly-optional field as required, or a field whose meaning changes while its name does not.

Operating it

How you see it in production
  • Maintain a schema registry or at least a checked-in catalogue of event types with owners. "Which service publishes this and what does the payload look like" should not require reading source.
  • Emit a metric per event type and version. A version that is still being published months after the migration was declared finished is a live consumer nobody migrated.
  • Log unknown event types received by consumers. A producer adding a type nobody handles is normal; a consumer receiving a type it does not recognise on a topic it subscribes to is a contract drift signal.
What changes at 10x and 100x
  • Names cost nothing to add and are effectively impossible to remove once external consumers exist. The catalogue grows monotonically, which is an argument for precision at creation time.
  • Fat payloads multiply by consumer count on the wire and in storage. At high fan-out this becomes a real bandwidth and retention cost, and thin events plus a fetch are cheaper overall (Event-Driven Backends).
  • Thin events at high volume turn into a fetch storm against the producer: N consumers each fetching each entity. That is the counter-argument, and it is a genuine one (The N+1 Query Problem).
What this costs
  • Fine-grained past-tense names mean more types to catalogue, document and version. The alternative is fewer names that mean less.
  • Thin events keep the schema small and the coupling low, at the cost of a synchronous fetch in every consumer — which reintroduces a dependency on the producer being available.
  • Renaming an event correctly means publishing both names for a deprecation window and running dual consumers. Getting the name right the first time is meaningfully cheaper than any rename.

Where this applies

Backend advice is context-sensitive. These labels say what each claim is specific to, and where a different stack or scale would differ.

  • GENERALNaming and payload-shape reasoning is independent of broker and language; only the mechanics of schema registration differ.
  • PROTOCOL-SPECIFICSome ecosystems enforce compatibility at the serialization layer — a schema registry with Avro or Protobuf will reject an incompatible payload change at publish time. With plain JSON nothing is enforced, so the same discipline has to live in review and tests (Contract Tests Between Services).

Where the depth lives

This domain teaches the application-side mechanism and hands the rest off.

Domains that do not exist yet
  • Distributed Systems — schema evolution across independently deployed producers and consumers with no coordinated release.