Exactly-Once Processing
Strategies for achieving exactly-once message processing.
The Challenge​
In distributed systems, messages can be delivered:
- At-most-once - Messages may be lost
- At-least-once - Messages may be duplicated
- Exactly-once - Each message processed exactly once
Saga Bus provides tools for exactly-once semantics.
Idempotency Middleware​
The idempotency middleware deduplicates messages:
import { createIdempotencyMiddleware } from '@saga-bus/middleware-idempotency';
const bus = createBus({
transport,
store,
sagas,
middleware: [
createIdempotencyMiddleware({
store: idempotencyStore,
keyExtractor: (msg) => msg.messageId,
ttl: 24 * 60 * 60 * 1000, // 24 hours
}),
],
});
Message ID Strategy​
Always include a unique message ID:
await bus.publish({
type: 'OrderSubmitted',
messageId: crypto.randomUUID(), // Unique per message
orderId: '123',
});
Transactional Outbox​
For guaranteed delivery without duplicates:
// Within a database transaction:
await db.transaction(async (tx) => {
// 1. Update business state
await tx.update('orders', { status: 'confirmed' });
// 2. Write to outbox table
await tx.insert('outbox', {
id: uuid(),
type: 'OrderConfirmed',
payload: { orderId },
});
});
// Separate process polls outbox and publishes
Optimistic Concurrency​
Saga Bus uses version-based concurrency:
// Each saga state has a version
interface SagaState {
metadata: {
version: number; // Incremented on each update
};
}
// Concurrent updates fail with ConcurrencyError
// and are automatically retried
Best Practices​
- Always use message IDs - Generate UUID for each message
- Keep idempotency window - Store processed IDs for 24+ hours
- Design handlers idempotently - Same input should produce same result
- Use transactional outbox - For critical state changes