Naming Events
Past-tense domain facts decouple; procedural names smuggle the consumer's behaviour into the producer.
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.
Why is OrderCreated a better event name than ProcessOrderEvent, and what breaks when I get this wrong?
We are introducing a topic for orders. Three teams will consume it, and two of them do not exist yet.
Name it after what should happen next, because that is the reason we are publishing it. ProcessOrderEvent, SendOrderConfirmation, UpdateInventoryEvent. Everyone knows what it means.
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.
ProcessOrderEventnames 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
SendOrderConfirmationis really a command. When a second subscriber attaches to it, confirmations go out twice (Commands vs Events). OrderUpdatedEventas 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.
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.
OrderUpdatedis one name for twenty different facts;OrderShipped,OrderCancelledandOrderAddressChangedare 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.
broker.publish('ProcessOrderEvent', {
orderId,
shouldSendEmail: true,
skipInventory: false,
})
// the producer is telling consumers what to do,
// and now owns a flag per consumer behaviourbroker.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 doThe 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?
| Name | Verdict | Why it ages the way it does |
|---|---|---|
orders.OrderPlaced | Good | A fact in the producer's domain. Stays accurate no matter who consumes it. |
billing.PaymentCaptured | Good | Names the specific financial event, distinguishable from authorised or settled. |
ProcessOrderEvent | Bad | Names a behaviour. Wrong for the second consumer; meaningless once "process" changes. |
SendWelcomeEmail | Command | Imperative, one legitimate handler. Route it as a job, not a topic (Job Queues). |
OrderUpdated | Too coarse | One name for twenty facts. Every consumer reimplements the producer's diff logic. |
UserEvent | Meaningless | Type discriminated inside the payload, so subscription filtering and schema checks are both impossible. |
OrderPlacedV2 | Version in the name | Splits the topic and doubles subscriptions. Version belongs in the envelope, not the type. |
InventoryReserved | Good | A 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.
Can the consumer fetch what it needs, and does it need the state as it was at the time?
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.
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).
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.
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
Updatedevent. Consumers that only care about cancellations should be able to say so in their subscription rather than in anif. - 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
- A name that outlives its meaning:
UserSignedUpis 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
undefinedand writing nulls (Writing Event Consumers). occurred-atset 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.
- 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).
- 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).
- "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.
- "
OrderUpdatedcovers 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
- 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.
- 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).
- 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.
- — Distributed Systems — schema evolution across independently deployed producers and consumers with no coordinated release.