Design a Timestamped Cloud Credit Ledger with Rebates, Merges, and History
CoinbaseSoftware Engineer · Online AssessmentLast updated
Design an in-memory cloud credit ledger that processes top-ups, charges with delayed deterministic rebates, account merges with canonical alias resolution, historical balance lookups, and spending leaderboards.
Problem Statement
Implement an in-memory credit ledger system, CloudCreditLedger, that manages user balances across discrete timestamps. All incoming operations arrive with non-decreasing timestamps.
The system must support top-ups, charges with delayed percentage-based rebates, merging accounts, historical point-in-time balance lookups, and spending rank queries.
Requirements
-
Rebate Processing:
- Charges may schedule a delayed rebate:
rebate_amount = floor(amount * rebate_pct / 100)to be credited attimestamp + rebate_delay. - Before executing any operation at time
T, all scheduled rebates withscheduled_time <= Tmust be applied. - If multiple rebates mature at the same timestamp, process them in the order their corresponding charges were created.
- Rebates credit the canonical target account (resolving merges) and do not reduce the account's gross spending record.
- Charges may schedule a delayed rebate:
-
Account Merges:
- Merging
source_idintotarget_idat timeTtransferssource_id's remaining balance atTintotarget_id. - Subsequent operations referencing
source_idare automatically redirected totarget_id. - Account merges are transitive (if A merges into B, and B merges into C, A resolves to C).
- Historical balance queries for
source_idatquery_time < Tmust return the exact balancesource_idpossessed atquery_timebefore the merge.
- Merging
-
Spending Leaderboard:
- Accounts are ranked by total gross credits spent via successful charges.
- When accounts merge, their historical gross spending totals are merged into the target account.
- Ties in total spending are broken alphabetically by the canonical account ID.
Class Interface
class CloudCreditLedger:
def __init__(self):
pass
def top_up(self, timestamp: int, account_id: str, amount: int) -> bool:
"""Adds amount to the account's balance. Returns True if amount > 0, else False."""
pass
def charge(self, timestamp: int, account_id: str, amount: int, rebate_delay: int, rebate_pct: int) -> bool:
"""Attempts to charge amount. If canonical balance >= amount, deducts amount and
schedules a rebate for floor(amount * rebate_pct / 100) at timestamp + rebate_delay.
Returns True on success, False if insufficient balance or amount bool:
"""Merges source_id into target_id. Returns False if source_id == target_id,
or if either account does not exist. Otherwise transfers source balance to target."""
pass
def get_balance(self, timestamp: int, account_id: str) -> int:
"""Returns the current balance of account_id's canonical account after processing
rebates up to timestamp. Returns -1 if account does not exist."""
pass
def get_historical_balance(self, timestamp: int, account_id: str, query_time: int) -> int:
"""Returns the balance account_id specifically held at query_time (query_time <= timestamp).
Does NOT resolve to merge target if query_time is prior to account_id being merged.
Returns -1 if account did not exist at query_time."""
pass
def get_top_spenders(self, timestamp: int, k: int) -> list[str]:
"""Returns up to k canonical account IDs with the highest gross spending.
Ties broken alphabetically."""
pass
Constraints
- Timestamps in method calls are strictly non-decreasing:
t_prev <= t_curr. 0 <= query_time <= timestamp <= 10^91 <= amount <= 10^60 <= rebate_delay <= 10^6,0 <= rebate_pct <= 100- Up to
100,000total method invocations.
Examples
ledger = CloudCreditLedger()
ledger.top_up(10, "acc_1", 100) # True (acc_1 balance: 100)
ledger.charge(12, "acc_1", 60, 5, 50) # True (acc_1 balance: 40; rebate of 30 due at t=17)
ledger.get_balance(15, "acc_1") # 40 (rebate not yet due)
ledger.get_balance(17, "acc_1") # 70 (rebate of 30 applied)
ledger.get_historical_balance(18, "acc_1", 14) # 40
ledger.top_up(20, "acc_2", 50) # True
ledger.merge_accounts(25, "acc_1", "acc_2") # True (acc_1 merged into acc_2; acc_2 balance: 70 + 50 = 120)
ledger.get_balance(26, "acc_1") # 120 (resolves to acc_2)
ledger.get_historical_balance(26, "acc_1", 20) # 70 (pre-merge balance of acc_1)
ledger.get_top_spenders(26, 2) # ["acc_2"] (acc_2 absorbed acc_1's 60 spent)
Hints
Hint 1
Use a min-heap to track pending rebates keyed by scheduled time and charge creation order, flushing mature rebates at the start of every method call.
Hint 2
Maintain a Disjoint Set Union (DSU) or parent pointers for canonical account resolution after merges, but log balance mutation checkpoints per account for historical queries.
Hint 3
For historical balance lookups, store a sorted list of (timestamp, balance) snapshots on each distinct account and use binary search (bisect) to find the state at query_time.
Follow-up questions
- How would you handle out-of-order timestamp events if operations were received from multiple microservices concurrently?
- If millions of accounts merge into deep chains, how does path compression impact the ability to query historical merge relationships at specific timestamps?
- How would you design the storage layer if historical snapshots exceed main memory and require persistence?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.