Transaction Script design pattern is a simple pattern that tell your program / function to do one thing, and one thing only. They usually work directly with databases and handle all the details for a particular operation.

Usually, every operation will be created as a separate function that will do an operation, including database operation.

Benefits of using it

  • Separates business logic from data access
  • Allows easily change business rules without modifying data structures
  • Easy to test and to mock database interaction
  • Quite often aligns with how business domains have been described

When should you use it

  • You want to have a flexibility of how you structure your calls?

When should you avoid it

  • If you have lots of interrelated business logic
  • If you repeat the same logic across multiple functions
  • System changes very often
  • If you like core reusability (it adds duplication)

Example

import { PrismaClient } from '@prisma/client';  
  
const prisma = new PrismaClient();  
  
// Transaction Scripts  
  
export async function transferFunds(fromAccountId: number, toAccountId: number, amount: number) {  
  const fromAccount = await prisma.account.findUnique({ where: { id: fromAccountId } });  
  const toAccount = await prisma.account.findUnique({ where: { id: toAccountId } });  
  
  if (!fromAccount || !toAccount) {  
    throw new Error('One or both accounts not found');  
  }  
  
  if (fromAccount.balance < amount) {  
    throw new Error('Insufficient funds');  
  }  
  
  // Start a database transaction  
  await prisma.$transaction(async (prisma) => {  
    // Deduct from source account  
    await prisma.account.update({  
      where: { id: fromAccountId },  
      data: { balance: { decrement: amount } },  
    });  
  
    // Add to destination account  
    await prisma.account.update({  
      where: { id: toAccountId },  
      data: { balance: { increment: amount } },  
    });  
  
    // Log the transaction  
    await prisma.transaction.create({  
      data: {  
        fromAccountId,  
        toAccountId,  
        amount,  
        type: 'TRANSFER',  
      },  
    });  
  });  
}  
  
export async function applyInterest(accountId: number) {  
  const account = await prisma.account.findUnique({ where: { id: accountId } });  
  if (!account) throw new Error('Account not found');  
  
  let interestRate: number;  
  switch (account.type) {  
    case 'SAVINGS':  
      interestRate = 0.02; // 2% for savings  
      break;  
    case 'CHECKING':  
      interestRate = 0.005; // 0.5% for checking  
      break;  
    default:  
      throw new Error('Unknown account type');  
  }  
  
  const interestAmount = account.balance * interestRate;  
  
  await prisma.$transaction(async (prisma) => {  
    await prisma.account.update({  
      where: { id: accountId },  
      data: { balance: { increment: interestAmount } },  
    });  
  
    await prisma.transaction.create({  
      data: {  
        toAccountId: accountId,  
        amount: interestAmount,  
        type: 'INTEREST',  
      },  
    });  
  });  
}  
  
export async function generateMonthlyStatement(accountId: number, month: number, year: number) {  
  const account = await prisma.account.findUnique({ where: { id: accountId } });  
  if (!account) throw new Error('Account not found');  
  
  const startDate = new Date(year, month - 1, 1);  
  const endDate = new Date(year, month, 0);  
  
  const transactions = await prisma.transaction.findMany({  
    where: {  
      OR: [  
        { fromAccountId: accountId },  
        { toAccountId: accountId },  
      ],  
      createdAt: {  
        gte: startDate,  
        lte: endDate,  
      },  
    },  
    orderBy: { createdAt: 'asc' },  
  });  
  
  let balance = account.balance;  
  const statement = transactions.map(t => {  
    if (t.fromAccountId === accountId) {  
      balance -= t.amount;  
      return { ...t, balance, type: 'DEBIT' };  
    } else {  
      balance += t.amount;  
      return { ...t, balance, type: 'CREDIT' };  
    }  
  }).reverse(); // Most recent first  
  
  return {  
    accountId,  
    accountNumber: account.accountNumber,  
    month,  
    year,  
    openingBalance: statement.length > 0 ? statement[statement.length - 1].balance : account.balance,  
    closingBalance: account.balance,  
    transactions: statement,  
  };  
}