Interview conceptSoftware Engineering Fundamentals

Payment Ledgers, Reconciliation, And State Machines

Asked of: Software Engineer

Last updated

Editorial architecture diagram showing idempotent payment commands flowing through a state machine into an append-only ledger, settlement, and reconciliation checks.

What's being tested

These problems test whether you can model money movement as explicit state transitions rather than as ad hoc mutations to a balance. You need to preserve monetary precision, deterministic ordering, idempotency, and invariants while handling rejection, retries, cancellation, capture, overdraft, and settlement. Stripe cares because a small correctness bug in a ledger can create duplicated funds, missing funds, or unreconcilable records; interviewers are probing both implementation discipline and your ability to explain tradeoffs clearly.

Core knowledge

  • Integer fixed-point money stores the smallest currency unit, such as cents, rather than floating-point values. For a percentage fee, calculate with integer arithmetic and document rounding: fee = floor(amount * rate / denominator) or the explicitly specified half-up rule.

  • A ledger should be append-oriented: record immutable entries such as debit, credit, fee, reversal, and settlement, then derive balances. A mutable balance cache improves reads, but it must be reproducible from entries and updated atomically with the authoritative write.

  • Use a state machine with enumerated states and an explicit transition table. For example, accepted -> captured -> settled, while accepted -> canceled may be valid; reject illegal transitions such as settled -> canceled unless a separate reversal operation exists.

  • Keep acceptance, authorization or limit usage, and settlement distinct. A payment can be accepted by business validation, consume an account limit, and remain unsettled; collapsing these into one boolean makes refunds, retries, and reconciliation ambiguous.

  • Idempotency means repeating the same logical command has the same effect as applying it once. Key commands by a stable operation or idempotency key, persist the result, and reject reuse with different payloads rather than silently applying a second mutation.

  • Define ordering precisely: sort by timestamp only when timestamps are guaranteed unique; otherwise use (timestamp, sequence_number) or another stable tie-breaker. Never rely on input order unless the contract explicitly guarantees it.

  • Maintain invariants such as ∑debits−∑credits=net balance change\sum \text{debits} - \sum \text{credits} = \text{net balance change} and, for a double-entry ledger, total debits equaling total credits. Assert these during tests and expose violations rather than repairing balances silently.

  • Reconciliation compares two independently produced views, such as internal ledger entries and processor settlement records. Match on stable identifiers, amount, currency, and lifecycle status; classify unmatched, duplicated, amount-mismatched, and timing-difference cases for deterministic remediation.

  • Process each account-currency pair independently. A balance for USD must not offset a balance for EUR, and an account identifier alone is insufficient as a key; use a composite key such as (account_id, currency).

  • For overdrafts, make the coverage rule explicit: consume the account’s available balance first, then debit a platform or reserve account for the deficit, or reject atomically. Preserve the invariant that the resulting account balance and platform coverage reflect the same accepted transaction.

  • Transactional atomicity matters whenever a command changes multiple records. In Postgres, a database transaction with row-level locking can protect a balance update; optimistic concurrency with a version column can work when conflicts are expected to be low.

  • Parsing is part of correctness: preserve empty fixed-position fields, validate field count and types before indexing, and separate syntax errors from semantic rejection. A parser should produce a typed command; the state machine should decide whether that command is legal.

Worked example — Design a Ledger with Separate Acceptance, Limit, and Settlement States

For “Design a Ledger with Separate Acceptance, Limit, and Settlement States,” I would first clarify the command grammar, whether deposits can be retried, whether timestamps determine ordering, and whether query output must reflect accepted commands only or settled funds. I would state that amounts are integer minor units, account balances are keyed by (account_id, currency), and commands are processed deterministically in the specified order. I would organize the solution around four pillars: parsing and validation, account initialization, explicit acceptance/limit/settlement state, and balance-query generation. I would represent each command’s lifecycle separately rather than using one status, because acceptance can succeed while settlement is delayed or limit usage remains active. I would also define the invariant that rejected commands have no financial side effects and that repeated command identifiers cannot create additional balance. A key tradeoff is whether to store only current balances or append immutable ledger entries plus a balance projection; the latter is easier to audit and replay, while the former is simpler and faster for a small coding exercise. I would explain atomic updates if acceptance changes both an account and a platform limit, using a transaction or a version check. If I had more time, I would add property-based tests for illegal transitions, duplicate commands, zero or negative amounts, currency isolation, and replaying the same input to obtain identical results.

A second angle

For “Design a Fixed-Position Payment Command State Machine,” the same principles apply, but the primary risk shifts from aggregation to parsing and transition legality. I would first preserve empty fields and validate exact field positions, then convert the parsed record into a typed command before applying business rules. The state table should distinguish approval, capture, cancellation, and retry, including whether a retry is idempotent or represents a new attempt. The important transfer is that every mutation needs a clear precondition, postcondition, and duplicate-command behavior, even when no balance is changed yet.

Common pitfalls

Analytical mistake: Treating a payment status as a single linear enum such as pending -> paid -> failed is tempting, but it cannot represent independent authorization, limit, and settlement dimensions. Use separate state or ledger concepts and document which transitions affect funds, limits, and reporting.

Pitfall: Using floating-point arithmetic or silently rounding fees can produce reconciliation differences of one or more minor units.

Communication mistake: Jumping directly into maps and loops without clarifying ordering, retries, currencies, or rejection semantics makes a correct implementation look accidental. State assumptions explicitly, write the invariants on a whiteboard, and call out what the prompt does not specify.

Depth mistake: Saying “wrap it in a database transaction” is not enough. Explain which records are locked or versioned, how duplicate commands are detected, what happens after a timeout, and how immutable entries or audit data let operators reconstruct the result.

Connections

An interviewer may pivot to double-entry bookkeeping, idempotency keys, optimistic concurrency control, or event sourcing and projections. They may also ask how to test invariants with property-based testing or how to diagnose a reconciliation break without mutating historical records.

Practice questions

Related concepts