Skip to main content

Mobile App Patterns

Specialized workflow patterns optimized for mobile banking applications, focusing on performance, offline capabilities, and user experience.

Overview

Mobile-optimized patterns provide:

  • Instant feedback - Immediate responses for users
  • Background sync - Data synchronization without blocking UI
  • Offline support - Queue operations when offline
  • Progressive loading - Load data incrementally
  • Battery efficiency - Minimize network calls

Pattern 1: Instant Loan Application

Submit loan application with immediate response, processing in background.

Mobile App Flow

// Mobile app (React Native / Flutter)
async function submitLoanApplication(application) {
try {
// Show loading indicator
showLoading('Submitting application...');

// Call API
const response = await fetch('/api/processes/loan-application/start', {
method: 'POST',
body: JSON.stringify({
customerId: application.customerId,
amount: application.amount,
purpose: application.purpose,
documents: application.documents
})
});

const result = await response.json();

// Hide loading
hideLoading();

// Show immediate success
showSuccess({
title: 'Application Submitted!',
message: 'Your loan application has been submitted. We\'ll notify you of the decision.',
applicationId: result.applicationId,
referenceNumber: result.referenceNumber
});

// Navigate to status page
navigateTo('ApplicationStatus', {
applicationId: result.applicationId
});

} catch (error) {
hideLoading();
showError('Failed to submit application: ' + error.message);
}
}

Backend Process

<bpmn:startEvent id="StartLoanApp" name="Start"/>

<!-- Quick validation (synchronous) -->
<bpmn:scriptTask id="ValidateApplication" name="Validate">
<bpmn:script>
var app = context.application;

// Quick validation only
if (!app.customerId) {
throw new BpmnError('VALIDATION_ERROR', 'Customer ID required');
}
if (!app.amount || app.amount <= 0) {
throw new BpmnError('VALIDATION_ERROR', 'Valid amount required');
}

// Generate reference number
context.referenceNumber = 'LA-' + new Date().getTime();
context.applicationId = context.processInstanceId;

logger.info('Application validated: ' + context.referenceNumber);

return {
applicationId: context.applicationId,
referenceNumber: context.referenceNumber,
status: 'SUBMITTED'
};
</bpmn:script>
</bpmn:scriptTask>

<!-- ASYNC: Everything after this runs in background -->
<bpmn:serviceTask id="SaveApplication"
name="Save Application"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="SaveLoanApplicationCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Credit check -->
<bpmn:serviceTask id="CreditCheck" name="Credit Check">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CreditCheckCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Send push notification when complete -->
<bpmn:sendTask id="SendPushNotification" name="Send Push">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="push"/>
<custom:property name="deviceTokens" value="${context.customerDeviceTokens}"/>
<custom:property name="title" value="Loan Application Update"/>
<custom:property name="body" value="Your loan application has been ${context.decision}"/>
<custom:property name="data" value="${JSON.stringify({applicationId: context.applicationId, decision: context.decision})}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>

<bpmn:endEvent id="End"/>

API Response (< 200ms):

{
"processInstanceId": "abc-123",
"applicationId": "APP-2026-001",
"referenceNumber": "LA-1704902400000",
"status": "SUBMITTED",
"message": "Your application has been submitted successfully."
}

Timeline:

  • 0ms: User taps "Submit"
  • 50ms: Quick validation passes
  • 100ms: API returns response ✅
  • 150ms: User sees success message
  • 200ms: User navigates to status page
  • Background: Credit check, risk assessment, decision (2-5 seconds)
  • Later: Push notification sent with decision

Pattern 2: Fund Transfer with Instant Confirmation

Transfer funds with immediate confirmation, settlement in background.

Mobile App

async function transferFunds(transfer) {
try {
showLoading('Processing transfer...');

const response = await fetch('/api/processes/fund-transfer/start', {
method: 'POST',
body: JSON.stringify({
fromAccount: transfer.fromAccount,
toAccount: transfer.toAccount,
amount: transfer.amount,
narration: transfer.narration
})
});

const result = await response.json();

hideLoading();

// Show instant success
showSuccess({
title: 'Transfer Initiated!',
message: 'Your transfer of ₦' + transfer.amount + ' is being processed.',
transactionId: result.transactionId,
referenceNumber: result.referenceNumber
});

// Optimistic UI update (show in transaction list immediately)
updateTransactionList({
id: result.transactionId,
amount: transfer.amount,
status: 'PROCESSING',
timestamp: new Date()
});

} catch (error) {
showError('Transfer failed: ' + error.message);
}
}

Backend Process

<bpmn:startEvent id="StartTransfer" name="Start"/>

<!-- Validate transfer (synchronous) -->
<bpmn:scriptTask id="ValidateTransfer" name="Validate">
<bpmn:script>
var transfer = context.transfer;

// Quick validation
if (transfer.amount <= 0) {
throw new BpmnError('INVALID_AMOUNT', 'Amount must be greater than 0');
}
if (transfer.fromAccount === transfer.toAccount) {
throw new BpmnError('SAME_ACCOUNT', 'Cannot transfer to same account');
}

// Check balance (fast database query)
var balance = BankLingo.ExecuteCommand('GetAccountBalance', {
accountNumber: transfer.fromAccount
});

if (balance.result.availableBalance < transfer.amount) {
throw new BpmnError('INSUFFICIENT_FUNDS', 'Insufficient funds');
}

// Generate transaction ID
context.transactionId = 'TXN-' + new Date().getTime();
context.referenceNumber = generateReference();

return {
transactionId: context.transactionId,
referenceNumber: context.referenceNumber,
status: 'PROCESSING'
};

function generateReference() {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var ref = '';
for (var i = 0; i < 12; i++) {
ref += chars.charAt(Math.floor(Math.random() * chars.length));
}
return ref;
}
</bpmn:script>
</bpmn:scriptTask>

<!-- ASYNC: Actual transfer processing in background -->
<bpmn:serviceTask id="ProcessTransfer"
name="Process Transfer"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="ProcessTransferCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Post to general ledger -->
<bpmn:serviceTask id="PostToGL" name="Post to GL">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="PostJournalEntryCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Update account balances -->
<bpmn:serviceTask id="UpdateBalances" name="Update Balances">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="UpdateAccountBalancesCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Send push notification -->
<bpmn:sendTask id="SendCompletionNotification" name="Send Notification">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="push"/>
<custom:property name="deviceTokens" value="${context.deviceTokens}"/>
<custom:property name="title" value="Transfer Complete"/>
<custom:property name="body" value="Your transfer of ₦${context.amount} was successful"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>

<bpmn:endEvent id="End"/>

Benefits:

  • ✅ User sees instant confirmation
  • ✅ UI updates optimistically (shows transaction immediately)
  • ✅ Actual settlement happens in background
  • ✅ Push notification when complete

Pattern 3: Bill Payment with Queue

Allow offline bill payments, sync when online.

Mobile App with Offline Queue

// Queue manager
class PaymentQueue {
constructor() {
this.queue = this.loadQueue();
}

// Add payment to queue
async addPayment(payment) {
const queueItem = {
id: generateId(),
payment: payment,
timestamp: new Date().toISOString(),
status: 'QUEUED',
retries: 0
};

this.queue.push(queueItem);
this.saveQueue();

// Try to process immediately if online
if (navigator.onLine) {
await this.processQueue();
}

return queueItem;
}

// Process queued payments
async processQueue() {
const pending = this.queue.filter(item => item.status === 'QUEUED');

for (const item of pending) {
try {
const result = await fetch('/api/processes/bill-payment/start', {
method: 'POST',
body: JSON.stringify(item.payment)
});

if (result.ok) {
item.status = 'COMPLETED';
item.completedAt = new Date().toISOString();
item.result = await result.json();

// Show notification
showNotification('Payment processed: ' + item.payment.billerName);
} else {
item.status = 'FAILED';
item.error = await result.text();
}
} catch (error) {
item.retries++;
if (item.retries >= 3) {
item.status = 'FAILED';
item.error = error.message;
}
}
}

this.saveQueue();
this.updateUI();
}

loadQueue() {
return JSON.parse(localStorage.getItem('paymentQueue') || '[]');
}

saveQueue() {
localStorage.setItem('paymentQueue', JSON.stringify(this.queue));
}
}

// Usage
async function payBill(payment) {
const queue = new PaymentQueue();

if (navigator.onLine) {
// Online: Process immediately
try {
showLoading('Processing payment...');

const response = await fetch('/api/processes/bill-payment/start', {
method: 'POST',
body: JSON.stringify(payment)
});

const result = await response.json();

hideLoading();
showSuccess('Payment successful!');

} catch (error) {
// Failed online, add to queue
await queue.addPayment(payment);
showInfo('Payment queued. Will process when connection improves.');
}
} else {
// Offline: Add to queue
await queue.addPayment(payment);
showInfo('Payment queued. Will process when you\'re back online.');
}
}

// Listen for online event
window.addEventListener('online', async () => {
const queue = new PaymentQueue();
await queue.processQueue();
});

Backend Process

<bpmn:startEvent id="StartBillPayment" name="Start"/>

<!-- Validate payment -->
<bpmn:scriptTask id="ValidatePayment" name="Validate">
<bpmn:script>
var payment = context.payment;

if (!payment.billerId || !payment.amount) {
throw new BpmnError('VALIDATION_ERROR', 'Invalid payment details');
}

context.paymentId = 'PAY-' + new Date().getTime();

return {
paymentId: context.paymentId,
status: 'PROCESSING'
};
</bpmn:script>
</bpmn:scriptTask>

<!-- ASYNC: Process payment -->
<bpmn:serviceTask id="ProcessBillPayment"
name="Process Payment"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="ProcessBillPaymentCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Send receipt -->
<bpmn:sendTask id="SendReceipt" name="Send Receipt">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="push"/>
<custom:property name="title" value="Payment Complete"/>
<custom:property name="body" value="Payment to ${context.billerName} successful"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>

<bpmn:endEvent id="End"/>

Pattern 4: Progressive Account Statement Loading

Load recent transactions immediately, older transactions in background.

Mobile App

async function loadAccountStatement(accountId) {
try {
// Phase 1: Load recent transactions (fast)
showLoading('Loading transactions...');

const recentResponse = await fetch(
`/api/accounts/${accountId}/transactions?days=30`
);
const recentData = await recentResponse.json();

hideLoading();

// Display recent transactions
displayTransactions(recentData.transactions);

// Phase 2: Load older transactions in background (async)
loadOlderTransactions(accountId, recentData.oldestDate);

} catch (error) {
hideLoading();
showError('Failed to load transactions');
}
}

async function loadOlderTransactions(accountId, fromDate) {
// Start background process for older data
const response = await fetch('/api/processes/load-history/start', {
method: 'POST',
body: JSON.stringify({
accountId: accountId,
fromDate: fromDate,
toDate: calculateOldDate(fromDate) // 1 year back
})
});

const result = await response.json();

// Poll for completion or wait for push notification
pollForHistoryComplete(result.processInstanceId);
}

function pollForHistoryComplete(processId) {
const interval = setInterval(async () => {
const status = await fetch(`/api/processes/${processId}/status`);
const data = await status.json();

if (data.status === 'COMPLETED') {
clearInterval(interval);

// Load and append older transactions
const history = await fetch(`/api/processes/${processId}/result`);
const historyData = await history.json();

appendTransactions(historyData.transactions);
showInfo('Full history loaded');
}
}, 2000); // Poll every 2 seconds
}

Backend Process

<bpmn:startEvent id="StartHistoryLoad" name="Start"/>

<!-- Quick acknowledgment -->
<bpmn:scriptTask id="AcknowledgeRequest" name="Acknowledge">
<bpmn:script>
context.historyLoadId = 'HIST-' + new Date().getTime();

return {
historyLoadId: context.historyLoadId,
status: 'PROCESSING'
};
</bpmn:script>
</bpmn:scriptTask>

<!-- ASYNC: Load historical data -->
<bpmn:serviceTask id="LoadHistoricalTransactions"
name="Load History"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="LoadHistoricalTransactionsCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Generate PDF statement (optional) -->
<bpmn:serviceTask id="GeneratePDFStatement" name="Generate PDF">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="GenerateStatementPDFCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Cache for future use -->
<bpmn:serviceTask id="CacheResults" name="Cache Results">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CacheStatementCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Notify completion -->
<bpmn:sendTask id="NotifyComplete" name="Notify">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="push"/>
<custom:property name="title" value="Statement Ready"/>
<custom:property name="body" value="Your full account statement is ready"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>

<bpmn:endEvent id="End"/>

User Experience:

  1. Instant: See last 30 days (< 500ms)
  2. Background: Older transactions loading
  3. Notification: "Full history loaded" (after 2-3 seconds)
  4. Progressive: Smooth, non-blocking experience

Pattern 5: Document Upload with Progress

Upload documents with progress tracking and background processing.

Mobile App

async function uploadDocument(document) {
try {
// Create form data
const formData = new FormData();
formData.append('file', document.file);
formData.append('documentType', document.type);
formData.append('customerId', document.customerId);

// Upload with progress tracking
const response = await fetch('/api/documents/upload', {
method: 'POST',
body: formData,
// Track upload progress
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
updateProgress(percentCompleted);
}
});

const result = await response.json();

// Show upload complete
showSuccess('Document uploaded!');

// Start background processing
const processResponse = await fetch('/api/processes/document-processing/start', {
method: 'POST',
body: JSON.stringify({
documentId: result.documentId
})
});

const processResult = await processResponse.json();

// Show processing status
showInfo('Document is being verified. You\'ll be notified when complete.');

// Navigate to document list
navigateTo('Documents');

} catch (error) {
showError('Upload failed: ' + error.message);
}
}

Mobile-Specific Best Practices

✅ Do This

// ✅ Show immediate feedback
showSuccess('Request submitted!');

// ✅ Use optimistic UI updates
updateUI(newData); // Update before server confirms

// ✅ Queue operations when offline
if (!navigator.onLine) {
queueOperation(operation);
}

// ✅ Send push notifications for completion
// (In backend BPMN)

// ✅ Use progressive loading
loadRecentData();
then(() => loadOlderData());

// ✅ Cache data locally
localStorage.setItem('lastBalance', balance);

// ✅ Minimize network calls
// Batch requests when possible

❌ Don't Do This

// ❌ Long synchronous waits
await processEverything(); // 10 seconds
showResult();

// ❌ No offline support
// App crashes when offline

// ❌ No progress indicators
// User doesn't know what's happening

// ❌ Large data transfers
// Don't load full transaction history at once

// ❌ No error recovery
// Failed requests are lost forever

// ❌ Blocking UI
// User can't do anything while waiting

Push Notification Integration

Backend Configuration

<bpmn:sendTask id="SendPush" name="Send Push Notification">
<bpmn:extensionElements>
<custom:properties>
<!-- Message type -->
<custom:property name="messageType" value="push"/>

<!-- Device tokens (FCM/APNS) -->
<custom:property name="deviceTokens" value="${context.customerDeviceTokens}"/>

<!-- Notification content -->
<custom:property name="title" value="Transaction Complete"/>
<custom:property name="body" value="Your transfer of ₦${context.amount} was successful"/>

<!-- Custom data payload -->
<custom:property name="data" value="${JSON.stringify({
transactionId: context.transactionId,
type: 'TRANSFER_COMPLETE',
amount: context.amount,
timestamp: new Date().toISOString()
})}"/>

<!-- Badge count -->
<custom:property name="badge" value="1"/>

<!-- Sound -->
<custom:property name="sound" value="default"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>

Mobile App Notification Handler

// Handle incoming push notifications
PushNotification.onNotification((notification) => {
const data = notification.data;

switch (data.type) {
case 'TRANSFER_COMPLETE':
// Update transaction list
refreshTransactions();

// Show in-app notification
showInAppNotification({
title: notification.title,
message: notification.body,
action: () => navigateTo('TransactionDetails', {
id: data.transactionId
})
});
break;

case 'LOAN_DECISION':
// Navigate to loan details
navigateTo('LoanApplication', {
id: data.applicationId
});
break;

case 'DOCUMENT_VERIFIED':
// Update document status
updateDocumentStatus(data.documentId, 'VERIFIED');
break;
}
});

Features Used:

  • Phase 2: Async Boundaries
  • Phase 3: Callbacks (push notifications)

Status: ✅ Production Ready
Version: 2.0
Last Updated: January 2026