LLD-018 · LOW LEVEL DESIGN
Wallet / Payment Ledger
Preserve balanced postings and available funds across holds, capture, release and reversal.
Local Java 21 reference verified: 227 checks. Learner drafts are not executed. Author self-review; independent review pending.
Existing introductory lesson · Version 1
Understand the problem
Design a local one-currency wallet ledger with funded accounts, atomic transfers, reservations, capture/release and append-only reversals. Distinguish posted from available balance, prevent concurrent overspending and explain why a compensating transfer can fail after funds were spent or reserved.
Before you start
- Exact integer money
- Balanced postings and immutable history
- Atomic multi-account updates
- Idempotency and reservation lifecycle
Scope and explicit decisions
- Synthetic one-currency in-memory ledger. It does not connect to banks/payment rails, authenticate users, convert currencies, reconcile external statements or implement a production regulated account system.
- Constructor funding is a fixture. An internal @clearing contra-account offsets all initial wallet balances so opening postings sum to zero. Aggregate initial funding must fit a nonnegative long; this bounds the model’s capital. The clearing account cannot be used as a normal transfer wallet.
- Money is integer minor units. Wallet balances and active reserves stay nonnegative; available=posted-reserved. Every normal posting conserves total value including the clearing account. No overdraft or new external deposit/withdrawal API is implemented.
- Transfers atomically debit and credit two different registered wallets. A reservation does not post a transfer; it reduces available funds. Capture atomically releases that reservation and posts its transfer to a different wallet.
- Hold release is idempotent by hold identity. Captured holds cannot be released, and released holds cannot be captured. Holds have no TTL or automatic cleanup; a higher-level owner must resolve abandoned reservations.
- Transfer/reserve/capture/reverse keys share a ledger namespace and use typed fingerprints. Matching retries return the original posting or current hold identity; conflicting operation reuse rejects.
- Reversal appends an inverse of an original non-opening posting once. It must satisfy current available-fund constraints, so it may fail when the recipient spent or reserved the money. Reversing a capture does not reactivate its historical hold.
- All checked balance updates are prepared in a temporary map before publication. Histories, holds and operation results are retained in memory; no restart durability or retention policy is provided.
Worked examples
Input: A100/B0, transfer30 to B, reserve50 from A.
Outcome: A posted70/available20; B30; clearing-100. Capture50 gives A20/B80.
A reservation changes spending power before it changes posted ownership.
Input: B reserves all80, then reverse the captured50.
Outcome: Reversal rejects. After releasing B’s hold, inverse posting restores A70/B30.
Compensation is another constrained business transaction, not guaranteed rollback.
Try before revealing
Hint 1
Where does the opposite side of opening funding appear?
Hint 2
Which funds are posted but unavailable?
Hint 3
Can capture and release both win one active hold?
Requirements and invariants
R1 — Every posted transaction conserves value and projection equals ledger replay. transfer protects I1.
R2 — Normal wallets cannot spend or reverse below active reserved funds. nextBalances protects I2.
R3 — Capture and release cannot both consume one active hold. capture protects I3.
R4 — Retries and corrections retain stable identities without rewriting original history. reverse protects I4.
I1: Every posting delta and the full balance map including clearing sum to zero; rebuild equals the materialized balances.
I2: For each wallet, 0<=reserved<=posted, hence available>=0.
I3: A hold transitions ACTIVE→CAPTURED or ACTIVE→RELEASED once; only ACTIVE contributes to reserved funds.
I4: Each operation key maps one typed request to one result, and each original posting has at most one accepted reversal.
Derive the design
An available-balance number is not an accounting model
A simple map of wallet totals can check a debit, but without immutable balanced entries it cannot explain history or recover a projection. The reference stores postings plus a rebuildable balance map, then separates reserved funds from posted value. One monitor protects the multi-account/hold transition; a concurrent map alone would not prevent compound overspending.
Alternative: Per-account locks can improve unrelated transfer throughput, but transfers need stable lock ordering and coherent idempotency/hold metadata. A durable ledger needs real transactional storage and tested isolation; an in-memory fake cannot prove those guarantees.
Choose account and amount semantics first
State currency, minor units, balance signs, overdraft and source of opening funds. The clearing entry makes conservation explicit instead of hiding initial value creation.
Separate encumbrance from ownership transfer
A hold is not an external charge or a posted transfer. Keeping reserved and posted separate makes available-fund checks precise.
Prepare every checked update before commit
Use a temporary next-balance map and a reserved map appropriate to capture. Validate all wallets before changing projection, posting and operation result.
Keep corrections visible and fallible
A reversal may fail under current availability. Preserve history and expose that failure rather than deleting the original posting or violating another active promise.
Responsibilities and ownership
Ledger
Owns: Balances, reservations, postings, dedupe results and reversal index
Methods: transfer, reserve, capture, release, reverse
One local atomic owner coordinates all involved wallets.
Posting
Owns: Immutable balanced deltas and optional original-reference
Methods: Record accessors
Original financial facts are not edited by correction.
Hold
Owns: Wallet, amount and reservation lifecycle identity
Methods: Immutable state snapshots
Not a posted debit until captured; no hidden expiration.
Typed operation fingerprints / clearing
Owns: Retry meaning / opening-value offset
Methods: Structured equality / immutable opening posting
Fixture funding is not an external deposit or bank reconciliation.
Synthetic local accounting only; no external banking settlement is implied.
- Typed transfer/capture request
↓ Resolve retry identity
Prepare zero-sum deltas↓ Account for active holds
Validate all available balances↓ One local transaction owner
Commit posting + projection + result
Only one terminal path wins. Reversing a capture posting does not reactivate the hold.
Atomic debit/credit and reserve release
No posting; clear encumbrance
Data model, schema and relationships
A wallet projection is derived from immutable balanced journal entries. Holds encumber available funds without posting money; capture converts an active hold into a balanced transfer exactly once.
Implementation boundary: The reference uses signed exact deltas, one synthetic clearing account and local reservation maps. These tables describe internal one-currency accounting; external settlement and regulated account types are separate.
These are proposed logical schemas with field contracts, not executable database migrations. PK identifies a row, FK references an existing parent, UK is a uniqueness rule. A composite PK identifies the combination. Nullable references are marked with ?. A foreign key protects identity; cross-row limits, timing and state transitions also require the transaction rules below.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Entity definitions and field contracts
wallet_account
One currency balance owner.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | text · PK | Account identity |
currency | text · VALUE | Declared currency |
posted | bigint · VALUE | Derived signed balance |
reserved | bigint · VALUE | Active hold projection |
Code mapping: Wallet balances and reserved maps.
- Normal wallet posted>=reserved>=0; internal clearing has explicit signed exemption.
journal_entry
Immutable balanced operation.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Posting identity |
reverses_id | uuid? · FK | Optional original posting · journal_entry.id |
operation_key | text · UK | Logical command identity |
sequence | bigint · UK | Monotonic posting order for replay; all lines use the same entry sequence |
Code mapping: Posting record.
- Sum of entry line deltas is zero; one permitted reversal per original under reference policy.
journal_line
Account delta inside one posting.
| Field | Type / key | Meaning and reference |
|---|---|---|
entry_id | uuid · PK,FK | Balanced entry · journal_entry.id |
account_id | text · PK,FK | Affected account · wallet_account.id |
delta | bigint · VALUE | Exact signed minor-unit change |
entry_sequence | bigint · VALUE | Immutable posting sequence copied consistently from entry for account history scans |
Code mapping: Posting deltas map.
- PK(entry_id,account_id); validate full entry sum with overflow-safe arithmetic.
wallet_hold
Active encumbrance lifecycle.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Hold identity |
account_id | text · FK | Funds owner · wallet_account.id |
amount | bigint · VALUE | Reserved amount |
state | enum · VALUE | ACTIVE CAPTURED RELEASED |
Code mapping: Hold record.
- One terminal path wins; captured hold does not reactivate on posting reversal.
wallet_operation
Typed immutable retry evidence.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | text · PK | Operation key |
account_id | text · FK | Initiator scope · wallet_account.id |
fingerprint | json · VALUE | Operation kind and exact inputs |
result_id | text · VALUE | Recorded hold/posting identity |
Code mapping: Operation fingerprints.
- Global namespace rejects same key with different operation meaning.
Relationship cardinalities and their reasons
| Parent → child | Multiplicity | FK / why |
|---|---|---|
| journal_entry → journal_entry is corrected by | 0..1 parent per child; 0..1 children per parent | journal_entry.reverses_id → journal_entry.idInverse entries append history; at most one reversal is an additional uniqueness constraint. |
| journal_entry → journal_line contains deltas | 1 parent per child; 0..* children per parent | journal_line.entry_id → journal_entry.idAn entry must have enough lines to express conservation; FK alone cannot enforce cross-row zero sum. |
| wallet_account → journal_line receives deltas | 1 parent per child; 0..* children per parent | journal_line.account_id → wallet_account.idMany-to-many account/entry facts are resolved through immutable lines. |
| wallet_account → wallet_hold encumbers funds | 1 parent per child; 0..* children per parent | wallet_hold.account_id → wallet_account.idACTIVE hold sum equals reserved projection. |
| wallet_account → wallet_operation initiates operations | 1 parent per child; 0..* children per parent | wallet_operation.account_id → wallet_account.idRetry identity protects both financial effects and encumbrance state. |
Transaction boundaries
- Transfer: lock all affected accounts in stable order, resolve retry key/fingerprint, validate exact balanced deltas/availability, append entry/lines and update projection atomically.
- Capture: condition ACTIVE hold, remove its encumbrance, apply debit/credit and store operation result in the same transaction.
- Reversal: append inverse lines only if present available constraints permit; never edit old journal facts.
Indexes and uniqueness
- PK entry/account for replay; index journal_line(account_id,entry_sequence).
- Unique operation key; unique non-null reverses_id under one-reversal policy; index active wallet_hold(account_id).
Schema traps and failure cases
- A row CHECK cannot enforce cross-row posting sum; use a single transactional posting owner/deferred constraint mechanism.
- Balanced internal entries do not prove bank settlement. Unknown external transfer requires reconciliation evidence.
Trace the records through one operation
- Opening balances have an opposing synthetic clearing posting.
- Hold H reserves 40, reducing available but not posted balance.
- Capture H posts payer -40 and payee +40; racing release loses terminal transition.
Method contracts
Ledger(initialFunding)
Validate accounts/amounts and checked aggregate capital. Create balanced opening posting against @clearing.
transfer(from,to,amount,key) → Posting
Resolve matching key, validate wallets/positive amount, prepare balanced updates and enforce posted>=reserved before committing one entry.
reserve(wallet,amount,key) → Hold
Create an ACTIVE hold only within current available funds; no posting is added. Same key resolves that hold identity.
capture(holdId,to,key); release(holdId)
Capture releases reserved amount and posts its transfer together. Release only clears an ACTIVE reservation; repeats of RELEASED are no-ops.
reverse(postingId,key); balances(); rebuild()
Append one inverse subject to current funds. Rebuild includes opening and correction facts, preserving audit history.
What, when, why and how
Each concept is attached to the requirement it protects, its implementation, an alternative and a test.
Balanced postings and conservation
- what
- Every movement has equal and opposite account deltas.
- when
- Money-like value moves between wallets.
- why
- Independent debit/credit writes can create, lose or conceal value after partial failure.
- how
- Opening funding is offset by clearing; transfer/capture create zero-sum delta maps. nextBalances validates sum with BigInteger and computes checked next balances before commit.
Code: Posting, nextBalances, commit · Invariants: I1
Alternatives, cost and failure example
Alternative: A mature accounting model may distinguish account types and debit/credit conventions explicitly; this reference uses signed deltas for clarity.
Cost: The implementation copies M balances per posting and checks all wallet constraints, so update is O(M+P), not universally O(1).
Counterexample: Debit A succeeds, credit B fails and history still claims a completed transfer.
Test: Assert zero sum and rebuild equality after each seeded transfer and every race result.
Posted versus available balance
- what
- Reservations encumber funds without yet moving posted value.
- when
- A payment is authorized/reserved before final capture.
- why
- Using posted balance alone lets another transfer spend money already promised to a hold.
- how
- reserve increases reserved only. available subtracts it. transfer/reversal validate next posted balances against current reserves; capture lowers reserve and posts debit/credit atomically.
Code: reserve, available, capture · Invariants: I2, I3
Alternatives, cost and failure example
Alternative: A separate pending ledger can express similar semantics, but consistency between pending and posted state still needs an atomic boundary.
Cost: Long-lived abandoned holds reduce availability indefinitely here; expiry/recovery belongs to a separate explicit policy.
Counterexample: A has70 posted and50 reserved, but a transfer of21 is accepted by checking only70.
Test: Reserve50 after balance70 and reject transfer21; capture50 transfers it once while preserving available20.
Idempotent result and exclusive hold terminal state
- what
- Retries resolve the original request, while capture/release race for one active reservation.
- when
- Network retries or concurrent settlement/cancellation occur.
- why
- Duplicating a transfer or both capturing and releasing a hold violates value/availability guarantees.
- how
- Typed operation records compare exact business arguments. Capture checks ACTIVE after repeat resolution; release returns RELEASED idempotently but rejects CAPTURED. All share one monitor.
Code: Operation, capture, release · Invariants: I3, I4
Alternatives, cost and failure example
Alternative: Durable unique operation keys and conditional hold-state writes are needed for multiple service instances.
Cost: Result retention grows and must outlive the supported retry horizon.
Counterexample: Capture and release independently read ACTIVE, then both mutate reserved accounting.
Test: Barrier-controlled capture/release race yields one winner; matching capture retry returns the same Posting.
Reversal is a new constrained transaction
- what
- Correct a posted fact through an inverse rather than erasing history.
- when
- An accepted transfer needs business reversal.
- why
- The recipient may no longer have available funds, so compensation cannot be assumed infallible.
- how
- reverse negates original deltas, checks current reserves/funds, appends a linked Posting and records one reversal. Opening and reversal-of-reversal are excluded.
Code: reverse, rebuild · Invariants: I1, I2, I4
Alternatives, cost and failure example
Alternative: A business liability/manual-review account can represent an owed refund when direct reversal cannot succeed, but that is a different declared policy.
Cost: A failed reversal leaves the original fact intact and may require a real-world recovery process; this class does not invent funds.
Counterexample: Reverse capture from B while all B funds are reserved elsewhere.
Test: Reject without adding history, then release the encumbrance and append the inverse successfully.
Read and run the Java reference
Reveal complete Solution.java
Java 21. Save as Solution.java; compile with javac --release 21. The repository also contains Test.java and the lld:java verification command. This reference implements the local contract above.
nextBalances validates conservation and availability
BigInteger sum avoids overflow in the validation itself. Checked long deltas are prepared before updating authoritative state.
capture uses post-release reserved requirements
The hold amount is removed from the temporary reserve map before validating the debit, so reserved funds can be consumed by their own capture without double encumbrance.
previous resolves typed retry meaning
One key cannot become a different operation or change amount/recipient. Result lookup happens before terminal hold checks on repeated capture.
reverse does not rewrite hold history
A captured hold remains CAPTURED after its posting is reversed. The inverse changes money facts; it does not silently create a new active authorization.
Complexity and limits
- Posting with M wallets and P deltas copies/checks O(M+P) state; map lookup is expected O(1). Reserve/release are expected O(1) apart from stored record copies.
- Capture copies reserved/balance maps and validates all wallets: O(M). Rebuild costs O(M+total retained delta entries).
- Space grows with wallets, postings, holds and operation/reversal history. Queue or hold counts do not bound byte retention automatically.
- One monitor avoids local account-lock deadlocks but serializes unrelated transfers. It gives no cross-process durability, real payment settlement or regulatory guarantees.
Walk through the actual reference
These walkthrough fixtures are emitted only after the Java harness verifies their operations and expected state. Concurrency tests additionally exercise competing callers.
Posted and available funds
Step 1 of 3: Fund A100, transfer30 to B
A70/B30, clearing-100
Invariant: All posting deltas sum to zero
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Fund A100, transfer30 to B | A70/B30, clearing-100 | All posting deltas sum to zero |
| Reserve50 from A | A posted70, available20 | Reserved funds cannot be spent again |
| Capture hold to B | A20/B80, hold CAPTURED | Reservation release and debit/credit commit together |
Reversal can fail when funds are encumbered
Step 1 of 2: Reserve all B80 then reverse capture
Rejected; posting history unchanged
Invariant: Correction cannot violate active reservations
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Reserve all B80 then reverse capture | Rejected; posting history unchanged | Correction cannot violate active reservations |
| Release B hold and reverse | Append inverse; A70/B30 | Original posting remains auditable |
Mistakes and counterexamples
Spend posted balance without subtracting holds
Failing case: A second transfer consumes already reserved funds.
Repair: Validate available funds and atomically coordinate capture/release.
Update debit and credit separately
Failing case: A failure occurs between them.
Repair: Commit balanced postings and projection together.
Assume reversal always succeeds
Failing case: Recipient has spent or encumbered the money.
Repair: Enforce current constraints and track business recovery explicitly.
Erase original transactions during correction
Failing case: Audit/rebuild cannot explain a disputed transfer.
Repair: Append an inverse with a stable original reference.
Engineer → Senior → Staff
Implement exact transfers and balance evidence.
Executed: opening conservation, transfers, exact amounts and deterministic projection rebuild.
Change exercise
Add a fee as a balanced multi-account posting rather than hidden subtraction.
Worked answer guidance
Include a fee recipient/account and preserve zero sum. Validate every resulting available balance atomically and keep the fee rule/version in operation identity.
Interview follow-ups
Why is clearing negative?
It offsets synthetic opening funding in this simplified signed-delta model. It is not a normal wallet that callers may spend.
Why use BigInteger for sum validation?
Even if each amount fits long, summing several deltas can overflow a long before detecting imbalance.
Your design attempt
Each language has a separate draft across all three levels. Saving is device-only and does not sync to your account. Anyone using this browser profile can load it. Loading is always explicit.
Java 21 reference code is provided for local study. Use Run program in the editor to execute your code and test driver. Program runs show output; they do not grade your design.
Run program · Java 21
No unsaved changes · 208/20000
Self-review
Checks apply to this visit. Self-review is not a passed submission or verified mastery.
Provenance and scope of evidence
Original teaching material. The local contract is implemented by Solution.java and tested by Test.java on Java 21. Distributed persistence, real provider/device adapters and deployment are not claimed.
Reference verification is not independent editorial review, production deployment verification or a claim that distributed adapters are implemented.