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:
- Instant: See last 30 days (< 500ms)
- Background: Older transactions loading
- Notification: "Full history loaded" (after 2-3 seconds)
- 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;
}
});
Related Documentation
- Async Boundaries - Async execution fundamentals
- User Tasks - User task patterns
- Error Handling - Mobile error handling
- Callbacks - External callbacks
Features Used:
- Phase 2: Async Boundaries
- Phase 3: Callbacks (push notifications)
Status: ✅ Production Ready
Version: 2.0
Last Updated: January 2026