Event
WARNING
We created this page with the help of the GenAI tool.
We're currently double-checking it to ensure the information is 100% correct and free of hallucinations.
Events are immutable records of facts that have happened in your system. They are the foundation of Event Sourcing.
Overview
In Event Sourcing, events serve dual purposes:
- Historical record - What happened in your business process
- State source - Events are replayed to rebuild current state
Events are immutable facts. Once recorded, they cannot be changed or deleted.
Type Definition
type Event<
EventType extends string = string,
EventData extends DefaultRecord = DefaultRecord,
EventMetaData extends DefaultRecord | undefined = undefined,
> = Readonly<{
type: EventType;
data: EventData;
metadata?: EventMetaData;
kind?: 'Event';
}>;| Property | Type | Description |
|---|---|---|
type | string | Unique event type name (e.g., 'ProductItemAdded') |
data | object | Business data payload (must be a record, not primitive) |
metadata | object? | Optional infrastructure data (user ID, tenant, timestamps) |
kind | 'Event'? | Discriminator for union types with Commands |
Basic Usage
Defining Event Types
import type { Event } from '@event-driven-io/emmett';
type ProductItemAddedToShoppingCart = Event<
'ProductItemAddedToShoppingCart',
{
shoppingCartId: string;
productItem: PricedProductItem;
}
>;Union Types for Aggregates
Define all events for an aggregate as a discriminated union:
import type { Event } from '@event-driven-io/emmett';
type ShoppingCartEvent =
| Event<
'ShoppingCartOpened',
{
cartId: string;
clientId: string;
openedAt: Date;
}
>
| Event<
'ProductItemAdded',
{
productId: string;
quantity: number;
price: number;
}
>
| Event<
'ProductItemRemoved',
{
productId: string;
quantity: number;
}
>
| Event<
'ShoppingCartConfirmed',
{
confirmedAt: Date;
}
>
| Event<
'ShoppingCartCancelled',
{
cancelledAt: Date;
}
>;Creating Events with Factory
Use the event factory function for runtime event creation:
import { event } from '@event-driven-io/emmett';
const added = event<ProductItemAdded>('ProductItemAdded', {
productId: 'shoes-1',
quantity: 2,
price: 99.99,
});
// Result: { type: 'ProductItemAdded', data: {...}, kind: 'Event' }Events with Metadata
Metadata carries cross-cutting concerns like user identity or tracing:
type AuditMetadata = {
userId: string;
correlationId: string;
timestamp: Date;
};
type AuditedProductItemAdded = Event<
'ProductItemAdded',
{ productId: string; quantity: number; price: number },
AuditMetadata
>;
const auditedEvent = event<AuditedProductItemAdded>(
'ProductItemAdded',
{ productId: 'shoes-1', quantity: 2, price: 99.99 },
{ userId: 'user-123', correlationId: 'req-456', timestamp: new Date() },
);Read Events
When events are read from the event store, they include additional metadata:
type ReadEvent<
EventType extends Event,
MetadataType extends AnyRecordedMessageMetadata,
> = {
type: EventType['type'];
data: EventType['data'];
metadata: CombinedMetadata<EventType, MetadataType>;
};Stream Metadata
Events read from a stream include position information:
type CommonReadEventMetadata = {
streamName: string; // Stream the event belongs to
streamPosition: bigint; // Position within the stream (0-indexed)
createdAt: Date; // When the event was recorded
};Global Position
Some event stores provide global ordering:
type ReadEventMetadataWithGlobalPosition = CommonReadEventMetadata & {
globalPosition: bigint; // Position across all streams
};Utility Types
Extracting Event Properties
import type {
EventTypeOf,
EventDataOf,
EventMetaDataOf,
} from '@event-driven-io/emmett';
type ProductItemAdded = Event<'ProductItemAdded', { productId: string }>;
type EventType = EventTypeOf<ProductItemAdded>; // 'ProductItemAdded'
type EventData = EventDataOf<ProductItemAdded>; // { productId: string }
type EventMeta = EventMetaDataOf<ProductItemAdded>; // undefinedAny Event
For generic handlers that accept any event:
import type { AnyEvent } from '@event-driven-io/emmett';
function logEvent(event: AnyEvent): void {
console.log(`Event: ${event.type}`, event.data);
}Best Practices
1. Use Past Tense Names
Events represent facts that have happened:
// ✅ Good: Past tense
type ProductItemAdded = Event<'ProductItemAdded', {...}>;
type OrderShipped = Event<'OrderShipped', {...}>;
// ❌ Bad: Present/future tense
type AddProductItem = Event<'AddProductItem', {...}>; // This is a command2. Include Sufficient Context
Events should be self-contained:
// ✅ Good: Contains all necessary data
type ProductItemAdded = Event<
'ProductItemAdded',
{
productId: string;
productName: string; // Denormalized for projections
quantity: number;
unitPrice: number;
totalPrice: number; // Computed at event time
}
>;
// ❌ Bad: Missing context
type ProductItemAdded = Event<
'ProductItemAdded',
{
productId: string; // Need to look up product details elsewhere
}
>;3. Avoid Optional Fields
Events are facts; they should be complete:
// ✅ Good: Separate event types
type OrderShippedWithTracking = Event<
'OrderShipped',
{
orderId: string;
trackingNumber: string;
}
>;
type OrderShippedNoTracking = Event<
'OrderShippedNoTracking',
{
orderId: string;
}
>;
// ❌ Bad: Optional fields blur meaning
type OrderShipped = Event<
'OrderShipped',
{
orderId: string;
trackingNumber?: string; // When is it present?
}
>;4. Use Readonly Data
Events are immutable by design:
// The Event type enforces Readonly automatically
type ProductItemAdded = Event<
'ProductItemAdded',
{
productId: string;
items: ProductItem[]; // Becomes readonly
}
>;Type Source
import type { DefaultRecord } from './';
import type {
AnyRecordedMessageMetadata,
CombinedMessageMetadata,
CommonRecordedMessageMetadata,
RecordedMessage,
RecordedMessageMetadata,
RecordedMessageMetadataWithGlobalPosition,
RecordedMessageMetadataWithoutGlobalPosition,
} from './message';
export type StreamPosition = bigint;
export type GlobalPosition = bigint;
export type Event<
EventType extends string = string,
EventData extends DefaultRecord = DefaultRecord,
EventMetaData extends DefaultRecord | undefined = undefined,
> = Readonly<
EventMetaData extends undefined
? {
type: EventType;
data: EventData;
}
: {
type: EventType;
data: EventData;
metadata: EventMetaData;
}
> & { readonly kind?: 'Event' };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyEvent = Event<any, any, any>;
export type EventTypeOf<T extends Event> = T['type'];
export type EventDataOf<T extends Event> = T['data'];
export type EventMetaDataOf<T extends Event> = T extends { metadata: infer M }
? M
: undefined;
export type CreateEventType<
EventType extends string,
EventData extends DefaultRecord,
EventMetaData extends DefaultRecord | undefined = undefined,
> = Readonly<
EventMetaData extends undefined
? {
type: EventType;
data: EventData;
}
: {
type: EventType;
data: EventData;
metadata: EventMetaData;
}
> & { readonly kind?: 'Event' };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const event = <EventType extends Event<string, any, any>>(
...args: EventMetaDataOf<EventType> extends undefined
? [type: EventTypeOf<EventType>, data: EventDataOf<EventType>]
: [
type: EventTypeOf<EventType>,
data: EventDataOf<EventType>,
metadata: EventMetaDataOf<EventType>,
]
): EventType => {
const [type, data, metadata] = args;
return metadata !== undefined
? ({ type, data, metadata, kind: 'Event' } as EventType)
: ({ type, data, kind: 'Event' } as EventType);
};
export type CombinedReadEventMetadata<
EventType extends Event = Event,
EventMetaDataType extends AnyRecordedMessageMetadata =
AnyRecordedMessageMetadata,
> = CombinedMessageMetadata<EventType, EventMetaDataType>;
export type ReadEvent<
EventType extends Event = Event,
EventMetaDataType extends AnyRecordedMessageMetadata =
AnyRecordedMessageMetadata,
> = RecordedMessage<EventType, EventMetaDataType>;
export type AnyReadEvent<
EventMetaDataType extends AnyReadEventMetadata = AnyReadEventMetadata,
> = ReadEvent<AnyEvent, EventMetaDataType>;
export type CommonReadEventMetadata = CommonRecordedMessageMetadata;
export type ReadEventMetadata<HasGlobalPosition = undefined> =
RecordedMessageMetadata<HasGlobalPosition>;
export type AnyReadEventMetadata = AnyRecordedMessageMetadata;
export type ReadEventMetadataWithGlobalPosition =
RecordedMessageMetadataWithGlobalPosition;
export type ReadEventMetadataWithoutGlobalPosition =
RecordedMessageMetadataWithoutGlobalPosition;See Also
- Getting Started - Events
- Command - Requests to change state
- Decider - Pattern using events and commands
- Read Models - Building read models from events
