@nathapp/nestjs-outbox
v3.3.0
Published
Transactional outbox for NestJS: record domain events inside the caller's DB transaction, then relay them at-least-once to a broker. Zero runtime dependencies
Readme
@nathapp/nestjs-outbox
Transactional outbox for NestJS. Record domain events into an outbox store inside the caller's existing database transaction — the same transaction that persists the business change — then let a background relay publish them at-least-once to a message broker. No dual-write inconsistency.
Zero runtime dependencies. Pure seams: you supply the store (a transactional
Prisma/SQL table) and the publisher (e.g. @nathapp/nestjs-queue); the package
owns the write API, the relay engine, and the contracts.
Why
A service that writes to its database and then publishes to a broker has two independent operations. If the process dies between them, the state and the event diverge. The outbox pattern removes the second write from the critical path: the event is persisted atomically with the business change, and a separate relay delivers it later, retrying until it succeeds.
Design at a glance
txManager.run(async () => {
await businessRepo.create(...) // business change
await outbox.record({ type, payload }) // outbox row, SAME transaction
}) // commit => both durable, or neither
OutboxRelay (every pollIntervalMs, or driven from your own cron)
claimBatch(batchSize, leaseMs) -> publish -> markPublished | markRetry(backoff) | markDead- Write path enrolls in the active transaction via
ITransactionManager(@nathapp/nestjs-data) and is fail-closed: if the event cannot be recorded, the business transaction rolls back. - Relay is best-effort background work: it claims records under a lease
(crash-safe), publishes each via
IOutboxPublisher, and on repeated failure applies exponential backoff, giving up to a dead-letter state aftermaxAttempts. Cycle errors are logged, never fatal. - At-least-once delivery: pair the consumer with
@nathapp/nestjs-idempotencyto dedup.
Seams you implement
| Seam | Bind to | Responsibility |
| --- | --- | --- |
| IOutboxStore | a transactional table (Prisma/SQL) | save(record, client), claimBatch, markPublished, markRetry, markDead |
| IOutboxPublisher | your broker (e.g. QueueService) | publish(record) |
An InMemoryOutboxStore is provided for tests and demos only — it is not
transactional and must not be used in production.
