7.5 KiB
name, description, allowed-tools
| name | description | allowed-tools | |||
|---|---|---|---|---|---|
| using-interactive-transactions | Use interactive transactions with $transaction callback for atomic operations and automatic rollback. Use when operations must succeed or fail together. |
|
Interactive Transactions
Use the $transaction callback API for operations that must succeed or fail atomically. Interactive transactions provide automatic rollback on errors and allow complex multi-step logic.
When to Use
Use interactive transactions when:
- Multiple operations must all succeed or all fail
- Operations depend on results from previous operations
- Complex business logic requires atomic execution
- Implementing financial transfers, inventory management, or state changes
Do NOT use for:
- Single operations (no transaction needed)
- Read-only operations (use batch queries instead)
- Independent operations that can fail separately
$transaction Callback Pattern
await prisma.$transaction(async (tx) => {
const result1 = await tx.model1.create({ data: { ... } });
const result2 = await tx.model2.update({
where: { id: result1.relatedId },
data: { ... }
});
return { result1, result2 };
});
All operations use the tx client. If any operation throws, the entire transaction rolls back automatically.
Banking Transfer Example
async function transferMoney(fromId: string, toId: string, amount: number) {
return await prisma.$transaction(async (tx) => {
const fromAccount = await tx.account.findUnique({
where: { id: fromId }
});
if (!fromAccount || fromAccount.balance < amount) {
throw new Error('Insufficient funds');
}
const updatedFrom = await tx.account.update({
where: { id: fromId },
data: { balance: { decrement: amount } }
});
const updatedTo = await tx.account.update({
where: { id: toId },
data: { balance: { increment: amount } }
});
const transfer = await tx.transfer.create({
data: {
fromAccountId: fromId,
toAccountId: toId,
amount
}
});
return { updatedFrom, updatedTo, transfer };
});
}
If any step fails, all changes roll back. Both accounts and the transfer record are consistent.
Inventory Reservation Pattern
async function reserveInventory(orderId: string, items: Array<{ productId: string; quantity: number }>) {
return await prisma.$transaction(async (tx) => {
const order = await tx.order.update({
where: { id: orderId },
data: { status: 'PROCESSING' }
});
for (const item of items) {
const product = await tx.product.findUnique({
where: { id: item.productId }
});
if (!product || product.stock < item.quantity) {
throw new Error(`Insufficient stock for product ${item.productId}`);
}
await tx.product.update({
where: { id: item.productId },
data: { stock: { decrement: item.quantity } }
});
await tx.orderItem.create({
data: {
orderId,
productId: item.productId,
quantity: item.quantity,
price: product.price
}
});
}
return await tx.order.update({
where: { id: orderId },
data: { status: 'RESERVED' }
});
});
}
If stock is insufficient for any item, the entire reservation rolls back. No partial inventory deductions occur.
Multi-Step Atomic Operations
async function createUserWithProfile(userData: UserData, profileData: ProfileData) {
return await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: {
email: userData.email,
name: userData.name
}
});
const profile = await tx.profile.create({
data: {
userId: user.id,
bio: profileData.bio,
avatar: profileData.avatar
}
});
await tx.notification.create({
data: {
userId: user.id,
message: 'Welcome to our platform!'
}
});
return { user, profile };
});
}
User, profile, and notification are created atomically. If profile creation fails, the user is not created.
Error Handling and Rollback
try {
const result = await prisma.$transaction(async (tx) => {
const step1 = await tx.model.create({ data: { ... } });
if (someCondition) {
throw new Error('Business rule violation');
}
const step2 = await tx.model.update({ ... });
return { step1, step2 };
});
} catch (error) {
console.error('Transaction failed, all changes rolled back:', error);
}
Any thrown error triggers automatic rollback. No manual cleanup needed.
Transaction Timeout
await prisma.$transaction(async (tx) => {
}, {
timeout: 10000
});
Default timeout is 5 seconds. Increase for long-running transactions.
Isolation Level
await prisma.$transaction(async (tx) => {
}, {
isolationLevel: 'Serializable'
});
Available levels: ReadUncommitted, ReadCommitted, RepeatableRead, Serializable. Default is database-specific.
Common Patterns
Conditional Rollback:
await prisma.$transaction(async (tx) => {
const record = await tx.model.create({ data: { ... } });
if (!isValid(record)) {
throw new Error('Validation failed');
}
return record;
});
Dependent Operations:
await prisma.$transaction(async (tx) => {
const parent = await tx.parent.create({ data: { ... } });
const child = await tx.child.create({
data: { parentId: parent.id, ... }
});
return { parent, child };
});
Batch with Validation:
await prisma.$transaction(async (tx) => {
const records = await Promise.all(
items.map(item => tx.model.create({ data: item }))
);
if (records.length !== items.length) {
throw new Error('Not all records created');
}
return records;
});
Anti-Patterns
Mixing transaction and non-transaction calls:
await prisma.$transaction(async (tx) => {
await tx.model.create({ data: { ... } });
await prisma.model.create({ data: { ... } });
});
Use tx for ALL operations inside the transaction.
Long-running operations:
await prisma.$transaction(async (tx) => {
await tx.model.create({ data: { ... } });
await fetch('https://api.example.com');
await tx.model.update({ ... });
});
Keep external API calls outside transactions. Transactions hold database locks.
Catching errors inside transaction:
await prisma.$transaction(async (tx) => {
try {
await tx.model.create({ data: { ... } });
} catch (error) {
}
await tx.model.update({ ... });
});
Let errors propagate to trigger rollback. Handle errors outside the transaction.
Implementation Steps
- Identify atomic operations that must succeed or fail together
- Wrap in $transaction callback with
async (tx) => { ... } - Use tx client for all operations inside the callback
- Validate before operations to fail fast
- Throw errors to trigger rollback on business rule violations
- Return result from the callback for success case
- Handle errors outside the transaction for logging/recovery
Verification
After implementing interactive transactions:
- Test successful execution path
- Test rollback on database errors
- Test rollback on business rule violations
- Verify no partial state changes after failures
- Check transaction timeout for long operations
- Validate isolation level for concurrent access
Interactive transactions ensure data consistency through automatic rollback and atomic execution.