Transaction Service Pattern
Wrap multi-step writes in transactions to guarantee atomicity. Keep transactions short.
Pattern
// Pseudocode — transfer money between accounts
function transferFunds(fromId, toId, amount):
transaction = db.beginTransaction()
try:
fromAccount = transaction.query(
"SELECT * FROM accounts WHERE id = :fromId FOR UPDATE", fromId
)
toAccount = transaction.query(
"SELECT * FROM accounts WHERE id = :toId FOR UPDATE", toId
)
if fromAccount.balance < amount:
throw InsufficientFundsError
transaction.execute(
"UPDATE accounts SET balance = balance - :amount WHERE id = :fromId",
{ amount, fromId }
)
transaction.execute(
"UPDATE accounts SET balance = balance + :amount WHERE id = :toId",
{ amount, toId }
)
transaction.execute(
"INSERT INTO transfers (from_id, to_id, amount, created_at) VALUES (:fromId, :toId, :amount, NOW())",
{ fromId, toId, amount }
)
transaction.commit()
return { success: true }
catch error:
transaction.rollback()
throw errorRules
- Lock rows in consistent order to prevent deadlocks (always lock lower ID first).
- No network calls inside transactions (API calls, emails, queue publishes).
- Keep transactions under 100ms when possible.
- Use
FOR UPDATEfor pessimistic locking on concurrent resources. - Side effects (emails, webhooks) go after commit, not inside transaction.