Katkat

eSIM Data Wallet with Expiring Bundles and Late Usage Records

OpenAISoftware Engineer · Onsite

Last updated

Implement a prepaid data wallet where usage draws from the soonest-expiring bundle first and records arrive out of timestamp order, then add checkpointing so balance queries do not replay the full history. Tests event-sourced thinking, heap-based priority consumption, and cache invalidation.

A travel eSIM provider sells prepaid data bundles (for example "50 MB, valid for 10 days"). A customer's wallet can hold several bundles at once. Every bundle has a validity window, and any data left on it when it expires is lost. Usage records come from partner mobile networks in batches, often hours late, so events reach the billing service out of timestamp order.

Part 1: Wallet with expiry-first consumption

Implement:

class DataWallet:
    def add_bundle(self, bundle_id: str, amount: int, start: int, expires: int) -> None
    def consume(self, amount: int, at: int) -> None      # a usage record, in MB
    def balance(self, at: int) -> int                    # MB usable at time `at`
    def overage(self, at: int) -> int                    # total MB used without coverage up to `at`

Rules (treat these as already agreed with the interviewer):

  • A bundle is usable on the half-open interval [start, expires).
  • Usage draws from the usable bundle that expires soonest, then the next soonest, and so on. Ties go to the earlier start, then the smaller bundle_id.
  • If usable data can't cover a usage record, the wallet is drained to zero and the uncovered remainder is added to overage (billed separately). The balance never goes negative.
  • At equal timestamps, bundles take effect before usage.
  • Calls arrive in any timestamp order. Every answer must equal what you would get by applying all events received so far in timestamp order.
add_bundle("b1", 50, start=0, expires=10)
add_bundle("b2", 30, start=2, expires=6)
consume(20, at=4)                  # taken from b2
balance(4) -> 60     balance(6) -> 50   # b2's last 10 MB expire at 6
consume(15, at=3)                  # arrives late
balance(4) -> 45     balance(6) -> 45   # b2 is now used up before it expires
add_bundle("b0", 10, start=1, expires=3)   # late; expires before the t=3 usage
balance(2) -> 90     balance(4) -> 45
consume(100, at=8)
balance(8) -> 0      overage(8) -> 55     balance(7) -> 45

Part 2: Don't replay from the beginning every time

A heavy user's wallet accumulates hundreds of thousands of usage records, and almost all late records are only a little late. Replaying the whole history for every balance call is too slow. Make reads cheap when new events land near the end of the timeline, while keeping answers exactly correct for arbitrarily late events.

Constraints

  • Integer amounts and timestamps. expires > start, and bundle ids are unique.
  • Up to 10^5 events per wallet. Wallets are independent.

Hints

Hint 1

A late event can change the outcome of every later event. What do you need to store to recompute correctly?

Hint 2

Replay in (time, kind) order with a min-heap of active bundles keyed by expiry.

Hint 3

Expire bundles with expires <= t before applying each event, not only at query time.

Hint 4

For Part 2, a snapshot of the state after the first k events stays valid unless a new event is inserted before position k.

Follow-up questions

  • If records are never more than 6 hours late, how would you garbage-collect history safely?
  • How would you show a customer which bundle paid for a given day's usage, given that the answer can change when late records arrive?
  • How would you shard this across millions of wallets and keep replay consistent across service restarts?
  • What changes if usage records can be voided later, restoring data to the bundles they drew from?