Name the business fact before naming the event
A dependable event describes a business fact that has already happened, not an instruction for another system. customer.account.created is an event; create.customer.account is closer to a command. The distinction matters because an event may have several consumers that the producer does not know about, while a command normally has an intended handler. When a command is disguised as an event, ownership of retries, failures, and compensating actions quickly becomes unclear.
A useful convention is domain.entity.past-action, as in sales.order.confirmed, inventory.stock.reserved, or support.ticket.closed. The domain should represent a durable business boundary rather than the current department name. The entity should use language shared by business and engineering teams, and the action should state the completed transition. Avoid embedding vendors, applications, or delivery channels in a core event name unless they are part of the business meaning. A CRM replacement should not force every consumer to rename customer events.
Generic actions such as updated and changed deserve scrutiny. An address correction, a payment-state transition, and a cancellation-reason update can have different authorization, timing, and downstream behavior. If every consumer must inspect several payload fields to discover what happened, the event is probably too broad. The opposite extreme is also costly: do not create an event for every database column. Split events when a change has independent business meaning or causes consumers to take materially different actions.
Standardize the envelope and isolate domain data
For cross-team work, separate a stable event envelope from the domain payload. The envelope supports routing, tracing, deduplication, and version detection; the data section contains the order, customer, device, or other business snapshot. This prevents teams from inventing incompatible timestamp and trace fields, and it lets shared monitoring and replay tooling operate without understanding every domain model.
- event_id: A unique identifier used by consumers for idempotency. Redelivery of the same event should retain the same value.
- event_type: The stable semantic name, such as sales.order.confirmed, kept independent from a broker topic or queue name.
- schema_version: The explicit payload contract version, not a deployment date, Git commit, or producer service version.
- occurred_at: The time the business fact occurred. Record publication time separately if it is operationally useful.
- producer and subject: The source and primary entity identifier, supporting search, authorization, and replay.
- correlation_id and causation_id: Identifiers that connect an end-to-end workflow and the event or command that directly caused this event.
Consistent field names do not guarantee consistent meaning. Specify timestamp format and timezone, currency for money, units for quantities, and whether an identifier is an internal key, an external reference, or a public ID. Include enough of the business snapshot for the intended reaction, but do not publish a database row by default. Too little data creates synchronous callbacks to the source system; too much expands the contract, exposes unnecessary sensitive data, and makes future changes harder.
Version according to compatibility, not edit count
Versions protect consumers; they are not a development changelog. Adding an optional field can normally remain within the current major version if consumers tolerate unknown fields. Renaming or removing a field, changing its type, unit, identifier semantics, or required status, and assigning new meaning to an existing state are breaking changes. A payload can remain structurally identical and still be incompatible when an existing consumer would make a different business decision.
New enumeration values are a frequently underestimated case. They look additive, but many consumers implement exhaustive switches and fail on an unfamiliar value. Contracts should require an unknown or default path, while producers should still assess consumers before adding a value that introduces new workflow behavior. Apply the same behavioral test to nullability, precision, and tighter string formats. Passing schema validation alone does not prove that the integration remains compatible.
A practical default is to keep event_type stable and carry the major contract version in schema_version. Put versions in broker topics only when infrastructure must isolate permissions, retention, throughput, or migration traffic. Versioned topics provide clear routing but multiply subscriptions, access rules, dashboards, and replay procedures. Conversely, do not create a major version for every optional addition. Excess versions leave producers maintaining multiple formats indefinitely and make it difficult to determine which contracts are still active.
Treat rollout and retirement as part of the contract
An event catalog should record the owner, business definition, schema, examples, data classification, compatibility policy, and known consumers. Ownership belongs with the domain team that can decide what the event means, not with the team operating the broker. A platform team can govern envelope and transport standards, but it cannot decide whether order.confirmed means payment completed, manual approval received, or basic validation passed.
Before releasing a breaking version, inventory consumers and replay requirements, then choose among dual publishing, a translation layer, or a coordinated migration. Dual publishing is easy to understand but adds producer logic and creates a risk that consumers process the same business fact twice. A translator reduces source-system work but becomes another production component requiring ownership and monitoring. If stored events may be replayed, decide whether new consumers will read historical formats or whether events will be upgraded during replay.
Support each change with schema compatibility checks, producer contract tests, representative consumer tests, and alerts for unknown versions or parsing failures. A deprecation notice should identify the replacement, affected event types, migration steps, accountable owners, and the conditions for stopping publication. Set timing according to consumer release cycles and operational risk, then verify that the old version is actually removed. Indefinite dual publishing turns a temporary compatibility measure into permanent architecture debt; experienced integration teams manage naming, versioning, observability, and retirement as one lifecycle.
