A Deep Dive into Financial Transaction Architecture
Explore the intricate details of a financial transaction architecture, including ACID compliance, secure coding practices, and payment system integration.
A Deep Dive into Financial Transaction Architecture
I recently implemented a robust wallet-to-wallet transfer system for our multi-service platform, and I'd love to share the technical approach we took to ensure security, reliability, and data consistency.
The Challenge
Enable users to instantly transfer funds between wallets while maintaining ACID compliance, preventing race conditions, and ensuring complete audit trails.
Our Solution - Key Technical Highlights
✅ Atomic Transactions
We use database transactions to ensure all-or-nothing execution. If any step fails, the entire operation rolls back—no partial transfers, no lost funds.
✅ Dual-Entry Accounting
Every transfer creates TWO transaction records:
- DEBIT entry for the sender (wallet_withdrawal)
- CREDIT entry for the recipient (wallet_deposit)
This mirrors traditional double-entry bookkeeping, providing complete financial audit trails.
✅ Pre-Flight Validation
Before touching the database, we validate:
- Transfer limits (₦100 - ₦10M)
- Wallet status (both must be active)
- Sufficient balance
- User existence
- Self-transfer prevention
✅ Real-Time Balance Updates
Using direct SQL updates with parameterized queries:
balance = balance - amount (sender)
balance = balance + amount (recipient)
✅ Zero-Fee Transfers
No platform fees—encouraging financial inclusion and user adoption.
✅ Comprehensive Notifications
Both parties receive:
- Email confirmations
- In-app push notifications
- Transaction references for support queries
✅ Unique Transaction References
Format: WT-{UUID}-{timestamp}
Enables easy tracking and dispute resolution.
Security Measures
- Input validation at multiple layers
- Authorization checks (context-based user verification)
- SQL injection prevention via parameterized queries
- Transaction isolation to prevent race conditions
Implementation
Here's the core implementation of our wallet transfer system:
static async transferBetweenWallets({
fromUserId,
toUserId,
amount
}: {
fromUserId: number,
toUserId: number,
amount: number
}) {
try {
// Validation: Amount limits
if (amount < 100) {
ThrowError('Minimum transfer amount is NGN 100.00');
}
if (amount > 10000000) {
ThrowError('Maximum transfer amount is NGN 10,000,000.00');
}
if (fromUserId === toUserId) {
ThrowError('Cannot transfer to the same wallet.');
}
// Validation: Sender wallet
const fromWallet = await this.fetchWalletDetails({ userId: fromUserId });
if (fromWallet.wallet_status !== 'active') {
ThrowError('Your wallet is not active. Please contact support.');
}
if (fromWallet.balance < amount) {
ThrowError(`Insufficient balance. Available: NGN ${fromWallet.balance.toFixed(2)}`);
}
// Validation: Recipient
const toUser = await DBObject.findOne('users', { user_id: toUserId });
if (!toUser) {
ThrowError('Recipient user not found.');
}
const toWallet = await this.fetchWalletDetails({ userId: toUserId });
if (toWallet.wallet_status !== 'active') {
ThrowError('Recipient wallet is not active.');
}
// Generate unique reference
const reference = `WT-${uuidv4().substring(0, 8)}-${Date.now()}`;
// Start database transaction
await DBObject.transaction();
try {
// Create DEBIT transaction record
const debitTransactionData = {
transaction_reference: `${reference}-DEBIT`,
user_id: fromUserId,
related_entity_type: 'wallet_topup',
related_entity_id: toUserId,
transaction_type: 'wallet_withdrawal',
amount,
fee_amount: 0.00,
net_amount: amount,
currency: 'NGN',
payment_method: 'wallet',
transaction_status: 'successful',
processed_at: DateTime.now().toSQL({ includeOffset: false }),
created_at: DateTime.now().toSQL({ includeOffset: false }),
updated_at: DateTime.now().toSQL({ includeOffset: false })
};
// Create CREDIT transaction record
const creditTransactionData = {
transaction_reference: `${reference}-CREDIT`,
user_id: toUserId,
related_entity_type: 'wallet_topup',
related_entity_id: fromUserId,
transaction_type: 'wallet_deposit',
amount,
fee_amount: 0.00,
net_amount: amount,
currency: 'NGN',
payment_method: 'wallet',
transaction_status: 'successful',
processed_at: DateTime.now().toSQL({ includeOffset: false }),
created_at: DateTime.now().toSQL({ includeOffset: false }),
updated_at: DateTime.now().toSQL({ includeOffset: false })
};
// Insert transaction records
const [debitInsertErr, debitInsert] = await catchError(
DBObject.insertOne('payment_transactions', debitTransactionData)
);
if (debitInsertErr) {
logError({
message: 'Error inserting debit transaction',
source: 'WalletService.transferBetweenWallets',
error: debitInsertErr
});
await DBObject.rollback();
throw debitInsertErr;
}
const [creditInsertErr, creditInsert] = await catchError(
DBObject.insertOne('payment_transactions', creditTransactionData)
);
if (creditInsertErr) {
logError({
message: 'Error inserting credit transaction',
source: 'WalletService.transferBetweenWallets',
error: creditInsertErr
});
await DBObject.rollback();
throw creditInsertErr;
}
// Update sender wallet balance
const [updatedErr] = await catchError(
DBObject.updateDirect(
'UPDATE user_wallets SET balance = balance - :amount, last_transaction_at = :last_transaction_at, updated_at = :updated_at WHERE wallet_id = :wallet_id',
{
amount,
last_transaction_at: DateTime.now().toSQL({ includeOffset: false }),
updated_at: DateTime.now().toSQL({ includeOffset: false }),
wallet_id: fromWallet.wallet_id
}
)
);
if (updatedErr) {
await DBObject.rollback();
throw updatedErr;
}
// Update recipient wallet balance
const [updatedErr2] = await catchError(
DBObject.updateDirect(
'UPDATE user_wallets SET balance = balance + :amount, last_transaction_at = :last_transaction_at, updated_at = :updated_at WHERE wallet_id = :wallet_id',
{
amount,
last_transaction_at: DateTime.now().toSQL({ includeOffset: false }),
updated_at: DateTime.now().toSQL({ includeOffset: false }),
wallet_id: toWallet.wallet_id
}
)
);
if (updatedErr2) {
await DBObject.rollback();
throw updatedErr2;
}
// Commit transaction
await DBObject.commit();
// Send notifications
const fromUser = await DBObject.findOne('users', { user_id: fromUserId });
// Email notifications
if (fromUser?.email) {
await SendMail({
recipients: fromUser.email,
subject: 'Transfer Successful',
message: `
<h2>Transfer Confirmation</h2>
<p>Dear ${fromUser.first_name},</p>
<p>You have successfully transferred NGN ${amount.toFixed(2)} to ${toUser.first_name} ${toUser.last_name}.</p>
<p>Reference: ${reference}</p>
`
});
}
if (toUser?.email) {
await SendMail({
recipients: toUser.email,
subject: 'Money Received',
message: `
<h2>Transfer Received</h2>
<p>Dear ${toUser.first_name},</p>
<p>You have received NGN ${amount.toFixed(2)} from ${fromUser.first_name} ${fromUser.last_name}.</p>
<p>Reference: ${reference}</p>
`
});
}
// Push notifications
await createNotification({
user_id: fromUserId,
title: 'Transfer Successful',
message: `You sent NGN ${amount.toFixed(2)} to ${toUser.first_name} ${toUser.last_name}`,
notification_type: 'payment_update',
notification_channels: ['push', 'email'],
related_entity_id: debitInsert,
related_entity_type: 'transaction',
priority: 'high',
});
await createNotification({
user_id: toUserId,
title: 'Money Received',
message: `You received NGN ${amount.toFixed(2)} from ${fromUser.first_name} ${fromUser.last_name}`,
notification_type: 'payment_update',
notification_channels: ['push', 'email'],
related_entity_id: creditInsert,
related_entity_type: 'transaction',
priority: 'high',
});
return {
success: true,
message: 'Transfer completed successfully',
reference
};
} catch (error) {
await DBObject.rollback();
throw error;
}
} catch (error) {
logError({
message: 'Error processing wallet transfer',
source: 'WalletService.transferBetweenWallets',
error
});
throw error;
}
}
The Result
A production-ready transfer system that processes transactions in milliseconds while maintaining bank-level security and reliability.
Tech Stack
Backend: Node.js | TypeScript | MySQL | GraphQL
Key Takeaways
- ACID Compliance is Non-Negotiable: Financial systems must guarantee atomicity, consistency, isolation, and durability.
- Audit Trails Matter: Dual-entry accounting provides transparency and accountability.
- Validation is Multi-Layered: Never trust a single validation point—validate at every step.
- User Experience: Real-time notifications and clear transaction references build trust.
- Error Handling: Comprehensive logging and rollback mechanisms prevent data corruption.
Building financial systems requires meticulous attention to detail, but the result is a robust platform that users can trust with their money.
Article Information
Published on October 25, 2025
Estimated reading time: 2 minutes