Loading content...
In a large-scale event-driven system, you might have hundreds or thousands of different event types flowing through message brokers, stored in event stores, and processed by countless handlers. In this environment, consistent naming and structure aren't just nice-to-haves—they're essential for system comprehensibility and maintainability.
Well-named events are self-documenting. When a developer sees OrderPaymentFailed in a log, they immediately understand what happened without consulting documentation. When events follow predictable structural patterns, handlers can be written more efficiently and bugs become easier to diagnose.
This page establishes the naming conventions and structural patterns that transform a chaotic event stream into a well-organized, discoverable system.
By the end of this page, you will master event naming conventions that communicate clearly, structural patterns that enable consistency, namespace strategies for organizing events at scale, and anti-patterns to avoid. You'll be able to design event catalogs that remain maintainable as systems grow.
Event names are the primary way developers understand what an event represents. A well-chosen name immediately communicates the event's meaning; a poorly chosen name creates confusion and requires documentation lookups.
The Past Tense Rule:
The most fundamental convention for domain events is past tense naming. Events describe something that has already happened—a fact that cannot be un-done:
OrderPlaced — Something that happenedPaymentReceived — Past tense, completeCustomerRegistered — Registration occurredPlaceOrder — This is a command, not an eventProcessPayment — Imperative, suggests action to takeCustomerRegistering — Present continuous, incompleteWhy Past Tense Matters:
Clarity of Intent: Past tense immediately identifies this as an event (something that happened), not a command (something to do).
Immutability Mindset: Past tense reinforces that events are immutable facts. You can't undo OrderPlaced—you can only create new events like OrderCancelled.
Ubiquitous Language: Domain experts naturally speak in past tense when describing what happened: "The order was placed." Events should match this language.
| Intent | Command (Don't Use) | Event (Use This) |
|---|---|---|
| Customer signs up | RegisterCustomer | CustomerRegistered |
| Order is submitted | SubmitOrder | OrderSubmitted |
| Payment is processed | ProcessPayment | PaymentProcessed |
| Item is shipped | ShipItem | ItemShipped |
| Subscription renews | RenewSubscription | SubscriptionRenewed |
| Account is closed | CloseAccount | AccountClosed |
| Price changes | ChangePrice | PriceChanged |
Imagine your event appearing as a newspaper headline: 'ORDER PLACED by Customer #12345' reads naturally. 'PLACE ORDER for Customer #12345' sounds like an instruction. If it reads like a news report, your naming is correct.
A well-structured event name typically consists of two or three components that together create a complete, self-explanatory identifier.
The Standard Pattern: {Subject}{Action}
Examples:
Order + Placed = OrderPlacedCustomer + Registered = CustomerRegisteredPayment + Failed = PaymentFailedInventory + Depleted = InventoryDepleted1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// Standard pattern: {Subject}{Action}type StandardEvents = | 'OrderPlaced' | 'OrderConfirmed' | 'OrderShipped' | 'OrderDelivered' | 'OrderCancelled' | 'CustomerRegistered' | 'CustomerVerified' | 'CustomerSuspended' | 'PaymentAuthorized' | 'PaymentCaptured' | 'PaymentFailed' | 'PaymentRefunded'; // Extended pattern: {Subject}{Modifier}{Action}// Use when the subject alone is ambiguoustype ExtendedEvents = | 'OrderLineAdded' // Line within an order | 'OrderLineRemoved' | 'OrderShippingAddressChanged' | 'CustomerEmailVerified' // Specifically the email | 'CustomerPhoneUpdated' | 'ProductPriceIncreased' | 'ProductPriceDecreased' | 'InventoryStockReplenished' | 'SubscriptionPaymentFailed'; // Outcome-specific pattern: {Subject}{Outcome}// When the action has specific outcomes worth distinguishingtype OutcomeEvents = | 'PaymentSucceeded' | 'PaymentDeclined' | 'PaymentFailedDueToInsufficientFunds' | 'OrderDeliverySucceeded' | 'OrderDeliveryFailed' | 'VerificationPassed' | 'VerificationFailed'; // Lifecycle events: {Subject}{LifecycleStage}type LifecycleEvents = | 'OrderCreated' | 'OrderSubmitted' | 'OrderApproved' | 'OrderRejected' | 'OrderCompleted' | 'OrderArchived';When to Add Modifiers:
Simple {Subject}{Action} works for most cases, but add modifiers when:
Multiple subjects exist: CustomerEmailUpdated vs CustomerPhoneUpdated — which part of customer?
Different outcomes need distinction: PaymentSucceeded vs PaymentDeclined — same action, different results.
Hierarchy matters: OrderLineRemoved — it's not the order being removed, it's a line within the order.
Specificity aids routing: ProductPriceIncreased lets systems react differently to increases vs decreases.
While modifiers add clarity, don't go overboard. CustomerShippingAddressCityChanged is probably too specific—CustomerAddressUpdated with city in the payload is usually sufficient. Let the event name convey the business concept; let the payload carry the details.
As systems grow, unqualified event names become problematic. Is OrderConfirmed from the e-commerce system or the restaurant reservation system? Namespaces solve this by providing context and preventing collisions.
Namespace Strategies:
Dot Notation uses periods to create hierarchical namespaces, similar to package naming in Java or module paths in Python.
Pattern: {company}.{domain}.{subdomain?}.{eventName}
Examples:
com.acme.orders.OrderPlacedcom.acme.orders.fulfillment.ItemPickedcom.acme.customers.CustomerRegisteredcom.acme.payments.PaymentReceivedAdvantages:
com.acme.orders.*)Disadvantages:
12345678910111213141516171819202122
// Event types with dot notationconst eventTypes = { orders: { placed: 'com.acme.orders.OrderPlaced', confirmed: 'com.acme.orders.OrderConfirmed', shipped: 'com.acme.orders.OrderShipped', fulfillment: { itemPicked: 'com.acme.orders.fulfillment.ItemPicked', orderPacked: 'com.acme.orders.fulfillment.OrderPacked' } }, customers: { registered: 'com.acme.customers.CustomerRegistered', verified: 'com.acme.customers.CustomerVerified' }} as const; // Subscribe to all order eventsbroker.subscribe('com.acme.orders.*', handler); // Subscribe to all fulfillment eventsbroker.subscribe('com.acme.orders.fulfillment.*', handler);Choosing a Strategy:
The best strategy depends on your ecosystem:
Beyond naming, the structure of events—how data is organized within them—significantly impacts usability and maintainability. Consistent structure enables generic processing, simplifies handler development, and aids debugging.
The Envelope Pattern:
One of the most common structural patterns is the envelope pattern, which separates metadata from business payload:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
// The envelope pattern separates concerns cleanly interface EventEnvelope<TPayload> { // === METADATA (same for all events) === meta: { eventId: string; // Unique event instance ID eventType: string; // 'OrderPlaced', etc. version: number; // Schema version occurredAt: string; // ISO 8601 timestamp // Source information aggregateId: string; aggregateType: string; aggregateVersion: number; // Correlation for tracing correlationId: string; causationId: string; // Optional metadata userId?: string; // Who triggered this tenantId?: string; // Multi-tenant systems environment?: string; // 'production', 'staging' }; // === PAYLOAD (event-specific) === payload: TPayload;} // Concrete event using the envelopetype OrderPlacedEvent = EventEnvelope<{ customerId: string; customerEmail: string; items: Array<{ productId: string; productName: string; quantity: number; unitPrice: number; }>; totalAmount: number; currency: string; shippingAddress: { line1: string; city: string; country: string; };}>; // Example instanceconst orderPlacedEvent: OrderPlacedEvent = { meta: { eventId: '550e8400-e29b-41d4-a716-446655440000', eventType: 'OrderPlaced', version: 1, occurredAt: '2024-01-15T14:30:00.000Z', aggregateId: 'order-123', aggregateType: 'Order', aggregateVersion: 1, correlationId: 'req-abc-123', causationId: 'cmd-place-order-456', userId: 'user-789', tenantId: 'acme-corp' }, payload: { customerId: 'cust-456', customerEmail: 'john@example.com', items: [ { productId: 'prod-001', productName: 'Widget Pro', quantity: 2, unitPrice: 29.99 } ], totalAmount: 59.98, currency: 'USD', shippingAddress: { line1: '123 Main St', city: 'Springfield', country: 'US' } }};Benefits of the Envelope Pattern:
Generic Processing: Infrastructure can handle metadata without knowing payload structure. Logging, routing, and storage work uniformly.
Clear Separation: Developers immediately see what's metadata vs business data. Reduces confusion and errors.
Extensibility: Adding new metadata fields doesn't affect payload. New payloads don't affect metadata handling.
Tooling Support: Parsers and validators can handle the wrapper generically while validating payloads specifically.
Some systems prefer flat structures where metadata fields sit alongside payload fields. This is simpler but loses the clear separation. Choose based on your ecosystem—if you have strong tooling that benefits from structure, use envelopes. If simplicity is paramount, flat works too.
Certain naming patterns cause problems at scale. Recognizing these anti-patterns helps you avoid them before they become entrenched.
EntityCreated, EntityUpdated, EntityDeleted are too vague. They don't capture business intent. What kind of creation? Why was it updated?CreateOrder, UpdateCustomer are commands, not events. Use past-tense: OrderCreated, CustomerUpdated.OrdPlcd, CustReg, PmtRcvd sacrifice clarity for brevity. With modern tooling, full words are fine.DatabaseRecordInserted, QueueMessageProcessed, CacheInvalidated leak implementation. What business thing happened?CustomerShippingAddressLine2Updated is too narrow. Group related changes: CustomerAddressUpdated.OrderPlaced with CustomerRegister (imperative) and PaymentProcess (infinitive) creates confusion.KafkaMessageReceived, SQSNotification tie events to infrastructure. What if you change messaging systems?OrderPlacedV2 pollutes the name. Version belongs in metadata, not the event type.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// ❌ ANTI-PATTERN: Generic CRUD eventsinterface EntityUpdated { entityType: string; // Which entity? What a mess! entityId: string; changes: Record<string, unknown>; // No semantic meaning}// What happened? No idea. Must inspect payload. // ✅ CORRECT: Specific, meaningful eventsinterface CustomerAddressUpdated { customerId: string; previousAddress: Address; newAddress: Address; reason: 'customer_request' | 'address_correction';} // ❌ ANTI-PATTERN: Command-style naminginterface ProcessPayment { // This is a command! orderId: string; amount: number;} // ✅ CORRECT: Past-tense event naminginterface PaymentProcessed { orderId: string; amount: number; transactionId: string; processedAt: Date;} // ❌ ANTI-PATTERN: Version in nameinterface OrderPlacedV2 { // Version pollution // ...} // ✅ CORRECT: Version in metadatainterface OrderPlaced { version: 2; // Schema version in metadata // ...payload} // ❌ ANTI-PATTERN: Technical implementation namesinterface DatabaseRowInserted { table: string; rowId: number; data: Record<string, unknown>;} // ✅ CORRECT: Business-meaningful namesinterface OrderCreated { orderId: string; customerId: string; items: OrderItem[];}Poor event names propagate through the system. Handlers, documentation, monitoring dashboards, and team conversations all use these names. Fixing a bad name later means renaming everywhere—schema changes, code changes, runbook updates. Get it right the first time.
Consistent casing improves readability and prevents confusion. While the choice of convention is somewhat arbitrary, consistency is essential.
| Convention | Example | Common Usage |
|---|---|---|
| PascalCase | OrderPlaced | Event types, class names (TypeScript, Java, C#) |
| camelCase | orderPlaced | Event fields, JSON properties (JavaScript, JSON) |
| snake_case | order_placed | Event fields (Python, Ruby, some databases) |
| kebab-case | order-placed | Topics, URLs, file names |
| SCREAMING_SNAKE | ORDER_PLACED | Constants, enum values (some languages) |
1234567891011121314151617181920212223242526272829
// Recommended: PascalCase for event types, camelCase for fields interface OrderPlaced { // PascalCase for type name eventId: string; // camelCase for fields eventType: 'OrderPlaced'; // PascalCase for type value occurredAt: Date; // Payload fields in camelCase orderId: string; customerId: string; orderItems: OrderItem[]; // camelCase totalAmount: number; shippingAddress: Address; // camelCase} // When serializing to JSON, maintain camelCaseconst serialized = JSON.stringify({ eventId: "123", eventType: "OrderPlaced", occurredAt: "2024-01-15T10:00:00Z", orderId: "order-456", customerId: "cust-789"}); // For Kafka topics, use kebab-caseconst topic = "orders.order-placed"; // kebab-case for topic // For Python consumers, you might transform to snake_case// But keep the canonical format consistent in the event itselfThe specific choice matters less than consistency. Document your conventions, enforce them in code review, and consider automated linting. Mixing CustomerEmail with customer_email and customerEmail in the same system is a readability nightmare.
As your event portfolio grows, an event catalog becomes essential—a central registry that documents all events in the system, their purposes, schemas, and ownership.
What an Event Catalog Should Include:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
# Event Catalog Entry: OrderPlacedname: OrderPlacedversion: 2status: active # active | deprecated | retired description: | Raised when a customer successfully places an order. This is the starting point for the order fulfillment process. Contains complete order details including items, pricing, and shipping information. business_context: domain: Orders bounded_context: Order Management aggregate: Order ownership: team: Order Platform slack_channel: "#order-platform" on_call_rotation: order-platform-oncall schema: format: json-schema location: ./schemas/orders/order-placed.v2.json producers: - service: order-service trigger: POST /orders API call consumers: - service: inventory-service action: Reserve stock for ordered items criticality: high - service: notification-service action: Send order confirmation email criticality: medium - service: analytics-service action: Record order for reporting criticality: low examples: - name: Standard order file: ./examples/order-placed-standard.json - name: Guest checkout order file: ./examples/order-placed-guest.json version_history: - version: 2 date: 2024-01-15 changes: - Added shippingAddress.phone field (optional) - Added estimatedDeliveryDate field migration: Consumers should handle missing phone gracefully - version: 1 date: 2023-06-01 changes: - Initial version sla: max_publish_latency_ms: 100 delivery_guarantee: at-least-once ordering: per-aggregate (by orderId)Tools for Event Catalogs:
Several tools can help manage event catalogs:
The tool matters less than having something. Without a catalog, events become tribal knowledge that dies when team members leave.
Consistent naming and structure are foundational to maintainable event-driven systems. Let's consolidate the key principles:
OrderPlaced, not PlaceOrder. This distinguishes events from commands.What's Next:
With naming and structure mastered, the final page in this module explores immutable event objects—the principle that events, once created, cannot be changed, and the implementation techniques that enforce this guarantee.
You now have a comprehensive framework for naming and structuring events consistently. Apply these conventions from day one, and your event ecosystem will remain comprehensible and maintainable as it grows. Next, we'll explore the immutability principle that makes events trustworthy.