Design refundable transaction ledger and prioritization rules
Company: Airbnb
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Onsite
Quick Answer: This question evaluates knowledge of data structures and algorithms for transactional ledgers, including tracking payments and refundable balances, rule-based prioritization for allocating refunds, handling partial and multiple refunds, and ensuring correctness when payments and refunds interleave.
Part 1: Build Ledger Storage Indexes
Constraints
- 0 <= len(payments) <= 100000
- 0 <= len(refund_allocations) <= 100000
- paymentId values are unique positive integers
- amount and refundedAmount are non-negative integers
- Every refund allocation references an existing payment and total allocations per payment do not exceed its amount
Examples
Input: ([[1, 10, 100, 5], [2, 10, 50, 3], [3, 20, 70, 4]], [[1, 30], [3, 70]])
Expected Output: [[[1, 10, 100, 5], [2, 10, 50, 3], [3, 20, 70, 4]], [[1, 70], [2, 50], [3, 0]], [[10, 2, 1], [20, 3]]]
Explanation: Payment 1 has 70 remaining, payment 3 is fully refunded, and user 10's payments are ordered by timestamps 3 then 5.
Input: ([], [])
Expected Output: [[], [], []]
Explanation: Empty input produces empty storage views.
Input: ([[5, 99, 200, 10]], [])
Expected Output: [[[5, 99, 200, 10]], [[5, 200]], [[99, 5]]]
Explanation: A single payment with no refunds remains fully refundable.
Input: ([[1, 1, 100, 1], [2, 1, 50, 2]], [[1, 20], [1, 30], [2, 10]])
Expected Output: [[[1, 1, 100, 1], [2, 1, 50, 2]], [[1, 50], [2, 40]], [[1, 1, 2]]]
Explanation: Multiple allocations against the same payment are accumulated.
Hints
- A hash map keyed by paymentId gives direct access to original and remaining payment data.
- For the per-user view, group payment IDs by userId and sort each group by timestamp.
Part 2: Simulate Basic Ledger APIs
Constraints
- 0 <= len(operations) <= 100000
- IDs, amounts, and timestamps are integers
- paymentId and refundId values are positive integers
- Refund amounts are non-negative
- A rejected operation must not change ledger state
Examples
Input: ([[1, 1, 10, 100, 1], [3, 10], [2, 101, 10, 30, 2, 1], [3, 10]],)
Expected Output: [1, 100, 1, 70]
Explanation: The payment increases user 10's balance to 100; the refund decreases it to 70.
Input: ([[3, 42]],)
Expected Output: [0]
Explanation: Unknown users have balance 0.
Input: ([[1, 1, 10, 100, 1], [2, 101, 10, 120, 2, 1], [3, 10]],)
Expected Output: [1, 0, 100]
Explanation: The refund is rejected because it exceeds the payment's remaining refundable amount.
Input: ([[1, 1, 10, 100, 1], [1, 1, 10, 50, 2], [2, 101, 10, 40, 3, 1], [2, 101, 10, 10, 4, 1], [3, 10]],)
Expected Output: [1, 0, 1, 0, 60]
Explanation: Duplicate payment and duplicate refund IDs are rejected.
Hints
- Keep one map from paymentId to its user and another map from paymentId to remaining refundable amount.
- Maintaining a cached balance per user makes getUserBalance constant time.
Part 3: Allocate an Unreferenced Refund by Priority Rules
Constraints
- 0 <= len(payments) <= 100000
- 0 <= refund amount <= 10^12
- paymentId values are unique positive integers
- remainingAmount values are non-negative integers
- If no rules are provided, default to OLDEST_FIRST then ID_ASC
Examples
Input: ([[1, 10, 70, 5], [2, 10, 50, 8], [3, 20, 100, 9]], [10, 80, 10], ['RECENT_FIRST'])
Expected Output: [[2, 50], [1, 30]]
Explanation: Recency-first chooses payment 2 before payment 1.
Input: ([[1, 10, 40, 5], [2, 10, 70, 3], [3, 10, 70, 7]], [10, 100, 9], ['LARGEST_REMAINING_FIRST', 'OLDEST_FIRST'])
Expected Output: [[2, 70], [3, 30]]
Explanation: Payments 2 and 3 tie on remaining amount, so OLDEST_FIRST chooses payment 2 first.
Input: ([[1, 10, 20, 1]], [10, 25, 2], ['OLDEST_FIRST'])
Expected Output: []
Explanation: Only 20 is refundable, so a refund of 25 cannot be fully covered.
Input: ([[1, 10, 20, 1]], [10, 0, 2], [])
Expected Output: []
Explanation: A zero-amount refund needs no allocations.
Input: ([[1, 10, 50, 5], [2, 10, 50, 5]], [10, 60, 5], ['ID_DESC'])
Expected Output: [[2, 50], [1, 10]]
Explanation: ID_DESC chooses payment 2 before payment 1.
Hints
- Convert each priority rule into a component of a sort key.
- After sorting eligible payments, greedily consume each payment's remaining amount until the refund is covered.
Part 4: Process Multiple Partial Refunds per Payment
Constraints
- 0 <= len(payment_amounts) <= 100000
- 0 <= len(refund_requests) <= 100000
- paymentId values in payment_amounts are unique positive integers
- Payment and refund amounts are non-negative integers
- Rejected requests must not change remaining refundable amounts
Examples
Input: ([[1, 100]], [[1, 30], [1, 50], [1, 25], [1, 20]])
Expected Output: [[1, 30, 70], [1, 50, 20], [0, 0, 20], [1, 20, 0]]
Explanation: The third refund is rejected because only 20 remains; the fourth refund exactly consumes the remainder.
Input: ([], [[1, 10]])
Expected Output: [[-1, 0, 0]]
Explanation: There are no payments, so the referenced payment is unknown.
Input: ([[7, 100]], [[7, 100], [7, 1]])
Expected Output: [[1, 100, 0], [0, 0, 0]]
Explanation: After a full refund, no additional refund is allowed.
Input: ([[1, 0]], [[1, 0]])
Expected Output: [[1, 0, 0]]
Explanation: A zero-amount refund against a zero-amount payment is valid and changes nothing.
Input: ([[1, 50], [2, 30]], [[1, 20], [2, 30], [1, 30]])
Expected Output: [[1, 20, 30], [1, 30, 0], [1, 30, 0]]
Explanation: Remaining refundable amounts are tracked independently per payment.
Hints
- Store only the remaining refundable amount for each payment; you do not need to recompute from history.
- A refund is valid exactly when requested amount <= remaining[paymentId].
Part 5: Process Interleaved Payments and Refunds with Allocations
Constraints
- 0 <= len(records) <= 100000
- Payment and refund IDs are positive integers and unique within their own type
- Amounts are positive integers
- An unreferenced refund may use only payments already ingested, with the same userId, timestamp <= refund timestamp, and positive remaining amount
- If rules is empty, default to OLDEST_FIRST then ID_ASC
Examples
Input: ([[1, 1, 10, 100, 1], [2, 101, 10, 30, 2, -1], [1, 2, 10, 50, 3], [2, 102, 10, 80, 4, -1]], ['RECENT_FIRST'])
Expected Output: [[[1, 30]], [[2, 50], [1, 30]]]
Explanation: The first refund uses payment 1. The second refund uses the most recent payment 2 first, then payment 1.
Input: ([[1, 1, 10, 100, 1], [1, 2, 10, 40, 2], [2, 201, 10, 60, 3, 1], [2, 202, 10, 50, 4, 1]], ['OLDEST_FIRST'])
Expected Output: [[[1, 60]], []]
Explanation: The second explicit refund against payment 1 is rejected because only 40 remains.
Input: ([[2, 101, 10, 20, 1, -1], [1, 1, 10, 50, 2], [2, 102, 10, 20, 3, -1]], ['OLDEST_FIRST'])
Expected Output: [[], [[1, 20]]]
Explanation: A refund before any eligible payment is rejected; the later refund can use the ingested payment.
Input: ([], ['RECENT_FIRST'])
Expected Output: []
Explanation: No records means no refund outputs.
Input: ([[1, 1, 10, 30, 1], [2, 101, 10, 50, 2, -1], [2, 102, 10, 20, 3, -1]], [])
Expected Output: [[], [[1, 20]]]
Explanation: The insufficient refund is rejected without changing payment 1, so the later refund can still use it.
Hints
- For an unreferenced refund, first collect candidate payments and check that total remaining amount is sufficient before mutating state.
- Use the same sort-key idea from configurable priority rules, then greedily allocate.
Part 6: Estimate Ledger Operation Complexity Costs
Constraints
- 0 <= len(operations) <= 100000
- userId values are positive integers
- For unreferenced refunds, 0 <= touchedPayments <= current number of payments for that user
- The estimator tracks payment counts only; refunds do not remove payments from the count
- All costs fit in a 64-bit signed integer
Examples
Input: ([],)
Expected Output: [0, 0, 0, 0]
Explanation: No operations means both designs have zero cost.
Input: ([[1, 10], [1, 10], [2, 10, 0, 1], [3, 10]],)
Expected Output: [6, 7, -1, 2]
Explanation: For this small workload, indexed insert overhead is not recovered.
Input: ([[1, 10], [1, 10], [1, 10], [1, 10], [1, 10], [2, 10, 0, 0], [2, 10, 0, 0], [2, 10, 0, 0], [2, 10, 0, 0], [2, 10, 0, 0]],)
Expected Output: [35, 19, 1, 5]
Explanation: Repeated unreferenced refunds with zero touched payments are much cheaper under the indexed model.
Input: ([[1, 1], [1, 2], [1, 1], [2, 1, 0, 1], [2, 2, 0, 1]],)
Expected Output: [8, 10, -1, 3]
Explanation: Payment counts are maintained separately per user.
Input: ([[1, 5], [2, 5, 1, 0], [3, 5]],)
Expected Output: [3, 4, -1, 1]
Explanation: A referenced refund is constant time in both designs, so the indexed add cost makes it more expensive here.
Hints
- Maintain the current number of payments per user while scanning operations once.
- ceil(log2(x)) for integer x >= 1 can be computed with bit_length.