The coding question
You get log lines in JSON, some for bookings and some for payments. Produce a report for the payment operations team, such as totals per booking and bookings that aren't fully paid. Then handle the follow-ups the interviewer adds.
Ask a few questions first
| Ask | What we'll assume |
|---|---|
| What does a line look like? | One JSON object per line, with a type, a booking ID and an amount. For example, a booking line with type 'booking' and the total, and a payment line with type 'payment' and what was paid. |
| What are amounts in? | Whole cents, one currency for now. Floats would round badly with money. |
| Are lines in order? | Not always. A payment can show up before its booking, so build the report at the end. |
| What about broken lines? | Skip them and count them, so the team knows something's off. Don't crash on one bad line. |
| What goes in the report? | For each booking: what's due, what's paid and what's left. Then the bookings not fully paid, and payments for bookings we never saw. |
A couple of examples
| Input | Output |
|---|---|
| Booking B1 for 30000, payment of 10000 on B1, booking B2 for 12000, payment of 12000 on B2, payment of 5000 on B1 | B1: due 30000, paid 15000, outstanding 15000. B2: due 12000, paid 12000, outstanding 0. Not fully paid: ['B1'] |
| Payment of 8000 on B7, then booking B7 for 8000, a payment of 2500 on B9, the line 'not json', and a line with type 'refund' | B7 fully paid. Payments without a booking: ['B9']. Bad lines: 2, since refunds aren't handled yet |
| No lines | No bookings, nothing unpaid, 0 bad lines |
Start with brute force
For each booking line, go back through every line looking for payments with that booking ID and add them up. Then go through all the payments again to find ones with no booking.
Cost: O(n²) for n lines. Fine for a hundred lines, slow for a day's logs. It also means every new follow-up adds another full pass.
A better way
One pass is enough. Keep two hash maps: booking ID to amount due, and booking ID to amount paid. Each line updates one of them. The report is built from the maps at the end, which also handles payments that arrive before their booking.
The real test is the follow-ups. Interviewers add things like refunds, duplicate payments or currencies. So split the code into small parts: parse a line, handle it based on its type, and build the report. A map from type to handler means a new log type is one new function and one new entry, not an edit to a long if-else chain. That's a small bit of object-oriented design.
Say the complexity of each step as you go. Each line is O(1), the report is O(b log b) because we sort b bookings by ID, and memory is O(b).
- Parse each line as JSON. If it's broken, has an unknown type or is missing a field, count it as bad and move on.
- Look up the handler for its type and call it.
- A booking sets its amount due. A payment adds to its amount paid.
- At the end, go through bookings in ID order and work out what's outstanding.
- List bookings with something outstanding and payments for unknown bookings.
The code
import json
def read_amount(event):
amount = event["amount"]
if type(amount) is not int:
raise ValueError("amounts are whole cents")
return amount
class PaymentReport:
def __init__(self):
self.due = {} # booking_id -> amount the guest owes, in cents
self.paid = {} # booking_id -> amount paid so far, in cents
self.bad_lines = 0
# One small function per log type, so a new type is one new entry.
self.handlers = {"booking": self.on_booking, "payment": self.on_payment}
def add_line(self, line):
try:
event = json.loads(line)
self.handlers[event["type"]](event)
except (ValueError, KeyError, TypeError):
# Broken JSON, an unknown type or a missing field.
self.bad_lines += 1
def on_booking(self, event):
booking_id = event["booking_id"]
self.due[booking_id] = read_amount(event)
def on_payment(self, event):
booking_id = event["booking_id"]
amount = read_amount(event)
self.paid[booking_id] = self.paid.get(booking_id, 0) + amount
def report(self):
# Built at the end, since a payment can show up before its booking.
rows = []
for booking_id in sorted(self.due):
due = self.due[booking_id]
paid = self.paid.get(booking_id, 0)
rows.append({"booking_id": booking_id, "due": due, "paid": paid, "outstanding": due - paid})
return {
"bookings": rows,
"not_fully_paid": [row["booking_id"] for row in rows if row["outstanding"] > 0],
"payments_without_booking": sorted(set(self.paid) - set(self.due)),
"bad_lines": self.bad_lines,
}
def build_report(lines):
report = PaymentReport()
for line in lines:
report.add_line(line)
return report.report()| Cost | Why | |
|---|---|---|
| Time | O(n + b log b) | One step per line, then sorting b bookings for the report |
| Space | O(b) | Two numbers per booking, not every line |
Test it
Walk through one of the examples out loud, then check the cases that tend to break things:
- A payment before its booking: it's added up anyway, and the booking matches it when it shows up.
- A payment for a booking that never shows up: listed under payments without a booking.
- Overpaid: outstanding goes negative. It isn't in 'not fully paid', and you could list it separately.
- An amount that's a string or a float: counted as a bad line instead of being added.
- Broken JSON, a JSON list instead of an object, or a missing field: counted as bad, and the rest still runs.
Common mistakes
- Using floats for money, so totals end up a fraction of a cent off.
- Assuming lines are in order and dropping a payment because its booking hasn't been seen yet.
- Crashing on the first bad line instead of counting it.
- Putting everything in one long loop with nested ifs, so each follow-up means rewriting it.
- Not saying the complexity of each step when the interviewer asks.
Follow-up questions
What about refunds?
Add a 'refund' handler that subtracts from the amount paid, and register it under its type. Nothing else changes. You might also report total refunded per booking.
What if the same payment gets logged twice?
Payments need an ID. Keep a set of payment IDs you've seen and skip repeats. That's O(1) per line and O(p) more memory.
What if there are several currencies?
Key the totals by (booking, currency), or convert at the rate from the booking time. Ask which one the team wants, since mixing currencies in one total is meaningless.
What if the logs don't fit in memory?
We only keep two numbers per booking, not lines, so it scales with bookings. If even that's too big, split lines by booking ID into files, run each one separately, and combine the results.
How the time adds up
| Minutes | What you're doing |
|---|---|
| 0–5 | Ask about the line format, order, money and bad lines |
| 5–10 | Brute force: search all lines for each booking |
| 10–20 | One pass with two maps, and handlers by type |
| 20–35 | Write the code |
| 35–40 | Test with the examples and bad lines |
| 40–45 | Follow-up questions |
Key concepts used here
- Hash maps and sliding windowsSwap nested loops for a single pass.
- Object-oriented designSplit a small program into classes that are easy to change.
We wrote this sample solution ourselves. Practice Loop is independent and isn't affiliated with or endorsed by Booking.com.