RSS Amplifier

Blog – Kevin Webber · Nov 4, 2025

The Battle of Event Naming in Event-Sourced Systems

0
Sign in to vote or save

Kevin Webber

The event sourcing community has strong opinions on event naming that can confuse folks new to the space, and at times can feel like something of a holy war. Having worked in event-sourced systems for well over a decade, and having been published by O’Reilly on event sourcing as early as 2017, one thing that is hard to escape is how visceral the event naming topic can become even amongst people that agree on nearly everything else surrounding event sourcing.

The naming debates take several forms.

  • Some teams default to CRUD-style names like OrderUpdated or AccountModified that erase the business meaning of what actually happened. A deposit, a name correction, and a fee assessment are three different business facts, but AccountModified flattens them into one.
  • Others encode context into the name. OrderPlacedByPhone puts channel information in the name rather than the payload, creating a proliferation of event types for what the business considers a single type of fact.
  • Compound names like ItemSoldAndTaxCalculated couple two independent facts into one event. (Greg Young’s versioning guidance is direct on this last point; if you find “and” in an event name, you have two events.)

Those are problems of what the name encodes. There is also a separate debate about tense that is often misunderstood.

Past tense naming is often reduced to a style convention. It is not. In a reactive system, there is no “now.” There are facts that have already occurred and intent that has not yet been realized. Events are past tense because they represent recorded facts. Commands are future tense because they represent intent that may or may not succeed. The distinction is not merely grammatical. A system that conflates the two will make false assumptions about outcomes, and false assumptions about outcomes are the root cause of most failures in distributed systems.

Capturing meaning and time in the name

In a domain-driven approach, event names come from the ubiquitous language of the business. If the business says “order placed,” the event is OrderPlaced. If the business says “funds reserved,” the event is FundsReserved. The name is a human-readable label that aligns with how domain experts talk about the process. Encoding implementation semantics into the name creates a translation layer between the business vocabulary and the code, which is exactly the kind of drift that DDD and Event Storming are designed to prevent.

This becomes especially clear when events are wide events. A wide event carries its full context: identity, causality, intent, correlation, writer, processing metadata, and observability data. The ObzenFlow framework’s ChainEvent structure demonstrates this. The event name is a simple string (event_type: String) inside the payload. All reasoning about causality, agency, and intent happens through the surrounding context fields, not through the name.

pub enum ChainEventContent {
    Data {
        event_type: String, // "OrderPlaced", "FundsReserved", etc.
        payload: Value,
    },
    FlowControl(FlowControlPayload),
    Delivery(DeliveryPayload),
    Observability(ObservabilityPayload),
}

When events carry their full context, the name does not need to encode what the payload already contains. OrderPlaced is a perfectly valid event name if that is how the business describes what happened.

Capturing depth and maturity in the payload

The CHAIN maturity model makes this concrete. CHAIN defines progressive maturity levels for each dimension of system reflectivity. At C1 (single-writer journals), events are appended with monotonic ordering and a simple event_type string. At C2 (causal linking), events carry explicit causal_parent_ulid and correlation_ulid fields that link every event to the command or event that triggered it. At higher levels, vector clocks enable distributed causal ordering and graph-based traversal of the entire causal history.

At each level, the properties that matter for traceability, auditability, and replay are carried by structured context fields. Causality is tracked through causal parent references. Intent is captured in an explicit intent context. Agency is recorded through writer IDs. Narrative is reconstructed from the event stream. None of these properties depend on the event name carrying structural information.

ObzenFlow itself operates at C2 maturity with causal linking and correlation propagation. It does not need C4 (enforced distributed causality with vector clock rejection) because the current domains do not require it. This is the point of a maturity model. You design to the level your system needs, measure where you are, and increase maturity when the domain demands it. Overloading event names with implementation detail does not move you up the maturity curve. Building structured context into your event model does.

To see what this looks like at high maturity, consider an event at CHAIN level A4 (verifiable agency). The event name is TradeClosed. Two words, straight from the ubiquitous language. The context carries everything the system needs for auditability, non-repudiation, and regulatory compliance.

INSERT INTO account_events (
  event_type,
  amount,
  subject_user_id,
  principal_id,
  principal_verified,
  identity_provider,
  principal_subject_claim,
  policy_input,
  policy_decision,
  policy_version,
  workflow_id,
  workflow_approver_id,
  workflow_approved_at,
  event_signature,
  signer_cert_thumbprint,
  proof_chain
) VALUES (
  'TradeClosed',                    -- event name: two words from the business
  1000,
  42,
  99,
  TRUE,
  'OIDC',
  'user-alice@example.com',
  '{"event":{...},"principal":{...},"environment":{...}}'::jsonb,
  'ALLOW',
  'rev-20250510a',
  'wf-20250510-closure',
  1234,
  '2025-05-10T20:59:00-04:00',
  'MEUCIQDox...base64-signature...',
  'AB:12:CD:34:EF:56:78:90',
  '[{"seq":5321,"signature":"...","signer":"policy-engine"},
    {"seq":5319,"signature":"...","signer":"workflow-service"}]'::jsonb
);

This event records who acted (principal_id, verified through OIDC), what policy authorized the action (policy_input, policy_decision, policy_version), who approved the workflow (workflow_approver_id), and a cryptographic proof chain that an auditor can verify independently. The event name contributed none of that information. The structured context carried all of it. Renaming this event to TradeUpdated would add zero auditability, zero traceability, and zero regulatory value. It would erase the business meaning and force every consumer to inspect the payload to determine what actually happened.

Naming events after the business process keeps the model aligned with the domain. Naming events after implementation structure couples the model to a specific architecture. For systems designed to evolve over time, the first approach is more durable.

The naming debate does not need to be a holy war. Name events the way the business describes what happened. Let the payload carry everything else.

Read the original on kevinwebber.ca

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.