Skip to main content

Transfer Transaction

Complete guide to transfer transactions in BankLingo V2, including atomic operations, deadlock prevention, and multi-account impact tracking.


Overview​

A Transfer Transaction moves funds from one deposit account to another. Transfers require atomic operations to ensure both accounts are updated consistently.

Key Characteristics:

  • Atomic Operations: Both debit and credit must succeed or both fail
  • Deadlock Prevention: Accounts locked in consistent order (ascending by ID)
  • Intra-Bank vs Inter-Bank: Different settlement handling
  • Approval Workflow: Large transfers may require approval
  • Reversal Support: Full reversal with restoration of both accounts

Transaction Types:

  • Intra-Bank Transfer: Between accounts in same bank
  • Inter-Bank Transfer: To account in different bank (via settlement)
  • Own Account Transfer: Between customer's own accounts (may have reduced fees)

Transaction Flow​

Entities Impacted​

Scenario 1: Intra-Bank Transfer​

Transfer NGN 50,000 from Account A to Account B

PENDING Phase:

{
transactionId: "TRF123456",
transactionState: "PENDING",
impactedEntities: [
// Source Account (Debit)
{
entityType: "DepositAccount",
entityId: 100,
entityKey: "ACC-SOURCE",
fieldName: "AvailableBalance",
oldValue: 100000.00,
newValue: 49900.00, // 100000 - 50000 - 100 (fee)
deltaAmount: -50100.00
},
{
entityType: "DepositAccount",
entityId: 100,
entityKey: "ACC-SOURCE",
fieldName: "HoldAmount",
oldValue: 0.00,
newValue: 50100.00,
deltaAmount: +50100.00
},
// Destination Account (Credit - pending)
{
entityType: "DepositAccount",
entityId: 200,
entityKey: "ACC-DEST",
fieldName: "AvailableBalance",
oldValue: 50000.00,
newValue: 100000.00, // Pending credit visible
deltaAmount: +50000.00
},
{
entityType: "DepositAccount",
entityId: 200,
entityKey: "ACC-DEST",
fieldName: "PendingCredits",
oldValue: 0.00,
newValue: 50000.00,
deltaAmount: +50000.00
}
]
}
```text
**COMPLETED Phase**:

```typescript
{
transactionId: "TRF123456",
transactionState: "COMPLETED",
impactedEntities: [
// Source Account - Release Hold, Update BookBalance
{
entityType: "DepositAccount",
entityId: 100,
entityKey: "ACC-SOURCE",
fieldName: "BookBalance",
oldValue: 100000.00,
newValue: 49900.00,
deltaAmount: -50100.00
},
{
entityType: "DepositAccount",
entityId: 100,
entityKey: "ACC-SOURCE",
fieldName: "HoldAmount",
oldValue: 50100.00,
newValue: 0.00,
deltaAmount: -50100.00
},
// Destination Account - Finalize Credit
{
entityType: "DepositAccount",
entityId: 200,
entityKey: "ACC-DEST",
fieldName: "BookBalance",
oldValue: 50000.00,
newValue: 100000.00,
deltaAmount: +50000.00
},
{
entityType: "DepositAccount",
entityId: 200,
entityKey: "ACC-DEST",
fieldName: "PendingCredits",
oldValue: 50000.00,
newValue: 0.00,
deltaAmount: -50000.00
},
// GL Accounts
{
entityType: "GLAccount",
entityKey: "2100-001", // Customer Deposits (Source)
fieldName: "DebitAmount",
deltaAmount: +50000.00
},
{
entityType: "GLAccount",
entityKey: "2100-001", // Customer Deposits (Dest)
fieldName: "CreditAmount",
deltaAmount: +50000.00
},
{
entityType: "GLAccount",
entityKey: "4100-004", // Transfer Fee Income
fieldName: "CreditAmount",
deltaAmount: +100.00
}
]
}
```text
### Scenario 2: Inter-Bank Transfer

**Transfer NGN 100,000 to external bank via settlement**

```typescript
{
transactionId: "TRF789012",
transactionState: "COMPLETED",
impactedEntities: [
// Source Account
{
entityType: "DepositAccount",
entityId: 100,
entityKey: "ACC-SOURCE",
fieldName: "AvailableBalance",
oldValue: 200000.00,
newValue: 99700.00, // 200000 - 100000 - 300 (inter-bank fee)
deltaAmount: -100300.00
},
{
entityType: "DepositAccount",
entityId: 100,
entityKey: "ACC-SOURCE",
fieldName: "BookBalance",
oldValue: 200000.00,
newValue: 99700.00,
deltaAmount: -100300.00
},
// Settlement Account
{
entityType: "SettlementAccount",
entityId: 50,
entityKey: "NIBSS-SETTLE-001",
fieldName: "PendingDebits",
oldValue: 500000.00,
newValue: 600000.00,
deltaAmount: +100000.00
},
{
entityType: "SettlementAccount",
entityId: 50,
entityKey: "NIBSS-SETTLE-001",
fieldName: "TransactionCount",
oldValue: 150,
newValue: 151,
deltaAmount: +1
},
// GL Accounts
{
entityType: "GLAccount",
entityKey: "2100-001", // Customer Deposits
fieldName: "DebitAmount",
deltaAmount: +100000.00
},
{
entityType: "GLAccount",
entityKey: "1200-001", // Settlement Account (NIBSS)
fieldName: "CreditAmount",
deltaAmount: +100000.00
},
{
entityType: "GLAccount",
entityKey: "4100-005", // Inter-Bank Transfer Fee Income
fieldName: "CreditAmount",
deltaAmount: +300.00
}
]
}
```text
### Scenario 3: Own Account Transfer

**Transfer NGN 20,000 between customer's savings and current accounts (zero fee)**

```typescript
{
transactionId: "TRF345678",
transactionState: "COMPLETED",
impactedEntities: [
// Savings Account (Source)
{
entityType: "DepositAccount",
entityId: 300,
entityKey: "SAV-001",
fieldName: "AvailableBalance",
oldValue: 80000.00,
newValue: 60000.00,
deltaAmount: -20000.00
},
{
entityType: "DepositAccount",
entityId: 300,
entityKey: "SAV-001",
fieldName: "BookBalance",
oldValue: 80000.00,
newValue: 60000.00,
deltaAmount: -20000.00
},
// Current Account (Destination)
{
entityType: "DepositAccount",
entityId: 310,
entityKey: "CUR-001",
fieldName: "AvailableBalance",
oldValue: 15000.00,
newValue: 35000.00,
deltaAmount: +20000.00
},
{
entityType: "DepositAccount",
entityId: 310,
entityKey: "CUR-001",
fieldName: "BookBalance",
oldValue: 15000.00,
newValue: 35000.00,
deltaAmount: +20000.00
},
// GL (Internal Transfer - no net change to deposits)
{
entityType: "GLAccount",
entityKey: "2100-001", // Customer Deposits
fieldName: "DebitAmount",
deltaAmount: +20000.00
},
{
entityType: "GLAccount",
entityKey: "2100-001", // Customer Deposits
fieldName: "CreditAmount",
deltaAmount: +20000.00
}
]
}
```text
---

## Deadlock Prevention

### The Deadlock Problem

**Without Proper Locking Order**:

```text
Transaction A: Transfer from Account 100 to Account 200
Transaction B: Transfer from Account 200 to Account 100

Timeline:
1. Transaction A locks Account 100
2. Transaction B locks Account 200
3. Transaction A tries to lock Account 200 (waits for B to release)
4. Transaction B tries to lock Account 100 (waits for A to release)
5. DEADLOCK! Both transactions stuck forever.
```text
### Solution: Consistent Lock Order

**Always lock accounts in ascending order by AccountId**:

:::warning[Code Removed]
**Implementation details removed for security.**

Contact support for implementation guidance.
:::text
**Result**: Transaction A and B both lock Account 100 first, then Account 200. No deadlock possible.

---

## Approval Workflow

### Auto-Approval Conditions

Transfer is auto-approved if **ALL** conditions met:

1. Amount ≤ Auto-Approval Limit (from product config)
2. Source account state is ACTIVE
3. Destination account can receive credits (not CLOSED)
4. No fraud flags on either account
5. Beneficiary is whitelisted (if required by product)
6. Transfer limit not exceeded (single + daily)

### Manual Approval Process

**PENDING → APPROVAL → COMPLETED**

```typescript
// 1. Create transfer in PENDING state
const transfer = {
state: "PENDING",
amount: 500000, // Above auto-approval limit
sourceAccountId: 100,
destAccountId: 200,
approvalRequired: true
};

// 2. Holds placed immediately
sourceAccount.AvailableBalance -= 500000;
sourceAccount.HoldAmount += 500000;

destAccount.AvailableBalance += 500000; // Pending credit visible
destAccount.PendingCredits += 500000;

// 3. Notify approvers
await notificationService.NotifyApprovers({
transactionId: transfer.id,
amount: 500000,
sourceAccount: "ACC-100",
destAccount: "ACC-200",
reason: "Amount exceeds auto-approval limit"
});

// 4. Approver decision
if (approved) {
// Release holds, update book balances
sourceAccount.BookBalance -= 500000;
sourceAccount.HoldAmount -= 500000;

destAccount.BookBalance += 500000;
destAccount.PendingCredits -= 500000;

transfer.state = "COMPLETED";
} else {
// Reverse everything
sourceAccount.AvailableBalance += 500000;
sourceAccount.HoldAmount -= 500000;

destAccount.AvailableBalance -= 500000;
destAccount.PendingCredits -= 500000;

transfer.state = "REJECTED";
}
```text
---

## Limits & Validation

### Validation Rules

| Rule | Check | Error Code | Message |
|------|-------|------------|---------|
| **Source Account Active** | SourceAccount.State is ACTIVE | 05 | Source account not active |
| **Source Not Locked** | !SourceAccount.IsLocked | 05 | Source account locked |
| **Dest Account Valid** | DestAccount exists and not CLOSED | 14 | Invalid destination account |
| **Positive Amount** | Amount > 0 | 12 | Invalid amount |
| **Sufficient Balance** | Amount ≤ SourceAccount.AvailableBalance | 51 | Insufficient funds |
| **Single Transfer Limit** | Amount ≤ Product.TransferTransactionLimit | 61 | Amount exceeds transfer limit |
| **Daily Transfer Limit** | TodayTransfers + Amount ≤ Product.DailyTransferLimit | 65 | Daily transfer limit exceeded |
| **Beneficiary Whitelisting** | If required: Dest in WhitelistedBeneficiaries | 57 | Beneficiary not whitelisted |
| **Same Currency** | Source.Currency matches Dest.Currency | 12 | Currency mismatch |
| **Different Accounts** | SourceAccountId != DestAccountId | 12 | Cannot transfer to same account |

---

## Fee Calculation

### Fee Configuration

```yaml
transferFees:
- transferType: INTRA_BANK
ownAccount: true
feeType: FLAT
amount: 0.00

- transferType: INTRA_BANK
ownAccount: false
feeType: FLAT
amount: 100.00

- transferType: INTER_BANK
feeType: TIERED
tiers:
- minAmount: 0
maxAmount: 10000
fee: 200.00
- minAmount: 10001
maxAmount: 100000
fee: 500.00
- minAmount: 100001
maxAmount: null
fee: 1000.00

- transferType: INSTANT_TRANSFER
feeType: PERCENTAGE
percentage: 1.5
minAmount: 100.00
maxAmount: 5000.00

Calculation Logic​

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


GL Posting​

Journal Entries​

Example 1: Intra-Bank Transfer

AccountDescriptionDebitCredit
2100-001Customer Deposits (Source)50,000.00-
2100-001Customer Deposits (Dest)-50,000.00
4100-004Transfer Fee Income-100.00
2100-001Customer Deposits (Fee from Source)100.00-

Example 2: Inter-Bank Transfer

AccountDescriptionDebitCredit
2100-001Customer Deposits (Source)100,000.00-
1200-001Settlement Account (NIBSS)-100,000.00
4100-005Inter-Bank Fee Income-300.00
2100-001Customer Deposits (Fee from Source)300.00-

Example 3: Own Account Transfer (Zero Fee)

AccountDescriptionDebitCredit
2100-001Customer Deposits20,000.00-
2100-001Customer Deposits-20,000.00

API Usage​

Request​

POST /api/v2/transactions/transfer
Content-Type: application/json
Authorization: Bearer {token}

{
"sourceAccountEncodedKey": "ACC-SOURCE",
"destAccountEncodedKey": "ACC-DEST",
"amount": 50000.00,
"transferType": "INTRA_BANK",
"narration": "Monthly rent payment",
"customerReference": "RENT-JAN-2025",
"beneficiaryName": "Jane Doe",
"channelType": "ONLINE_BANKING"
}
```text
### Response - Success (Auto-Approved)

```json
{
"success": true,
"transactionKey": "TRF123456",
"transactionId": 789012,
"state": "COMPLETED",
"sourceAccount": {
"accountNumber": "0123456789",
"accountName": "John Doe",
"oldBalance": 100000.00,
"newBalance": 49900.00
},
"destAccount": {
"accountNumber": "9876543210",
"accountName": "Jane Doe",
"oldBalance": 50000.00,
"newBalance": 100000.00
},
"transferAmount": 50000.00,
"feeAmount": 100.00,
"totalDebit": 50100.00,
"transactionDate": "2025-12-28T10:30:00Z",
"impactRecordId": 456,
"message": "Transfer successful"
}
```text
### Response - Pending Approval

```json
{
"success": true,
"transactionKey": "TRF123457",
"transactionId": 789013,
"state": "PENDING",
"sourceAccount": {
"accountNumber": "0123456789",
"holdPlaced": 500000.00,
"availableBalance": 0.00
},
"destAccount": {
"accountNumber": "9876543210",
"pendingCredit": 500000.00
},
"transferAmount": 500000.00,
"approvalRequired": true,
"message": "Transfer pending approval. Funds held."
}
```text
### Response - Error (Insufficient Funds)

```json
{
"success": false,
"errorCode": "51",
"errorMessage": "Insufficient funds",
"sourceAccount": {
"accountNumber": "0123456789",
"availableBalance": 30000.00
},
"requestedAmount": 50000.00,
"shortfall": 20000.00
}
```text
---

## Error Codes

| Code | Message | Cause | Resolution |
|------|---------|-------|------------|
| 00 | Success | Transaction completed | None |
| 05 | Account locked/inactive | Source or dest account not ACTIVE | Contact branch |
| 12 | Invalid amount/params | Amount ≤ 0, currency mismatch, same account | Correct parameters |
| 14 | Invalid destination | Dest account not found or CLOSED | Verify account number |
| 51 | Insufficient funds | Amount > Source.AvailableBalance | Check balance |
| 57 | Beneficiary not whitelisted | Dest not in whitelist | Add beneficiary first |
| 61 | Amount exceeds limit | Amount > TransferTransactionLimit | Reduce amount or request approval |
| 65 | Daily limit exceeded | Today's transfers + Amount > DailyLimit | Wait until next day |
| 91 | System error | Database/network failure | Retry or contact support |

---

## Narration Templates

### Customer Narration (Source)

```text
Transfer to {BeneficiaryName} - {Amount:N2}
Ref: {TransactionReference}
```text
**Example**: `Transfer to Jane Doe - 50,000.00 Ref: TRF123456`

### Customer Narration (Destination)

```text
Transfer from {SenderName} - {Amount:N2}
Ref: {TransactionReference}
```text
**Example**: `Transfer from John Doe - 50,000.00 Ref: TRF123456`

### Channel Narration

```text
{SenderName} → {BeneficiaryName} - {Amount:N2} - {ChannelType}
```text
**Example**: `John Doe → Jane Doe - 50,000.00 - ONLINE_BANKING`

---

## Concurrent Safety

### Scenario: Simultaneous Transfers

**Setup**:
- Account A Balance: NGN 100,000
- Transaction 1: Transfer A → B (NGN 60,000)
- Transaction 2: Transfer A → C (NGN 60,000)
- Both requests arrive simultaneously

**With Proper Locking** (SAFE):

:::warning[Code Removed]
**Implementation details removed for security.**

Contact support for implementation guidance.
:::text
---

## Best Practices

### For Users

1. **Verify Beneficiary Details**: Double-check account number and name
2. **Use References**: Add meaningful references for tracking
3. **Check Limits**: Know your transfer limits to avoid delays
4. **Whitelist Beneficiaries**: Pre-register frequent beneficiaries for faster transfers
5. **Monitor Pending Credits**: Check PendingCredits if transfer seems delayed

### For Developers

1. **Always Lock in Order**: Use ascending AccountId order to prevent deadlocks
2. **Use Transactions**: Wrap entire transfer in database transaction
3. **Track All Impacts**: Record changes to both source and dest in TransactionImpactRecord
4. **Handle Rejections**: Properly reverse holds and pending credits on rejection
5. **Test Concurrency**: Simulate simultaneous transfers in testing
6. **Validate Both Accounts**: Check both source and dest before proceeding

### For Operations

1. **Monitor Large Transfers**: Flag transfers > threshold for review
2. **Review Pending Transfers**: Investigate transfers stuck in PENDING > 1 hour
3. **Reconcile Daily**: Ensure total debits = total credits
4. **Audit Limits**: Review and adjust transfer limits quarterly
5. **Track Failures**: Monitor transfer failure rates by channel and reason

---

## Reversal Process

### Full Reversal Steps

```typescript
// Original Transfer
{
transactionId: "TRF123456",
sourceAccountId: 100,
destAccountId: 200,
amount: 50000.00,
feeAmount: 100.00,
state: "COMPLETED",
impacts: [
{ entity: "DepositAccount", entityId: 100, field: "BookBalance", delta: -50100.00 },
{ entity: "DepositAccount", entityId: 200, field: "BookBalance", delta: +50000.00 }
]
}

// Reversal Transaction
{
transactionId: "REV123456",
originalTransactionId: "TRF123456",
sourceAccountId: 200, // Swapped: now source is original dest
destAccountId: 100, // Swapped: now dest is original source
amount: 50000.00,
feeAmount: 0.00, // No fee on reversal
state: "COMPLETED",
impacts: [
{ entity: "DepositAccount", entityId: 100, field: "BookBalance", delta: +50100.00 }, // Opposite
{ entity: "DepositAccount", entityId: 200, field: "BookBalance", delta: -50000.00 } // Opposite
]
}

// Update Original Transaction
originalTransaction.state = "REVERSED";
originalTransaction.reversedDate = DateTime.UtcNow;
originalTransaction.reversalTransactionId = "REV123456";
```text
---

## V2 API Commands

BankLingo V2 provides the **InitiateTransferCommand** for BPMCore integration.

### Command Overview

- **Command**: `InitiateTransferCommand`
- **Implementation**: `CB.Administration.Api/Commands/BPMCore/DepositWithdrawal/AdministrationCoreTransferCommandHandlers.cs`
- **Purpose**: Initiate fund transfers with dual-account tracking and deadlock prevention
- **BPM Integration**: Accepts parameters via `BpmUtil.GetPropertyValue()`
- **State Management**: PENDING → APPROVED → SETTLED

### Key Features

**Dual-Account Impact Tracking**:
- Source account: Hold placed on available balance (PENDING)
- Destination account: Pending credit tracked separately
- Both accounts tracked via `TransactionImpactRecord`
- Delta-based tracking for each account independently

**Deadlock Prevention**:
- Accounts locked in deterministic order (by EncodedKey sort)
- Prevents circular wait conditions
- Enables safe concurrent transfers between same accounts
- Example: Transfer A→B and B→A can execute concurrently without deadlock

**Hold and Pending Credit Management**:

**PENDING State**:

```text
Source Account:
BookBalance: Unchanged
AvailableBalance: Decreased (hold placed)

Destination Account:
BookBalance: Unchanged
PendingCredit: Increased (not yet available)

SETTLED State:

Source Account:
BookBalance: Decreased
AvailableBalance: Already reduced (hold removed)

Destination Account:
BookBalance: Increased
AvailableBalance: Increased (pending credit finalized)

Approval Workflow Integration:

  • Large transfers require approval with holds in place
  • Rejection releases source hold and destination pending credit
  • Approval proceeds to settlement with dual balance updates
  • TransactionImpactTracker records all state changes

Concurrent Safety:

  • Optimistic locking with version checking on both accounts
  • Deterministic lock ordering prevents deadlocks
  • Delta-based balance updates (not absolute values)
  • Atomic dual-account updates

Fee Handling:

  • Transfer fees deducted from source account
  • Fee amount tracked separately in impact records
  • GL posting includes fee income account
  • Fee waived for own-account transfers

Reversal Support:

  • Reversal swaps source/destination accounts
  • Delta-based restoration ensures accuracy
  • All original impacts reversed with opposite deltas
  • Maintains audit trail with original + reversal records

Implementation Checklist​

Database Requirements​

  • Add PendingCredits column to DepositAccount
  • Add HoldAmount column to DepositAccount
  • Add Version column to DepositAccount for optimistic locking
  • Create TransferTransaction table
  • Create TransactionImpactRecord table
  • Create ImpactedEntity table
  • Add index on TransferTransaction(SourceAccountId, DestAccountId)
  • Add index on ImpactedEntity(EntityType, EntityId)

Code Requirements​

  • Implement ProcessTransferAsync with deadlock prevention
  • Add account locking in consistent order (ascending by Id)
  • Implement hold placement on source account
  • Implement pending credit on destination account
  • Track all entity impacts in TransactionImpactRecord
  • Implement approval workflow with hold management
  • Add hold/credit release on rejection
  • Implement reversal handler with swapped accounts
  • Add fee calculation by transfer type
  • Implement GL posting logic for transfers

Testing Requirements​

  • Unit test: Deadlock prevention with simultaneous transfers
  • Unit test: Hold placement and release
  • Unit test: Pending credit placement and finalization
  • Integration test: Intra-bank transfer end-to-end
  • Integration test: Inter-bank transfer with settlement
  • Integration test: Own account transfer (zero fee)
  • Integration test: Transfer + reversal = original balances
  • Load test: 100 concurrent transfers
  • UI test: Online banking transfer workflow

Documentation Requirements​

  • Update API documentation with transfer endpoints
  • Create user guide on beneficiary whitelisting
  • Document error codes and resolution steps
  • Create runbook for stuck pending transfers
  • Document GL account mapping for different transfer types

Developer Resources​

For API implementation details, see:


Next: Cheque Transaction →