better-effect
Outbox

Outbox patterns

Choose transaction boundaries, routes, and recovery policies for reliable publishing.

Use an outbox when a domain write and the job that follows it must become visible together. For an order confirmation, a direct saveOrder(); SendConfirmation.enqueue() can lose the confirmation if the process crashes between writes, while enqueueing before saving can leave a confirmation for an order that rolled back. Prepare the typed request and OutboxRecord first, then call the database adapter's transaction helper. The adapter runs only your domain callback, appends the record automatically, owns the transaction lifecycle, and lets the publisher enqueue it after commit.

The safe handoff

domain transaction
  ├─ write application data
  └─ append prepared OutboxRecord
        │ commit

OutboxPublisher → JobStore → Worker.handle

The publisher is at-least-once. Give the confirmation request a deterministic Job ID or idempotency key, and use that same key when calling the email provider or writing a sent-message record. If a crash happens after the JobStore accepts the request but before outbox settlement, the publisher can enqueue it again; the stable key makes that retry converge. The remote email is still outside the database transaction. The Outbox guide covers record preparation, routes, and the PostgreSQL example; each adapter guide documents its own transaction helper and client-ownership options.

Use a Zod 4 schema through better-effect-schema and Codec.standardSchema for the SendConfirmation payload before creating the outbox record. The trusted JSON escape hatch is for values that are already trusted JSON, not the default boundary codec.

Advanced: caller-owned transactions

Low-level appendIn APIs are escape hatches for code that already owns a native transaction; they are not part of the normal record-first guide.

On this page