Katkat

Add Nested Begin, Commit, and Abort Semantics to a Shopping Cart

DecagonSoftware Engineer · Technical Screen

Last updated

Design and implement an in-memory shopping cart that supports nested transactions via begin, commit, and abort operations with proper scope isolation.

Problem Statement

Design an in-memory transactional shopping cart class, TransactionalCart, that supports nested transactions. Nested transactions allow operations to be staged, committed to parent scopes, or rolled back cleanly without affecting outer scopes or the base cart state.

Requirements

Implement the following methods in TransactionalCart:

  • add_item(item_id: str, quantity: int) -> None: Adds quantity units of item_id. quantity will always be a positive integer.
  • remove_item(item_id: str, quantity: int) -> None: Removes quantity units of item_id. If quantity is greater than or equal to the current quantity of item_id, the item is removed entirely. If item_id does not exist in the cart, raise a KeyError.
  • get_quantity(item_id: str) -> int: Returns the current quantity of item_id visible in the current transaction scope. Returns 0 if the item is not present.
  • begin() -> None: Starts a new nested transaction scope.
  • commit() -> None: Merges the changes from the current nested transaction into the parent transaction scope (or base state if it was the outermost transaction). Raises an InvalidTransactionError if no transaction is active.
  • abort() -> None: Discards all modifications made in the current transaction scope, restoring state to the moment the corresponding begin() was called. Raises an InvalidTransactionError if no transaction is active.

All cart modifications must immediately be visible to subsequent queries in the current scope and any child scopes opened afterwards.

Example

cart = TransactionalCart()
cart.add_item("apple", 3)
assert cart.get_quantity("apple") == 3

cart.begin() # Depth 1
cart.add_item("apple", 2)
cart.add_item("banana", 5)
assert cart.get_quantity("apple") == 5

cart.begin() # Depth 2
cart.remove_item("apple", 4)
assert cart.get_quantity("apple") == 1
cart.abort() # Discards Depth 2 changes

assert cart.get_quantity("apple") == 5
assert cart.get_quantity("banana") == 5

cart.commit() # Commits Depth 1 changes to base state
assert cart.get_quantity("apple") == 5
assert cart.get_quantity("banana") == 5

Constraints

  • Nested transaction depth can reach up to 1,000 levels.
  • Total operations (reads, writes, transaction controls) will be up to 100,000.
  • item_id is an alphanumeric string of length 1 to 32 characters.
  • quantity is an integer where 1 <= quantity <= 10^6.

Hints

Hint 1

Consider whether a full snapshot copy at each `begin()` or a stack of diffs/undo actions scales better when transactions modify only a small subset of items.

Hint 2

If using a stack of delta maps, committing an inner transaction means merging its diffs into the parent level's diff rather than directly updating the root state.

Hint 3

Ensure operations like `remove_item` that completely eliminate an item record explicit 'tombstones' or absolute values in your diff layer so ancestor values are not inadvertently exposed.

Follow-up questions

  • How would you adapt your design if calculating the cart total requires dynamic discount evaluation that depends on cumulative basket value?
  • How would you handle thread-safety if multiple background tasks or client sessions share the same transaction tree?
  • How would you implement an append-only write-ahead log (WAL) to make these nested transactions crash-resilient without degrading read latency?