Transactions
Use executeSqlTransaction() to span multiple service calls in one atomic unit.
For operations that span multiple services (or multiple calls to the same service), the package exposes executeSqlTransaction() for explicit transaction control.
The helper
const = new ({} as any);
.(new ({} as any));executeSqlTransaction opens a transaction on the configured database, runs the callback, commits on success, and rolls back on any thrown error.
Passing { transaction: tx } to methods
Every CRUD method accepts an optional last argument of SqlOperationOptions. Pass { transaction: tx } to enlist the call in an existing transaction:
const tx = await users.executeSqlTransaction(async (tx) => {
// both writes share the same transaction
const user = await users.create({ name: 'Ada' }, { transaction: tx });
const profile = await profiles.create({ userId: user.id }, { transaction: tx });
return { user, profile };
});The tx handle is a Drizzle transaction object. It's typed as any in
SqlOperationOptions so the package doesn't depend on a specific
Drizzle transaction type. Pass it through to other Drizzle calls if you
need to drop down to raw queries inside the transaction.
Mixing package methods with raw Drizzle
import { eq } from 'drizzle-orm';
import { posts, users } from './db/schema';
await users.executeSqlTransaction(async (tx) => {
const user = await users.create({ name: 'Ada' }, { transaction: tx });
// Raw Drizzle inside the same transaction:
await tx.insert(posts).values({ authorId: user.id, title: 'Hello' });
});The tx is the standard Drizzle transaction handle — anything you can do with db you can do with tx, and it'll be enlisted in the same transaction.
Nesting
executeSqlTransaction can be nested in code, but each call opens its own savepoint. If an inner callback throws, the outer transaction is unaffected. For most use cases, prefer a single top-level executeSqlTransaction per request.
Failure modes
| Failure | Behavior |
|---|---|
| The callback throws | The transaction is rolled back. The error propagates to the caller. |
A nested massCreate throws BulkOperationException | The transaction is rolled back. The exception propagates. |
| The connection drops mid-transaction | The database rolls back. The package surfaces the underlying error. |
When to use transactions
- Cross-service atomicity — creating a
Userand aProfilein one atomic unit. - Read-modify-write consistency — read a row, decide based on its value, write back, all in one transaction.
- Bulk operations that need external side effects in the same atomic unit — see Bulk operations.
When not to use transactions
- Read-only queries —
findAll,count,findOnedon't need a transaction. - Independent writes — if the operations can fail or succeed independently, separate transactions are usually better (smaller blast radius on failure).
- Long-running operations — transactions hold locks. Don't include network calls, queue publishes, or unbounded
awaits inside a transaction.