Fintech Lab

Cheat sheet

Everything that's load-bearing across the curriculum, on one page. Print it, bookmark it, paste it into a Notion. It exists so you don't have to remember which side a credit goes on every time you read a journal entry.

The five account types and their natural side

"Natural side" is the side that INCREASES the balance. The opposite side decreases it.

TypeIncreases onDecreases onExamples
AssetDebitCreditBank, receivable, equipment
LiabilityCreditDebitUser wallet, accounts payable
EquityCreditDebitOwner's capital, retained earnings
Income (revenue)CreditDebitSales, interchange, interest
ExpenseDebitCreditSalaries, processing fees, bad debt

Mnemonic: DEA on the debit side (Debits favor Expenses, Drawings, Assets), CLIR on the credit side (Credits favor Liabilities, Income, Revenue). Or just memorise the table.

The invariants

  • Assets = Liabilities + Equity. The accounting equation. Holds before and after every journal entry. If it doesn't, you posted a bad entry.
  • Total debits = total credits. True for every individual entry AND across the whole ledger. Run this as a SQL query on cron; alert when it drifts.
  • Entries are immutable. Never UPDATE a posted line, never DELETE one. Corrections are NEW reversing entries. This is what makes the audit trail trustworthy.
  • sum(user wallets) = FBO balance. The iron law of sub-accounting. If it breaks, your sponsor bank's audit fails. Reconcile daily.

Common entry shapes

Deposit

Bank Account (asset)DEBITamount
User Wallet (liability)CREDITamount

Two lines. Asset up, liability up. The money came in from outside and you now owe it to the user.

Withdrawal (settled)

User Wallet (liability)DEBITamount
Bank Account (asset)CREDITamount

Reverse of a deposit. Liability down, asset down. The money left.

Withdrawal (initiated, not yet settled)

User Wallet (liability)DEBITamount
Pending Withdrawal (liability)CREDITamount

Reservation pattern. Total liability unchanged; just moves from 'available' to 'pending'. The bank account moves later when the provider actually settles.

Refund

User Wallet (liability)DEBITuserCreditedAmount
Platform Fee Revenue (income)DEBITfeeAmount
Bank Account (asset)CREDITgrossAmount

A reversing entry; the original deposit stays on the books forever. Revenue gets a debit because we're un-recognising it.

Card swipe interchange (issuer side)

Network Receivable (asset)DEBITyourNetCut
Network Fee Expense (expense)DEBITnetworkCut
Interchange Revenue (income)CREDITgrossInterchange

Three lines. You earn the gross, pay the network its slice, take home the rest as a receivable until settlement.

Compensating entry (saga rollback)

Original credit accountDEBIToriginalAmount
Original debit accountCREDIToriginalAmount

The exact inverse of the step you're undoing. NEW entry, not a delete. Audit trail shows: initiated, failed, compensated.

Engineering patterns that show up everywhere

  • Idempotency keys. Every entry that originates from an external event carries the provider's event_id in anidempotency_keycolumn. A partial UNIQUE INDEX makes duplicate posts impossible at the DB layer.
  • FOR UPDATE on the wallet row. Inside the withdrawal transaction. Pairs with a CHECK (balance >= 0) as a DB-level backstop. Belt-and-suspenders against double-spend.
  • Outbox table. Side-effects (SMS, email, webhooks) get inserted into an outbox table in the SAME transaction as the journal entry. A separate worker drains the outbox with FOR UPDATE SKIP LOCKED so many workers can run in parallel without double-delivering.
  • Balance projection. Cache the current balance on the wallet row for fast reads. Update it atomically inside the same transaction as every journal entry. If it ever drifts, rebuild from the ledger (which is always authoritative).
  • Partition by month. journal_entry gets partitioned by posted_at once it crosses billions of rows. Hot partitions stay on fast disk; cold ones move to cheap storage. A monthly snapshot makes historical-balance queries one row.
  • COALESCE every SUM. SUM() over zero rows returns NULL. NULL arithmetic propagates and breaks alerts and projections silently. Wrap every aggregate that could see an empty set: COALESCE(SUM(...), 0).
  • bigint, never float. Money is always stored in minor units (kobo, cents) as a bigint. Floating point loses pennies; numeric loses speed at scale. bigint is the answer.

Common mistakes

  • UPDATEing wallet.balance instead of posting a new entry. Breaks the audit trail. Always post the entry; let the projection follow.
  • Trusting a webhook to fire exactly once.Webhooks are at-least-once. If your handler isn't idempotent at the DB layer, you ship a double-booking bug in v1.
  • Storing money as a number, numeric without scale, or a float. The fintech version of off-by-one. bigint kobo forever.
  • Treating "wallet balance" as one number. It's at least three: available, pending, total. Use a reservation pattern from day one.
  • Computing running_balance and caching it on the line row. The next backdated entry breaks every cached balance below it. Compute the running balance at READ time.

Search lessons

Type to find any of the 85 lessons. Press Enter to open.