Katkat

Design Add, Overwrite, Undo, and Redo for Billing State

RedditSoftware Engineer · Technical Screen

Last updated

Design an in-memory billing account state manager that supports delta adjustments, absolute balance overwrites, and robust undo/redo capabilities.

Problem Statement

In financial and billing systems, balance updates must be strictly auditable, deterministic, and reversible. You are tasked with designing an in-memory billing ledger class, BillingAccount, that tracks customer balances in integer cents (to avoid floating-point inaccuracies).

The ledger must support incremental updates (add_delta), absolute adjustments (overwrite_balance), and full reversible history (undo and redo).

Implement the BillingAccount class with the following methods:

  • __init__(initial_balance_cents: int = 0): Initializes the account. Raises ValueError if initial_balance_cents int: Adjusts current balance by amount_cents (can be positive or negative). If the resulting balance would be negative, the transaction must be rejected, the state unchanged, and a ValueError raised. On success, any existing redo history is invalidated, and the new balance is returned.
  • overwrite_balance(new_balance_cents: int) -> int: Explicitly sets the balance to new_balance_cents. Raises ValueError if new_balance_cents bool: Reverts the most recent mutation (add_delta or overwrite_balance). Returns True if an action was reverted, or False if there are no actions to undo.
  • redo() -> bool: Reapplies the most recently undone action. Returns True if an action was reapplied, or False if there are no actions to redo.
  • get_balance() -> int: Returns the current balance in integer cents.

All operations (add_delta, overwrite_balance, undo, redo, get_balance) must run in O(1) time.

Example

account = BillingAccount(100) # Initial balance: $1.00 (100 cents)

account.add_delta(50)         # Balance: 150 cents
account.overwrite_balance(300)# Balance: 300 cents
account.add_delta(-100)       # Balance: 200 cents

account.undo()                # Undoes -100 delta -> Balance: 300 cents. Returns True.
account.undo()                # Undoes overwrite -> Balance: 150 cents. Returns True.
account.redo()                # Redoes overwrite -> Balance: 300 cents. Returns True.

account.add_delta(20)         # Balance: 320 cents. Redo branch is now invalidated.
account.redo()                # Returns False (redo history was cleared)
account.get_balance()         # Returns 320

Constraints

  • 0 <= balance_cents <= 10^12
  • -10^10 <= amount_cents <= 10^10
  • At most 10^5 operations will be called across any test run.
  • Balance cannot drop below 0 at any point.

Hints

Hint 1

Use the Command pattern: store command records containing both the previous state and new state (or command-specific inverse metadata) to make undoing simple.

Hint 2

Maintain two stacks—`undo_stack` and `redo_stack`—and remember to clear `redo_stack` whenever a new forward operation (`add_delta` or `overwrite_balance`) succeeds.

Hint 3

Validate operations before mutating state or pushing records onto the history stack so invalid operations do not corrupt history.

Follow-up questions

  • How would you scale this design to support concurrent transactions while guaranteeing linearizability and idempotency?
  • If transaction history must persist across service restarts, how would you model the write-ahead log (WAL) and snapshotting strategy?
  • How would you implement a memory cap on the undo stack while maintaining deterministic auditability?