A NestJS module implementing the Transactional Outbox Pattern for reliable event delivery in distributed systems. Ensures atomic database updates and event emissions, preventing data inconsistencies when services crash or networks fail.
- Atomic Operations: Database changes and events are persisted in a single transaction
- Guaranteed Delivery: Polling mechanism ensures events are delivered even after crashes
- PostgreSQL LISTEN/NOTIFY: Real-time event delivery without polling latency (MikroORM + PostgreSQL only)
- Graceful Shutdown: In-flight events complete before application terminates
- Multiple ORMs: TypeORM and MikroORM drivers included
- Flexible Processing: Immediate or deferred event processing per event type
- Dual Write Consistency: Database updates and event emissions are atomic—no partial failures
- Reliable Event Delivery: Events survive network issues, service crashes, and downtime
- Cross-Module Consistency: All system parts stay synchronized via guaranteed event delivery
# Core package
npm install @fullstackhouse/nestjs-outbox
# Choose your ORM driver
npm install @fullstackhouse/nestjs-outbox-typeorm-driver
# or
npm install @fullstackhouse/nestjs-outbox-mikro-orm-driverimport { OutboxEvent } from '@fullstackhouse/nestjs-outbox';
export class OrderCreatedEvent implements OutboxEvent {
public readonly name = OrderCreatedEvent.name;
constructor(
public readonly orderId: string,
public readonly customerId: string,
) {}
}import { Injectable } from '@nestjs/common';
import { OnEvent } from '@fullstackhouse/nestjs-outbox';
@Injectable()
export class OrderNotificationListener {
constructor(private readonly emailService: EmailService) {}
@OnEvent(OrderCreatedEvent.name)
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.emailService.sendOrderConfirmation(event.orderId);
}
}Multiple event handlers in one class:
@Injectable()
export class OrderNotificationListener {
@OnEvent(OrderCreatedEvent.name)
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
// Handle order created
}
@OnEvent(OrderUpdatedEvent.name)
async handleOrderUpdated(event: OrderUpdatedEvent): Promise<void> {
// Handle order updated
}
}import {
TransactionalEventEmitter,
TransactionalEventEmitterOperations
} from '@fullstackhouse/nestjs-outbox';
@Injectable()
export class OrderService {
constructor(private readonly eventEmitter: TransactionalEventEmitter) {}
async createOrder(data: CreateOrderDto) {
const order = new Order(data);
// Event and entity are persisted atomically
await this.eventEmitter.emit(
new OrderCreatedEvent(order.id, data.customerId),
[{ entity: order, operation: TransactionalEventEmitterOperations.persist }]
);
}
}import { OutboxModule } from '@fullstackhouse/nestjs-outbox';
import {
TypeORMDatabaseDriverFactory,
TypeOrmOutboxTransportEvent,
OutboxTransportEventMigrations,
} from '@fullstackhouse/nestjs-outbox-typeorm-driver';
@Module({
imports: [
TypeOrmModule.forRoot({
// ... your config
entities: [TypeOrmOutboxTransportEvent],
migrations: [...OutboxTransportEventMigrations],
migrationsRun: true,
}),
OutboxModule.registerAsync({
imports: [TypeOrmModule.forFeature([TypeOrmOutboxTransportEvent])],
useFactory: (dataSource: DataSource) => ({
driverFactory: new TypeORMDatabaseDriverFactory(dataSource),
events: [
{
name: OrderCreatedEvent.name,
listeners: {
expiresAtTTL: 1000 * 60 * 60 * 24, // 24 hours
maxExecutionTimeTTL: 1000 * 15, // 15 seconds
readyToRetryAfterTTL: 10000, // 10 seconds
},
},
],
retryEveryMilliseconds: 30_000,
maxOutboxTransportEventPerRetry: 10,
}),
inject: [DataSource],
}),
],
})
export class AppModule {}| Option | Description |
|---|---|
name |
Event class name |
listeners.expiresAtTTL |
How long events are retained and retried (ms) |
listeners.maxExecutionTimeTTL |
Max listener execution time before retry (ms) |
listeners.readyToRetryAfterTTL |
Delay before retrying failed events (ms) |
immediateProcessing |
Process immediately (true, default) or defer to poller (false) |
| Option | Description |
|---|---|
driverFactory |
Database driver factory instance |
events |
Array of event configurations |
retryEveryMilliseconds |
Polling interval for retry mechanism |
maxOutboxTransportEventPerRetry |
Batch size per polling cycle |
isGlobal |
Register module globally (optional) |
| Method | Behavior |
|---|---|
emit() |
Persists event, attempts delivery, returns immediately |
emitAsync() |
Persists event, waits for all listeners to complete |
Note: When
immediateProcessing: false, both methods behave identically—they persist the event and return immediately. All processing happens via the poller.
Immediate (true, default) |
Deferred (false) |
|---|---|
| Lower latency | Higher latency (waits for poller) |
| Best-effort first delivery | All delivery via poller |
| Most use cases | Fire-and-forget, safer crash recovery |
| Driver | Databases | Real-time Support |
|---|---|---|
| TypeORM | PostgreSQL, MySQL | Polling only |
| MikroORM | PostgreSQL, MySQL | PostgreSQL LISTEN/NOTIFY |
LISTEN/NOTIFY is enabled by default when using the MikroORM driver with PostgreSQL. Just use MikroORMDatabaseDriverFactory and the module handles everything automatically:
import { MikroORM } from '@mikro-orm/core';
import { OutboxModule } from '@fullstackhouse/nestjs-outbox';
import { MikroORMDatabaseDriverFactory } from '@fullstackhouse/nestjs-outbox-mikro-orm-driver';
@Module({
imports: [
OutboxModule.registerAsync({
imports: [MikroOrmModule.forFeature([MikroOrmOutboxTransportEvent])],
useFactory: (orm: MikroORM) => ({
driverFactory: new MikroORMDatabaseDriverFactory(orm),
events: [/* ... */],
retryEveryMilliseconds: 30_000,
maxOutboxTransportEventPerRetry: 10,
}),
inject: [MikroORM],
}),
],
})
export class AppModule {}To disable LISTEN/NOTIFY and use polling only:
new MikroORMDatabaseDriverFactory(orm, { listenNotify: { enabled: false } })The PostgreSQLEventListener:
- Uses PostgreSQL triggers to send notifications on event insert
- Automatically reconnects on connection failures (configurable delay, default 5s)
- Works alongside polling as a fallback mechanism
- Requires the LISTEN/NOTIFY migration from
OutboxMigrations
The module automatically handles graceful shutdown:
- Tracks in-flight event processing
- Waits for all pending events to complete before shutdown
- Properly disconnects event listeners
- Prevents new event processing during shutdown
To support additional ORMs or databases:
- Implement
DatabaseDriver- Handle transactions, pessimistic locking, and persist/flush operations - Implement
DatabaseDriverFactory- Factory to instantiate your driver - Create a persistable model - Implement
OutboxTransportEventinterface - Add migrations - Create the
outbox_transport_eventtable
See existing drivers in packages/ for reference. Contributions welcome via PR.
Publishing to npm is automated via GitHub releases using npm trusted publishers with OIDC—no npm tokens required.
Create a new release with a tag like v2.1.0 and the workflow will:
- Extract version from the tag (strips
vprefix) - Update all package versions
- Build and publish to npm with provenance attestations
MIT
