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,
};
}