Transactions
Business writes that were seen calling prisma.$transaction include order command, marketplace ingestion, stock command, fulfillment workflow, returns, goods receipt, stock transfer, stock count, packing manifest, NDR, shipments, wallet ledger, settlement loader, and the outbox claim.
Rules that follow from that
| Write | Atomic with |
|---|---|
Aggregate plus outboxWriter.enqueue(tx, ...) | The outbox row |
Aggregate plus eventEmitter.emit | Nothing. The emit is after or outside the transaction semantics of the database |
Courier ship() and the local shipment row | Not the same transaction |
Bull queue.add and the business row | Only if you enqueue after commit, or accept a job for a rolled-back row if you enqueue inside the transaction before commit |
Enqueue Bull work after commit unless you have a reconciliation path. The outbox exists so side effects can commit with the row and run later.
Isolation
The isolation level passed to $transaction was not surveyed. Prisma uses the database default unless an option is set.
Outbox claim
The dispatcher claims with FOR UPDATE SKIP LOCKED. A PROCESSING lock older than 5 minutes returns to PENDING without incrementing attempts. Two workers can run the dispatcher only if you run two worker processes. Whether production runs one worker replica is unknown.
Idempotency tools inside a transaction
IdempotentEventHandlerService wraps a consumer in a transaction, locks the order when orderId is set, and inserts ProcessedEvent. It is opt-in per listener.
StockMovement uniqueness includes createdAt, so it does not by itself reject a second movement with the same idempotency key and a new timestamp.
Optimistic version columns were not surveyed across all models.