Aggregate loans and match repayments
AffirmSoftware Engineer · Technical ScreenLast updated
Implement a transaction matching engine that aggregates user loans and allocates repayments chronologically to outstanding balances using a FIFO waterfall approach.
Problem Statement
In lending platforms, borrowers can take out multiple distinct loans over time and submit repayments that must be applied against their outstanding balances. Your task is to implement a loan matching engine that applies repayment transactions against existing loans.
Each loan has a loan_id, user_id, initial amount (in cents), and a timestamp. Each repayment has a payment_id, user_id, amount (in cents), and a timestamp.
Repayments should be allocated according to the following rules:
- Repayments must be processed in chronological order by payment
timestamp. - A repayment can only be applied to loans belonging to the same
user_idthat were issued on or before the payment'stimestamp(loan.timestamp <= payment.timestamp). - Outstanding loans for a user are paid off in first-in, first-out (FIFO) order based on
loan.timestamp. If two loans have the same timestamp, break ties lexicographically byloan_id. - If a repayment exceeds the remaining balance of the oldest loan, that loan is fully paid off, and the remaining repayment amount cascades to the next eligible loan.
- Any portion of a repayment that cannot be applied to an eligible loan (e.g., all current loans are paid off) is left unallocated.
Return a list of allocation records in the order they occur: {"payment_id": str, "loan_id": str, "amount_applied": int}.
Function Signature
def match_repayments(loans: list[dict], repayments: list[dict]) -> list[dict]:
pass
Input Formats
loans: List of dicts, each with keys:"loan_id"(str),"user_id"(str),"amount"(int),"timestamp"(int).repayments: List of dicts, each with keys:"payment_id"(str),"user_id"(str),"amount"(int),"timestamp"(int).
Constraints
1 <= len(loans), len(repayments) <= 10^51 <= amount <= 10^9(all amounts are positive integers)- All
loan_ids withinloansare unique. - All
payment_ids withinrepaymentsare unique. - Timestamps are non-negative integers.
Examples
Example 1
loans = [
{"loan_id": "L1", "user_id": "U1", "amount": 100, "timestamp": 1},
{"loan_id": "L2", "user_id": "U1", "amount": 150, "timestamp": 3},
]
repayments = [
{"payment_id": "P1", "user_id": "U1", "amount": 120, "timestamp": 4},
]
# Output:
[
{"payment_id": "P1", "loan_id": "L1", "amount_applied": 100},
{"payment_id": "P1", "loan_id": "L2", "amount_applied": 20}
]
Explanation: At timestamp 4, both L1 and L2 exist. P1 pays off L1 completely (100 applied), leaving 20 remaining from the payment, which is applied to L2.
Example 2
loans = [
{"loan_id": "L1", "user_id": "U1", "amount": 100, "timestamp": 5},
]
repayments = [
{"payment_id": "P1", "user_id": "U1", "amount": 50, "timestamp": 2},
]
# Output:
[]
Explanation: P1 occurred at timestamp 2, but L1 was not created until timestamp 5. The payment cannot be matched to future loans, so no allocations occur.
Hints
Hint 1
Group loans by user and sort them by timestamp and ID so you can easily access the oldest active loan for any given user.
Hint 2
Sort the repayments chronologically and maintain an active index or queue pointer per user to avoid re-iterating over already-satisfied loans.
Hint 3
Ensure you only match loans created on or before the current repayment's timestamp; loans with timestamps greater than the payment cannot receive funds.
Follow-up questions
- How would you modify your design to handle a streaming architecture where loans and repayments arrive via Kafka topics instead of static in-memory lists?
- If business requirements changed to allow unallocated repayment amounts to sit as user credit and automatically apply to future loans, how would your data structures adapt?
- How would you introduce support for interest accrual or fee priority (e.g., payment pays interest first, then principal)?
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.