Database Transactions

Patterns for handling database transactions with Prisma.

Overview#

Transactions ensure data integrity:

  • Atomic operations (all or nothing)
  • Consistent state changes
  • Isolated concurrent access
  • Durable commits

Prerequisites:

  • Prisma ORM setup
  • PostgreSQL, MySQL, or compatible database

Implementation#

Basic Transaction#

1// lib/db.ts 2import { prisma } from '@/lib/db' 3 4export async function transferCredits( 5 fromUserId: string, 6 toUserId: string, 7 amount: number 8) { 9 return prisma.$transaction(async (tx) => { 10 // Deduct from sender 11 const sender = await tx.user.update({ 12 where: { id: fromUserId }, 13 data: { credits: { decrement: amount } } 14 }) 15 16 if (sender.credits < 0) { 17 throw new Error('Insufficient credits') 18 } 19 20 // Add to recipient 21 await tx.user.update({ 22 where: { id: toUserId }, 23 data: { credits: { increment: amount } } 24 }) 25 26 // Record transaction 27 return tx.transaction.create({ 28 data: { 29 fromUserId, 30 toUserId, 31 amount, 32 type: 'TRANSFER' 33 } 34 }) 35 }) 36}

Sequential Operations#

1// lib/db.ts 2export async function createOrder( 3 userId: string, 4 items: { productId: string; quantity: number }[] 5) { 6 return prisma.$transaction(async (tx) => { 7 // 1. Check inventory 8 for (const item of items) { 9 const product = await tx.product.findUnique({ 10 where: { id: item.productId } 11 }) 12 13 if (!product || product.stock < item.quantity) { 14 throw new Error(`Insufficient stock for ${item.productId}`) 15 } 16 } 17 18 // 2. Create order 19 const order = await tx.order.create({ 20 data: { 21 userId, 22 status: 'PENDING', 23 items: { 24 create: items.map((item) => ({ 25 productId: item.productId, 26 quantity: item.quantity 27 })) 28 } 29 } 30 }) 31 32 // 3. Decrement inventory 33 for (const item of items) { 34 await tx.product.update({ 35 where: { id: item.productId }, 36 data: { stock: { decrement: item.quantity } } 37 }) 38 } 39 40 return order 41 }) 42}

Transaction Options#

1// lib/db.ts 2export async function riskyOperation() { 3 return prisma.$transaction( 4 async (tx) => { 5 // Long-running operation 6 await tx.heavyComputation.create({ /* ... */ }) 7 }, 8 { 9 maxWait: 5000, // Wait up to 5s for transaction slot 10 timeout: 10000, // Allow up to 10s for transaction 11 isolationLevel: 'Serializable' // Strictest isolation 12 } 13 ) 14}

Batch Operations#

1// lib/db.ts 2export async function batchUpdate(updates: { id: string; data: any }[]) { 3 // Use $transaction for batching multiple operations 4 return prisma.$transaction( 5 updates.map((update) => 6 prisma.item.update({ 7 where: { id: update.id }, 8 data: update.data 9 }) 10 ) 11 ) 12}

Optimistic Locking#

1// lib/db.ts 2export async function updateWithVersion( 3 id: string, 4 expectedVersion: number, 5 data: any 6) { 7 const result = await prisma.document.updateMany({ 8 where: { 9 id, 10 version: expectedVersion 11 }, 12 data: { 13 ...data, 14 version: { increment: 1 } 15 } 16 }) 17 18 if (result.count === 0) { 19 throw new Error('Concurrent modification detected') 20 } 21 22 return prisma.document.findUnique({ where: { id } }) 23}

Nested Transactions#

1// lib/db.ts 2export async function complexOperation(userId: string) { 3 return prisma.$transaction(async (tx) => { 4 // Create main record 5 const order = await tx.order.create({ 6 data: { userId, status: 'PENDING' } 7 }) 8 9 // Call another function that uses the same transaction 10 await processOrderItems(tx, order.id, items) 11 12 // Update user stats 13 await tx.user.update({ 14 where: { id: userId }, 15 data: { orderCount: { increment: 1 } } 16 }) 17 18 return order 19 }) 20} 21 22// Helper that accepts transaction client 23async function processOrderItems( 24 tx: Prisma.TransactionClient, 25 orderId: string, 26 items: OrderItem[] 27) { 28 for (const item of items) { 29 await tx.orderItem.create({ 30 data: { 31 orderId, 32 productId: item.productId, 33 quantity: item.quantity 34 } 35 }) 36 } 37}

Error Handling#

1// lib/db.ts 2import { Prisma } from '@prisma/client' 3 4export async function safeTransaction<T>( 5 operation: (tx: Prisma.TransactionClient) => Promise<T> 6): Promise<{ success: true; data: T } | { success: false; error: string }> { 7 try { 8 const data = await prisma.$transaction(operation) 9 return { success: true, data } 10 } catch (error) { 11 if (error instanceof Prisma.PrismaClientKnownRequestError) { 12 // Handle specific error codes 13 switch (error.code) { 14 case 'P2002': 15 return { success: false, error: 'Duplicate entry' } 16 case 'P2003': 17 return { success: false, error: 'Foreign key constraint failed' } 18 case 'P2025': 19 return { success: false, error: 'Record not found' } 20 default: 21 return { success: false, error: 'Database error' } 22 } 23 } 24 return { success: false, error: 'Transaction failed' } 25 } 26}

Usage Examples#

Server Action with Transaction#

1// actions/order.ts 2'use server' 3 4import { createOrder } from '@/lib/db' 5import { auth } from '@/auth' 6import { revalidatePath } from 'next/cache' 7 8export async function placeOrder(items: { productId: string; quantity: number }[]) { 9 const session = await auth() 10 if (!session?.user) throw new Error('Unauthorized') 11 12 try { 13 const order = await createOrder(session.user.id, items) 14 revalidatePath('/orders') 15 return { success: true, orderId: order.id } 16 } catch (error) { 17 return { success: false, error: error.message } 18 } 19}

API Route with Transaction#

1// app/api/transfer/route.ts 2import { NextResponse } from 'next/server' 3import { transferCredits } from '@/lib/db' 4 5export async function POST(request: Request) { 6 const { fromUserId, toUserId, amount } = await request.json() 7 8 try { 9 const transaction = await transferCredits(fromUserId, toUserId, amount) 10 return NextResponse.json({ success: true, transaction }) 11 } catch (error) { 12 return NextResponse.json( 13 { success: false, error: error.message }, 14 { status: 400 } 15 ) 16 } 17}

Best Practices#

  1. Keep transactions short - Long transactions block other operations
  2. Handle errors properly - Always catch and handle transaction failures
  3. Use appropriate isolation levels - Balance consistency vs. performance
  4. Avoid external API calls - Keep transactions database-only
  5. Test rollback scenarios - Ensure failed transactions clean up properly