Core Entities
Customer: customer_id, name, date_of_birth, ssn_hash, address, kyc_status (PENDING, VERIFIED, REJECTED). Account: account_id, customer_id, account_type (CHECKING, SAVINGS, MONEY_MARKET), status (ACTIVE, FROZEN, CLOSED), balance (NUMERIC(18,2)), available_balance (balance minus pending holds), interest_rate, overdraft_limit, currency, opened_at, closed_at. Transaction: txn_id (UUID), account_id, txn_type (DEPOSIT, WITHDRAWAL, TRANSFER_IN, TRANSFER_OUT, FEE, INTEREST, REVERSAL), amount, direction (CREDIT, DEBIT), balance_after, description, reference_id, status (PENDING, POSTED, REVERSED), initiated_at, posted_at. Hold: hold_id, account_id, amount, reason (PENDING_ACH, DEBIT_CARD_AUTH, LEGAL), expires_at.
Account Balance Design
Two balance concepts: ledger_balance (the official posted balance after all settled transactions) and available_balance (ledger_balance minus active holds). Customers can spend up to available_balance, not ledger_balance. A debit card swipe creates a hold (reduces available_balance) immediately but only posts to ledger_balance when the merchant settles (1-3 business days). An ACH transfer is pending for 1-2 business days before posting. Implementation: available_balance = ledger_balance – SUM(holds.amount WHERE holds.expires_at > NOW() AND account_id = :id). For performance: maintain a cached available_balance column, updated atomically on each hold creation/release and transaction posting.
Transaction Atomicity
class AccountService:
def transfer(self, from_id: int, to_id: int,
amount: Decimal, idempotency_key: str) -> tuple:
# Idempotency check
if self.txn_repo.exists(idempotency_key):
return self.txn_repo.get_by_key(idempotency_key)
with self.db.transaction():
# Lock both accounts in consistent order to prevent deadlocks
accounts = self.repo.lock_accounts(
sorted([from_id, to_id]) # lock by ascending ID
)
src = next(a for a in accounts if a.account_id == from_id)
dst = next(a for a in accounts if a.account_id == to_id)
if src.available_balance overdraft_available:
raise InsufficientFundsError()
# Assess overdraft fee if using overdraft protection
self._charge_overdraft_fee(src)
# Post debit
src.balance -= amount
debit_txn = Transaction(from_id, 'TRANSFER_OUT', amount,
'DEBIT', src.balance, idempotency_key)
# Post credit
dst.balance += amount
credit_txn = Transaction(to_id, 'TRANSFER_IN', amount,
'CREDIT', dst.balance, idempotency_key)
self.repo.save_all([src, dst, debit_txn, credit_txn])
return debit_txn, credit_txn
Interest Calculation
Savings and money market accounts earn interest. Two calculation methods: simple interest (rate * balance * days/365) and compound interest (daily compounding: balance * (1 + rate/365)^days). Daily accrual: a batch job runs nightly for all active savings accounts. Computes the daily interest accrual: accrual = balance * daily_rate. Adds the accrual to an interest_accrued accumulator (not yet the balance — avoids tiny daily transactions). Monthly posting: on the last day of the month, post accumulated interest as a CREDIT transaction and reset the accumulator. This produces one readable interest transaction per month rather than 30 tiny daily ones. Rate tiers: higher balances earn higher rates. Store rate_tiers = [(0, 1000, 0.01), (1000, 10000, 0.02), (10000, None, 0.025)] on the account type. Apply the tier matching the balance for each calculation.
Overdraft Protection and Fees
When a transaction would bring the balance below zero: check overdraft_limit. If amount exceeds available_balance + overdraft_limit: reject with InsufficientFundsError. If within limit: allow the transaction, post an OVERDRAFT_FEE ($35 typically), update overdraft_used. Fee waiver rules: first overdraft per calendar year waived for good-standing customers, or waived if the balance is restored within 24 hours (some banks). Monthly overdraft fee cap: max 3 overdraft fees per day, max 10 per month (regulatory). Track fee_count_today and fee_count_month on the account. Linked account protection: if overdraft_protection_source_account_id is set, automatically transfer the shortfall from the linked account instead of charging a fee (e.g., savings → checking).
Asked at: Stripe Interview Guide
Asked at: Coinbase Interview Guide
Asked at: Shopify Interview Guide
Asked at: Airbnb Interview Guide