Photo by Daniil Komov / Unsplash

From Debugging to Design: How Thinking in Flows Makes You a Better Payments Engineer

payment Jan 2, 2025

A mental model framework for understanding any payment system


This is the second in a series on payment systems engineering.
  1. This Map is Not the Territory: Why documentation and reality diverge
  2. From Debugging to Design: Mental models for understanding any payment flow
  3. Building for the 1%: Engineering exception handling as first-class work
  4. The FinTech Staff Engineer: Translating technical reality into business decisions

Each piece stands alone. Together, they form a framework for building reliable payment infrastructure at scale.


Introduction: Why Code Paths Are the Wrong Abstraction

Early in my career, I approached payment systems the way most engineers do: as a collection of APIs to integrate, databases to query, and edge cases to handle. I'd debug by tracing code paths, reading logs, and fixing the immediate problem.

It worked. Until it didn't.

The turning point came during development of TryGrip App. We had integrated multiple Nigerian financial providers, using an aggregator for bank connections and direct API integrations for wallet providers and fintechs. Each had different response formats and timing characteristics. Provider A returned authorizations in 2 seconds. Provider B took 30. Provider C's "success" response sometimes meant "we'll try to settle this." Provider D's settlement file arrived at 4 AM or 4 PM with no predictable schedule.

I couldn't hold this complexity in my head by thinking about code paths. There were too many exceptions and one-off behaviors masquerading as standard implementations.

I needed a different mental model. I found it by tracing flows instead of paths.

Money in payment systems if conserved. It doesn't vanish; it moves somewhere you haven't looked yet. Your job is to find where.

This framework; tracing funds flow, data flow and reconciliation flow, has become my primary tool for understanding and debugging payment systems, and for designing new ones. It works whether you're integrating a new provider, designing a ledger system or debugging why money disappeared between two systems.

Let me show you how it works.


The Three Flows Framework

Every payment system, no matter how complex, can be understood through three distinct flows:

flowchart TD PE[Payment Event] --> FF[Funds Flow] PE --> DF[Data Flow] PE --> RF[Reconciliation Flow] FF -.->|Must align| DF DF -.->|Must align| RF RF -.->|Must align| FF FF --> FFQ["Where does money
actually move?"] DF --> DFQ["What information
moves and when?"] RF --> RFQ["How do we verify
what happened?"]

Funds Flow: The actual movement of money between accounts, ledgers and institutions.

Data Flow: The information about payments; the requests, responses, status updates, notifications

Reconciliation Flow: The process of verifying that funds flow and data flow tell the same story

When these three flows align, your system is healthy. When they diverge, you have bugs, missing money or angry customers.


Part 1: Funds Flow - The GPS Tracker Question

The Question

"If I could attach a GPS tracker to this money, where would it go and when?"

Mapping Funds Flow

For any payment operation, draw the actual movement of value; not the API calls, DB Updates or status changes but the money itself.

Example: Card Payment to Merchant

flowchart TB A["Customer's Bank Account
Balance: $1,000"] -->|"1. Authorization
$100 hold placed
(Available: $900)"| B["Issuing Bank
Suspense/Hold Account"] B -->|"2. Settlement T+1
$100 moves"| C["Acquiring Bank
Merchant Pool"] C -->|"3. Payout T+1 to T+3
$97 (fees deducted)"| D["Merchant's Bank Account
Received: $97"]

Cross-Border Funds Flow Example

flowchart TB A[Customer Account
$1,000 USD] -->|Debit| B[Sending Bank
USD Ledger] B -->|MT103| C[Correspondent 1
USD Nostro] C -->|FX Conversion
1 = ₦1,550| D[Correspondent 2
NGN Nostro] D -->|MT103| E[Receiving Bank
Nigeria] E -->|Credit
₦1,524,000
after fees| F[Beneficiary Account] style C fill:#fff3cd style D fill:#fff3cd

Each correspondent deducts fees from the principal. The customer sent $1,000; the beneficiary receives ₦1,524,000 instead of the expected ₦1,550,000.

Key Questions for Funds Flow

When mapping any payment system, ask:

  1. Where does value rest at each stage?
    1. Is it in a customer account, a suspense account, or a pooled account?
    2. Who has legal ownership? Who has operational control?
  2. When does ownership transfer?
    1. Authorization doesn't transfer ownership; settlement does
    2. The moment of ownership transfer has legal and accounting implications, and is often the root of reconciliation disputes
  3. Who bears risk at each stage?
    1. Before settlement: issuer risk (authorization might not settle)
    2. After settlement but before payout: acquirer risk; after payout: merchant risk (chargebacks)
    3. Understanding risk assignment explains why systems are designed the way they are
  4. What triggers movement to the next stage?
    1. Time-based? (Settlement batch runs at midnight)
    2. Event-based? (Capture request from merchant)

Some systems also use threshold triggers (payout when balance exceeds $100).

Funds Flow Debugging Pattern

When money goes missing:

1. Identify the last known location
   - "Customer account was debited at 14:32"
   - Evidence: bank statement, provider settlement file

2. Identify where it should have arrived
   - "Merchant account should have credited by 09:00 next day"
   - Evidence: merchant payout report, bank statement

3. Trace the path between them
   - Did it leave the issuer? (Check settlement file from acquirer)
   - Did it arrive at acquirer and reach the merchant's payout batch? (Check acquirer settlement report and payout records)

4. Find the divergence point
   - "It reached the acquirer but wasn't included in merchant payout"
   - Root cause: Merchant payout account details were invalid
   - Fix: Notification system for payout failures, not silent skips

I once spent 8 hours tracing missing funds that had actually arrived but our payout report column headers had shifted, and our parser was reading the wrong column as "amount." The money was there. Our belief system said it wasn't. The divergence wasn't in funds flow; it was in data flow. This is why you must trace both.


Part 2: Data Flow — The Belief System

The Question

"What information moves through the system, and does it accurately represent the funds flow?"

Why Data Flow Diverges from Funds Flow

Data and money move differently

| Characteristic  | Funds Flow      | Data Flow                     |
|-----------------|-----------------|-------------------------------|
| Speed           | Slow (days)     | Fast (seconds)                |
| Granularity     | Batched         | Real-time                     |
| Direction       | One-way         | Request-response              |
| Reversibility   | Difficult       | Easy (messages can be resent) |
| Source of truth | Settlement file | API response                  |

This is not a bug. It's the fundamental constraint of payment systems. Your architecture must acknowledge it.

Mapping Data Flow

For the same card payment:

Real-Time Data Flow (Authorization):

sequenceDiagram participant C as Customer participant T as POS Terminal participant G as Gateway participant P as Processor participant N as Card Network participant I as Issuer C->>T: Tap/Insert Card T->>G: Auth Request G->>P: Forward P->>N: Forward N->>I: Auth Request Note over T,I: Each creates a transaction record I-->>N: Approved N-->>P: Approved P-->>G: Approved G-->>T: Approved T-->>C: "Payment Approved"

Batch Data Flow (Settlement T+1):

flowchart LR I[Issuer] -->|Settlement File| N[Card Network] N -->|Clearing File| P[Processor] P -->|Settlement Report| A[Acquirer] A -->|Payout Report| M[Merchant]
gantt title Data vs Funds Timeline dateFormat HH:mm axisFormat %H:%M section Data Flow Authorization Response :done, 14:00, 2m Your DB shows "Authorized" :done, 14:02, 58m Settlement file received :done, 09:00, 30m section Funds Flow Hold placed :active, 14:00, 2m Actual settlement (batch) :crit, 03:00, 60m Funds in merchant account :crit, 09:30, 30m

Your system "knows" about authorization instantly, but funds don't actually move until the overnight batch. The gap between belief and reality is where bugs hide.

The Data-Funds Timing Gap

Event Funds Flow Timing Data Flow Timing Gap Risk
Authorization Hold placed instantly Response in ~2 seconds None (tightly coupled)
Settlement Funds move at batch time File available next morning 12–24 hours
Reversal Funds returned in batch Notification within hours/days Merchant may have shipped
Chargeback Funds pulled back Notification within days (dispute window up to 120 days) Customer has product

Critical Insight: Your system knows about things before they happen (authorization before settlement) and after they happen (settlement file after funds moved). Rarely at the exact moment.

Data Flow Design Patterns

Pattern 1: Event Sourcing for Payment State

Instead of storing current state, store the sequence of data flow events:

public class PaymentEventStore
{
    public async Task AppendEvent(PaymentEvent @event)
    {
        await _store.Append(@event);
    }

    public async Task<Payment> Reconstitute(Guid paymentId)
    {
        var events = await _store.GetEvents(paymentId);
        var payment = new Payment();

        foreach (var @event in events)
        {
            payment.Apply(@event);
        }

        return payment;
    }
}

// Events model data flow, not funds flow
public record AuthorizationRequested(Guid PaymentId, Money Amount, DateTime Timestamp);
public record AuthorizationApproved(Guid PaymentId, string AuthCode, DateTime Timestamp);
public record SettlementReceived(Guid PaymentId, Money SettledAmount, DateTime Timestamp);
public record ReconciliationCompleted(Guid PaymentId, bool Matched, DateTime Timestamp);

Pattern 2: Status vs. Substatus

Your data flow captures more nuance than "success/failure":

public enum PaymentStatus
{
    Pending,
    Authorized,
    Captured,
    Settled,
    Completed,
    Failed,
    Refunded
}

public enum PaymentSubstatus
{
    // Pending substatus
    AwaitingProviderResponse,
    ProviderTimeout,
    RetryScheduled,

    // Authorized substatus
    AwaitingCapture,
    PartiallyAuthorized,

    // Settled substatus
    AwaitingReconciliation,
    ReconciliationMismatch,

    // Failed substatus
    InsufficientFunds,
    CardExpired,
    FraudSuspected,
    ProviderError
}

Data Flow Debugging Pattern

When a payment shows wrong status:

1. List all events for this payment in order
   - Authorization requested at 14:32:01
   - Authorization approved at 14:32:03
   - Void requested at 14:32:45
   - Void timeout at 14:33:45
   - Settlement file shows settled (received next day 09:00)

2. Identify the divergence
   - Customer thinks payment was cancelled (void requested)
   - System also thinks payment was cancelled (void timeout = assumed success?)
   - But reality: void failed, payment settled anyway

3. Trace root cause
   - Void timeout was treated as success
   - Should have been treated as "unknown" requiring reconciliation

Part 3: Reconciliation Flow - Updating Your Beliefs

The Question

"How do we know what we think happened actually happened?"

Why Reconciliation is Non-Negotiable

Your database contains your belief about what happened. The providers settlement file contains reality. Reconciliation is how you update your beliefs to match.

flowchart BT subgraph Volume["Volume vs Effort"] DI["DATA INGESTION
Settlement files, webhooks, API polling
100% of transactions"] MA["MATCHING
Automated ID/composite matching
95%+ auto-resolved"] DD["DISCREPANCY DETECTION
Automated rules flag anomalies
4% need review"] EI["EXCEPTION INVESTIGATION
Manual review, alerts
1% require human judgment"] end DI --> MA --> DD --> EI style DI fill:#d4edda style MA fill:#d4edda style DD fill:#fff3cd style EI fill:#f8d7da

The pyramid represents both volume and effort: most transactions auto-reconcile at the base, but the exceptions at the top consume disproportionate engineering time.

Ingestion: The Hidden Complexity

For TryGrip app, we maintained parsers for multiple settlement file formats. Fixed-width positional files. CSV with different column orders across providers. ISO 20022 XML. Excel files with merged cells. Each provider was certain their format was "standard." None of them were wrong. All of them were different.

public interface ISettlementFileParser
{
    bool CanParse(string fileName, byte[] content);
    Task<IEnumerable<SettlementRecord>> Parse(Stream file);
}

// We had multiple implementations
public class Iso20022Parser : ISettlementFileParser { /* XML parsing with namespace handling */ }
public class FixedWidthParser : ISettlementFileParser { /* ~300 lines pulling fields by position */ }
public class GenericCsvParser : ISettlementFileParser { /* ~200 lines with column inference */ }

Matching: Exact, Composite & Fuzzy

Not all records match cleanly on ID. Real reconciliation requires multiple strategies:

public class MatchingEngine
{
    public MatchResult Match(
        IEnumerable<InternalRecord> internal,
        IEnumerable<ExternalRecord> external)
    {
        var result = new MatchResult();

        foreach (var internalRecord in internal)
        {
            // Strategy 1: Exact ID match
            var exactMatch = external.FirstOrDefault(e =>
                e.TransactionId == internalRecord.ProviderTransactionId);

            if (exactMatch != null)
            {
                result.AddMatch(internalRecord, exactMatch, MatchType.ExactId);
                continue;
            }

            // Strategy 2: Composite key match (amount + date + reference)
            var compositeMatch = external.FirstOrDefault(e =>
                e.Amount == internalRecord.Amount &&
                e.TransactionDate.Date == internalRecord.TransactionDate.Date &&
                e.Reference.Contains(internalRecord.OrderReference));

            if (compositeMatch != null)
            {
                result.AddMatch(internalRecord, compositeMatch, MatchType.Composite);
                continue;
            }

            // Strategy 3: Fuzzy match with confidence score
            var fuzzyMatches = external
                .Select(e => new { Record = e, Score = CalculateFuzzyScore(internalRecord, e) })
                .Where(x => x.Score > 0.8)
                .OrderByDescending(x => x.Score)
                .ToList();

            if (fuzzyMatches.Any())
            {
                result.AddPotentialMatch(internalRecord, fuzzyMatches.First().Record,
                    fuzzyMatches.First().Score, MatchType.Fuzzy);
                continue;
            }

            // No match found
            result.AddUnmatchedInternal(internalRecord);
        }

        return result;
    }
}

Discrepancy Types

public enum DiscrepancyType
{
    // Amount discrepancies
    AmountMismatch,              // Different amount (common with fee deductions)
    CurrencyMismatch,            // Different currency (FX issues)

    // Status discrepancies
    InternalSuccessExternalFailed,  // We think it worked, it didn't
    InternalFailedExternalSuccess,  // We think it failed, it worked (worst case)

    // Existence discrepancies
    MissingInExternal,           // We have it, they don't (concerning)
    MissingInInternal,           // They have it, we don't (orphan transaction)

    // Timing discrepancies
    SettlementDateMismatch,      // Settled on different date than expected
    DuplicateInExternal          // Provider shows it twice
}

Part 4: Applying the Framework

Use Case 1: Debugging a "Missing Money" Report

Symptom: Customer claims they were charged but didn't receive the product.

Using the framework:

FUNDS FLOW ANALYSIS
├── Check: Was customer's account debited?
│   └── Yes, bank statement shows debit
├── Check: Did funds reach our account?
│   └── Check settlement file... Yes, included in yesterday's settlement
└── Conclusion: Funds flow completed correctly

DATA FLOW ANALYSIS
├── Check: What does our system show?
│   └── Status: "Pending" (should be "Completed")
├── Check: Did we receive settlement confirmation?
│   └── Webhook log shows settlement received
├── Check: Did status update trigger?
│   └── Error in log: "Order not found for payment ID"
└── Conclusion: Data flow broken at order-payment linkage

RECONCILIATION FLOW ANALYSIS
├── Check: Did reconciliation flag this?
│   └── No—reconciliation matched payment correctly
├── Check: Why didn't it catch the order issue?
│   └── Reconciliation only checks payment status, not order fulfillment
└── Conclusion: Reconciliation scope too narrow

ROOT CAUSE: Payment completed but order status wasn't updated due to
missing order-payment link. Reconciliation didn't catch it because
it only validates payment data, not fulfillment data.

FIX: Extend reconciliation to include order-payment linkage validation.

Use Case 2: Designing a New Payment Integration

Before writing code, map the three flows:

NEW PROVIDER: Mobile Money Integration

FUNDS FLOW
├── Customer balance → Provider float account (instant)
├── Provider float → Our settlement account (T+1 batch, 4 PM cutoff)
├── Questions:
│   ├── What happens if provider float is insufficient at cutoff?
│   │   → Rollover to next day? Partial settlement? Error?
│   ├── Is there a reserve/hold mechanism?
│   └── Who bears FX risk if settlement currency differs?

DATA FLOW
├── Real-time: Webhook for each transaction (claimed 99.9% delivery)
├── Batch: Daily settlement report via SFTP (format: ISO 20022)
├── Questions:
│   ├── Is the webhook reliable enough to be source of truth?
│   │   → Add polling backup every 4 hours for unconfirmed transactions
│   └── What's the mapping from their statuses to our state machine?

RECONCILIATION FLOW
├── Match our records against daily settlement report
├── Expected discrepancy types:
│   ├── Amount mismatch (check: do they deduct fees before reporting?)
│   └── Missing/extra in settlement (webhook and settlement file disagree)
└── SLA: Reconciliation must complete before T+2 payout cutoff

Part 5: Using Flows for Provider Evaluation

Senior Engineer's LENS

When evaluating a new payment provider, most engineers compare API documentation. This is a trap. API documentation is marketing.

Instead, use the Three Flows framework as your evaluation scorecard:

Question What You Learn
Funds Flow
"Where does money rest between customer payment and our settlement?" Counterparty risk, float requirements
"What is the settlement schedule? Cutoff times?" Working capital implications
"What happens if you become insolvent mid-cycle?" Legal/operational risk
"Who bears FX risk at each stage?" Economic exposure
Data Flow
"What is your 95th percentile authorization latency?" Not average—tail risk
"Do your webhooks have delivery guarantees? Retry policies?" Reliability for state transitions
"How do your statuses map to our state machine?" Semantic compatibility
"What data do you provide for reconciliation?" Matching fidelity
Reconciliation Flow
"What format are settlement files? Sample available before contract?" Integration complexity
"When are files available relative to settlement?" Detection latency
"What is your dispute/chargeback process and timeline?" Exception handling cost

This framework saved us from signing with a provider whose API was beautiful but whose settlement file was a PDF emailed to an unmonitored inbox. We learned this in evaluation, not production.

Conclusion: From Debugging to Design

The Three Flows Framework shifts your thinking:

Before After
"What API endpoint do I call?" "Where does the money go when I call it?"
"What status does this response return?" "Does this status reflect funds reality?"
"How do I handle this error?" "What flow divergence caused this error?"
"Why is this payment stuck?" "Which flow is misaligned?"

The next time you're staring at a payment bug, don't trace code paths. Follow the money and follow the data. Then verify they tell the same story.

That's how you become a better payments engineer.

That's how you move from debugging to design.


What mental models do you use for understanding payment systems? I'm especially interested in frameworks that have helped you evaluate providers or debug cross-border flows.

Tags

Views: Loading...