Durable Work in PostgresPart 11

How this fits with DDD, Event Sourcing, CQRS, and sagas

How Postgres durable work fits with DDD, domain events, transactional outbox, event sourcing, CQRS projections, sagas, CDC, and workflow engines.

The Postgres queue is not an architecture by itself. It is infrastructure you can use inside several established enterprise patterns.

The clean way to fit it in is to ask one question: which model owns the truth? DDD aggregates own business invariants, event stores own replayable history, projections own read models, outboxes own publish intent, and workflow engines own durable progress.

Start with the source of truth

Most confusion comes from using pattern names as synonyms. They are not synonyms. They draw boundaries around different facts.

PatternOwns this truthWhere Postgres durable work fitsDo not use it as
DDD aggregateCurrent business invariantsWrite inbox/outbox rows in the same transaction as aggregate changesA background job runner
Domain eventSomething meaningful happened in the domainPersist event or outbox intent after aggregate decisionA raw table-change notification
Transactional outboxPublish intent after a local commitRelay rows with the same claim/lease loopThe system of record for history
Event sourcingReplayable event historyUse workers for projections, subscriptions, and catch-up jobsA queue table with completed rows deleted
CQRS projectionDerived read modelClaim and apply events idempotently with checkpointsThe write model
Saga / process managerProgress across local transactionsUse workflow instance and step rows for durable progressA distributed transaction
CDCObserved physical row changesUse when you cannot add outbox writes to the appA domain event contract
Domain events, projections, and saga steps

DDD decides what changed; durable work decides what happens later

In a DDD-style service, aggregates protect invariants inside a transaction. The durable-work table should not decide whether an order can be paid. It should record follow-up work after the aggregate decision is made.

BEGIN;

-- Aggregate decision: enforce business invariant
UPDATE orders
SET status = 'paid'
WHERE id = 9182
  AND status = 'pending_payment';

-- Durable work created by that decision
INSERT INTO inbox (partition_key, payload, idempotency_key)
VALUES (
  'order:9182',
  '{"type":"send_receipt","order_id":9182}'::jsonb,
  'send_receipt:9182'
);

COMMIT;

Domain events are meaning, not delivery

A domain event such as OrderPaid is a fact in the language of the business. The outbox is a delivery mechanism for that fact. Keeping those separate prevents two common mistakes: treating table changes as domain events, and treating the outbox as permanent event history.

ConceptExampleDurability expectation
Domain eventOrderPaidBusiness fact. Stable meaning.
Outbox rowPublish OrderPaid to transportDelivery intent. May be archived after publish.
Broker messageSerialized event on a topicTransport record. Retention depends on platform policy.
Projection checkpointSearch indexed through event 381Consumer progress. Rebuildable from source history.

Event sourcing changes the source of truth

With event sourcing, the event stream is the write-side source of truth. The Postgres claim loop still helps, but the durable row is usually a projection task, subscription checkpoint, or catch-up batch. It is not the event store unless you deliberately build an event store.

LayerOwnsPostgres durable-work role
Event storeAppend-only historySource stream for subscriptions
Command modelValid decisionsAppends events after invariant checks
Projection workerRead-model updatesClaims events or batches, applies idempotently, saves checkpoint
Read modelQuery shapeDerived table, cache, index, or search document

Sagas and workflows coordinate progress, not consistency

A saga or workflow coordinates a process across local transactions. It does not make those transactions atomic together. Each step commits locally, emits intent for the next step, and uses compensation when a later step cannot complete.

NeedUsePostgres implementation
Short choreography across servicesSaga eventsOutbox row per local commit, idempotent consumers
Service-local multi-step processDurable workflowWorkflow instance + step rows
Long human approval processWorkflow engineDedicated platform, UI, timers, search, and history tooling
Undo after partial successCompensationExplicit compensating step or event

CDC is integration infrastructure, not a domain model

Change Data Capture is useful when you cannot change the application write path. It observes row changes from the WAL and turns them into a stream. That is powerful, but it is not the same as a domain event. A CDC consumer sees orders.status changed; a domain event says OrderPaid.

Use CDC whenPrefer outbox when
The app is legacy or owned by another teamYou control the write path
Consumers need physical row changesConsumers need business events
You can tolerate schema couplingYou want a stable event contract
You need broad replication into analytics/searchYou need precise publish intent per domain action

Choose the pattern by the question you are answering

QuestionPattern that answers itDurable-work table role
Can this command change the business state?DDD aggregate / command modelNone until after the decision
How do I publish after commit?Transactional outboxRelay queue
How do I update a read model?CQRS projectionProjection work and checkpointing
How do I preserve replayable history?Event sourcingSubscription and projection workers
How do I coordinate multiple local commits?Saga / workflowDurable process state and step scheduling
How do I integrate a DB I cannot change?CDCConsumer work generated from WAL changes