Where a Pulsar transaction belongs
A UseCase owns the transaction boundary. A UseCase opens the transaction for its mutation workflow; Operations and Actions participate without owning one. A read-only UseCase may call Queries without a transaction, and returned values remain delivery-neutral.
Choose a transaction boundary
Only a UseCase opens DB::transaction(). Controllers, Jobs, Commands, Listeners, Operations, Actions, and Queries never begin, commit, or roll back one. A mutation UseCase opens a boundary when the workflow requires atomicity; a read-only UseCase does not require one.
Context: source-backed first-feature shape — The UseCase encloses the Action and returns the completed Domain value.
return DB::transaction(function () use ($data) {
return $createOrderAction->execute($data);
});Participation and failure
Operations and Actions participate by running inside the UseCase closure. They never create nested transaction ownership. A thrown exception leaves the closure, lets Laravel roll back, and has no success return or post-success Event. On success, the UseCase returns after the boundary so the Adapter can shape delivery.
| Workflow | Transaction rule | Return rule |
|---|---|---|
| Mutation UseCase | Open one boundary when atomicity is required. | Return a delivery-neutral result after success. |
| Read-only UseCase | No transaction requirement; may call one or more Queries. | Return collection, DTO, Value Object, primitive, array, or void. |
| Operation or Action | Participate in the calling UseCase boundary. | Return only to the workflow caller. |
Return after the workflow
Resources, Inertia wrappers, redirects, response objects, CLI exits, and scheduler timing are Adapter work. A UseCase must not return them. This separation gives a Job, Command, and Controller the same workflow result while retaining their own delivery responsibilities.
Transform transaction-in-Action
Context: ❌ Prohibited Action transaction — An Action must not create its own boundary.
return DB::transaction(fn () => $model->save());Context: ✅ Correct UseCase transaction — The UseCase owns the boundary and calls the Action inside it.
return DB::transaction(fn () => $action->execute($data));