Skip to content

Repository files navigation

Flutter Financial Transaction System

A production-grade Flutter application demonstrating financial transaction safety, idempotency, and crash recovery for MSB-regulated environments.

How the Design Prevents Duplicate Debits / Double Spending

Permanent Transaction ID (Idempotency)

The system prevents duplicate debits through idempotent transaction IDs:

  1. Single ID Generation: Each transaction receives a permanent client_transaction_id (UUID) that is generated once before the first API call and stored in the local database immediately.

  2. ID Reuse for All Operations: The same transaction ID is reused for:

    • All retry attempts
    • OTP verification requests
    • Recovery queries
    • Status checks
  3. Duplicate Prevention: Before creating a new transaction, the system checks for any existing transaction with status pending, unknown, or risk_required. If found, it reuses the same transaction ID (regardless of amount). A new transaction ID is only generated when no unresolved transaction exists.

  4. Backend Idempotency: The backend uses the client_transaction_id to ensure idempotency - multiple requests with the same ID are treated as the same transaction, preventing duplicate debits.

Implementation:

// Transaction ID generated ONCE
final transactionId = uuid.v4(); // e.g., "550e8400-e29b-41d4-a716-446655440000"

// Saved to DB immediately (before any network call)
await database.insertTransaction(transaction);

// ALL subsequent operations use SAME ID
await apiClient.sendTransaction(clientTransactionId: transactionId, ...);
await apiClient.verifyOtp(clientTransactionId: transactionId, ...);
await apiClient.getTransactionStatus(clientTransactionId: transactionId, ...);

Transaction ID Lifecycle:

CREATED → PENDING → (retry with same ID) → SUCCESS or FAILED
                                    ↓
                            Only after SUCCESS/FAILED
                            can a new transaction be created

Safety Guarantees:

  • User taps "Send" multiple times → Same ID, backend rejects duplicate
  • App crashes mid-transaction → Recovery uses same ID
  • OTP verification retries → Same ID, backend knows it's the same transaction
  • Network timeouts → Retry uses same ID, no duplicate debit

How the System Self-Recovers When Transaction State is Unknown

Automatic Recovery on App Launch

The system automatically recovers transaction state when it's unknown or uncertain:

  1. Immediate Persistence: Every transaction is persisted to Sqflite database before any network call, ensuring transaction state is never lost.

  2. Recovery Detection: On app launch, the system queries for transactions with uncertain status:

    • pending: Transaction sent but response not received
    • unknown: Network timeout or connection error occurred
    • risk_required: OTP verification needed
  3. Backend Status Query: For each uncertain transaction, the system queries the backend using the stored client_transaction_id:

    GET /transactions/{id}/status
    
  4. State Synchronization: The backend returns the authoritative status (success, failed, or pending), and the local database is updated accordingly.

Recovery Flow:

// On app start
1. Load all transactions from local DB
2. Filter: status ∈ {pending, unknown, risk_required}
3. For each transaction:
   - Call backend: GET /transactions/{id}/status
   - Update local DB with backend response
   - Update UI state

Why it's Safe:

  • No Data Loss: Transaction state persisted before network calls
  • Backend as Source of Truth: Backend determines final status
  • Automatic Recovery: No manual intervention needed
  • No Stuck Transactions: All uncertain states are resolved on next app launch

Example Scenarios:

  1. Network Timeout:

    • Transaction sent → Network timeout (504) → Status set to unknown
    • App restarts → Recovery queries backend → Backend returns success (transaction actually completed)
    • Result: Status updated correctly, no duplicate send
  2. App Crash Before Response:

    • Transaction sent → App crashes → Status remains pending in DB
    • App restarts → Recovery queries backend → Backend returns actual status
    • Result: Transaction state restored correctly
  3. Connection Error:

    • Transaction sent → Connection lost → Status set to unknown
    • App restarts → Recovery queries backend → Backend returns final status
    • Result: System self-recovers without user intervention

Built with safety, correctness, and recoverability as first-class concerns.

About

Production-grade transaction safety in Flutter — idempotent money movement, crash recovery, OTP step-up and duplicate-debit prevention, with written safety guarantees and test scenarios.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages