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#
- Keep transactions short - Long transactions block other operations
- Handle errors properly - Always catch and handle transaction failures
- Use appropriate isolation levels - Balance consistency vs. performance
- Avoid external API calls - Keep transactions database-only
- Test rollback scenarios - Ensure failed transactions clean up properly
Related Patterns#
- Prisma - Prisma setup and basics
- Soft Delete - Soft delete with transactions
- Pooling - Connection management