Template
Initial Problem Statement
You’re building an in-memory ledger for account balances with support for:
- deposits / withdrawals
- transactional semantics (begin/commit/rollback)
- “holds” (pending debits) that expire
- and
available_balancevsposted_balance
Design a class:
class Ledger:
def deposit(self, accountId: str, amount: int) -> None
def withdraw(self, accountId: str, amount: int) -> bool
def place_hold(self, accountId: str, amount: int, holdId: str, timestampMs: int, ttlMs: int) -> bool
def release_hold(self, accountId: str, holdId: str) -> bool
def capture_hold(self, accountId: str, holdId: str) -> bool
def posted_balance(self, accountId: str, timestampMs: int) -> int
def available_balance(self, accountId: str, timestampMs: int) -> int
def begin(self) -> None
def commit(self) -> bool
def rollback(self) -> bool
Semantics
Balances
- posted_balance: sum of completed deposits/withdrawals
- available_balance: posted_balance minus any active holds (pending debits)
Deposits / withdrawals
- deposit: always succeeds; increases posted
- withdraw: succeeds only if available_balance >= amount at time of call; decreases posted
Holds
A hold represents a pending debit (like a card authorization):
place_holdsucceeds only if available_balance >= amount at that time- A hold has
(holdId, amount, expiresAt)where expiresAt = timestampMs + ttlMs release_holdremoves an active hold (no posted change)capture_holdconverts an active hold into a posted withdrawal:- removes the hold
- subtracts hold amount from posted
Expiration
When calling posted_balance or available_balance (and also when placing/capturing holds), the system should treat expired holds as inactive
You may assume timestamps for a given account are non-decreasing across calls
Transactions
begin/commit/rollback apply to all operations above
begin()starts a transaction (can be nested)- During a transaction, all operations should be “tentative”
commit()persists the latest transaction layer into the next outer layer- rollback() discards changes in the latest layer
If commit() or rollback() is called with no open transaction, return false
Complexity requirements
Aim for near amortized per operation Holds expiration should not require scanning all holds every time
Example
L.deposit("A", 100)
L.place_hold("A", 30, "h1", t=0, ttl=10000) # true
L.available_balance("A", 0) -> 70
L.posted_balance("A", 0) -> 100
L.capture_hold("A", "h1") # true
L.posted_balance("A", 0) -> 70
L.available_balance("A", 0) -> 70
Constraints
Initial Thoughts, Questions
- Explain
expiresAt = timestampMs + ttlMstimestampMs= the time when the hold is created, in milliseconds since some reference (epoch or monotonic clock; doesn’t matter for the problem)ttlMs= time-to-live, in milliseconds — how long the hold should remain activeexpiresAt= the exact timestamp (ms) when the hold becomes inactive- So if you place a hold at time
1000withttlMs=5000, then expiresAt=6000 - At
timestamps >= 6000, that hold is expired and should no longer reduce available balance
- Are any API calls outside of a transaction immediately committed? If there was an API call without a transaction and a rollback() is called, do we remove the last API call? A better question is API + transaction1 + direct API call...if a rollback() is called do we remove direct API call or transaction1?
- Operations outside any transaction are immediately committed
rollback()only affects the most recent open transaction layer- If there is no open transaction,
rollback()returns false and does nothing - A direct API call after a committed block is not undone by rollback unless it was executed inside an open transaction
- Operations outside any transaction are immediately committed
deposit(100) # committed
begin()
withdraw(10) # tentative
deposit(5) # tentative
commit() # now both tentative ops become committed
deposit(20) # committed
rollback() # false (no open tx), does not undo deposit(20)
- Are transactions considered strictly ordered, or are there ID's associated with them?
- Transactions are strictly ordered by call sequence
- There are no transaction IDs
Implementation
- Storing monetary amounts via tuples - Dollars would be
BIG_INToutside of python, cents justINT - Transactions can be held on a stack where a new transaction gets
pushonto stack, and a commitpopone off - Holds themselves should be stored in a priority queue based on
expiresAt- This priority queue will get drained during any action that checks
available_balance, as holds directly affect only this part - Holds should also be stored in a dictionary by their
holdIdfor access - Any
capture_holdcall will focus onexpiresAtand time management
- This priority queue will get drained during any action that checks
- Always handle time based on parameters passed into API