Skip to main content

Messages

Messages are the foundation of saga communication. They represent events, commands, and queries.

Message Structure

Every message must have a type property:

interface OrderSubmitted {
type: 'OrderSubmitted';
orderId: string;
customerId: string;
items: Array<{ productId: string; quantity: number }>;
total: number;
submittedAt: Date;
}

Message Types

Events

Events describe something that happened. Use past tense:

// ✅ Good - past tense
interface PaymentCaptured {
type: 'PaymentCaptured';
orderId: string;
transactionId: string;
}

// ❌ Bad - not an event
interface CapturePayment { ... }

Commands

Commands request an action. Use imperative verbs:

// ✅ Good - imperative
interface CapturePayment {
type: 'CapturePayment';
orderId: string;
amount: number;
}

Message Envelope

When messages are transported, they're wrapped in an envelope:

interface MessageEnvelope<T = unknown> {
messageId: string; // Unique message ID
messageType: string; // The 'type' property value
correlationId?: string; // Business correlation ID
causationId?: string; // ID of message that caused this
timestamp: Date; // When message was created
headers: Record<string, string>; // Custom headers
payload: T; // The actual message
}

Best Practices

Include Business IDs

Always include the primary business identifier:

interface OrderShipped {
type: 'OrderShipped';
orderId: string; // ✅ Primary business ID
trackingNumber: string;
carrier: string;
}

Avoid Nested Messages

Keep messages flat:

// ✅ Good - flat structure
interface OrderSubmitted {
type: 'OrderSubmitted';
orderId: string;
customerId: string;
totalAmount: number;
}

// ❌ Avoid - deeply nested
interface OrderSubmitted {
type: 'OrderSubmitted';
order: {
id: string;
customer: {
id: string;
};
};
}

Use Union Types

Create a union of all messages for type safety:

export type OrderMessages =
| OrderSubmitted
| PaymentCaptured
| PaymentFailed
| OrderShipped
| OrderCompleted;

Type Inference

The saga DSL infers message types automatically:

const saga = createSagaMachine<OrderState, OrderMessages>()
.on('PaymentCaptured')
.handle(async (msg, state, ctx) => {
// TypeScript knows: msg.transactionId exists
console.log(msg.transactionId);
return state;
})