From a gap analysis against NestJS and AdonisJS.
There is no first-party answer to "these three repository calls commit or roll back together". examples/order-infrastructure hand-rolls it, and examples/order-amqp-worker's outbox relay already depends on the property without a framework primitive backing it — a placement and its outbox row must land in one transaction or the outbox pattern is decorative.
This is the highest-value gap because every non-trivial application needs it and retrofitting a transaction boundary after adapters exist is expensive.
The primitives are already here. A transaction is a unit-scoped resource, and the kernel already has all three parts:
- a unit per request / delivery / activity attempt, with an ambient
UnitRecord (packages/core/src/units.ts);
StartOptions.unit plus di's Module.forkScope, which build a per-unit fork of the graph;
- resourceful providers (
acquire/release), which di already guarantees are closed on every exit path including startup failure.
So the shape is likely a Transaction port provided by a resourceful, unit-scoped provider: acquire opens, release commits or rolls back on the unit's outcome. What does not exist is the package, the decision about who decides commit-vs-rollback (the unit's Result? an explicit call?), and how a repository adapter reaches the ambient transaction without becoming the hidden-dependency thesis #2 exists to prevent — note thesis #2 already lists "a database adapter" as a legitimate currentUnit() reader, which is the opening.
Adjacent and deliberately not folded in here: this repo has no first-party migrations opinion either. Prisma appears in an example; nothing states a stance.
Acceptance
- A decision recorded in
packages/core/CLAUDE.md or a new package's own: what owns the transaction boundary, and how an adapter reaches it.
- Commit/rollback is driven by the unit's outcome rather than by every handler remembering, or the reason it cannot be is written down.
- Nested/joined transactions have a stated answer, even if it is "not supported".
examples/order-infrastructure stops hand-rolling it, and order-amqp-worker's outbox write becomes genuinely atomic with the placement.
From a gap analysis against NestJS and AdonisJS.
There is no first-party answer to "these three repository calls commit or roll back together".
examples/order-infrastructurehand-rolls it, andexamples/order-amqp-worker's outbox relay already depends on the property without a framework primitive backing it — a placement and its outbox row must land in one transaction or the outbox pattern is decorative.This is the highest-value gap because every non-trivial application needs it and retrofitting a transaction boundary after adapters exist is expensive.
The primitives are already here. A transaction is a unit-scoped resource, and the kernel already has all three parts:
UnitRecord(packages/core/src/units.ts);StartOptions.unitplus di'sModule.forkScope, which build a per-unit fork of the graph;acquire/release), which di already guarantees are closed on every exit path including startup failure.So the shape is likely a
Transactionport provided by a resourceful, unit-scoped provider:acquireopens,releasecommits or rolls back on the unit's outcome. What does not exist is the package, the decision about who decides commit-vs-rollback (the unit'sResult? an explicit call?), and how a repository adapter reaches the ambient transaction without becoming the hidden-dependency thesis #2 exists to prevent — note thesis #2 already lists "a database adapter" as a legitimatecurrentUnit()reader, which is the opening.Adjacent and deliberately not folded in here: this repo has no first-party migrations opinion either. Prisma appears in an example; nothing states a stance.
Acceptance
packages/core/CLAUDE.mdor a new package's own: what owns the transaction boundary, and how an adapter reaches it.examples/order-infrastructurestops hand-rolling it, andorder-amqp-worker's outbox write becomes genuinely atomic with the placement.