0) Problem Restatement
Rippling asked a coding-style design question: build the core billing APIs for a delivery company.
add_driver(driver_id, hourly_rate): the rate in dollars per hour.record_delivery(driver_id, start_time, end_time): times in seconds.get_total_cost(): the total owed for all deliveries recorded so far.- Follow-ups:
pay_up_to(time): pay all deliveries that ended at or beforetime.get_total_cost_unpaid(): what's still owed.
The focus is clean APIs, correct money math, and efficient queries.
1) Key Decisions
- Money precision: never use floats for money. Store rates in cents per hour, and compute cost as
rate_cents_per_hour × seconds / 3600. To avoid losing fractions of cents, keep totals in "cent-seconds" (rate × seconds) and divide only when displaying, or useDecimal. - O(1) total: keep a running
totalupdated on eachrecord_delivery, instead of summing all deliveries on every query. - Unpaid with pay_up_to: deliveries must be paid in order of end time. Keep deliveries sorted by end time (or in a min-heap), and a pointer to the paid boundary.
2) Code (Python)
import heapq
from decimal import Decimal
class DeliveryBilling:
def __init__(self):
self.rates = {} # driver_id -> Decimal dollars per hour
self.total = Decimal(0) # all recorded deliveries
self.paid = Decimal(0)
self.unpaid_heap = [] # (end_time, seq, cost) of unpaid deliveries
self._seq = 0
def add_driver(self, driver_id, hourly_rate):
if driver_id in self.rates:
raise ValueError("driver exists")
self.rates[driver_id] = Decimal(str(hourly_rate))
def record_delivery(self, driver_id, start, end):
if driver_id not in self.rates:
raise KeyError("unknown driver")
if end <= start:
raise ValueError("end must be after start")
cost = self.rates[driver_id] * Decimal(end - start) / Decimal(3600)
self.total += cost
self._seq += 1
heapq.heappush(self.unpaid_heap, (end, self._seq, cost))
def get_total_cost(self):
return self.total.quantize(Decimal("0.01"))
def pay_up_to(self, time):
while self.unpaid_heap and self.unpaid_heap[0][0] <= time:
_, _, cost = heapq.heappop(self.unpaid_heap)
self.paid += cost
def get_total_cost_unpaid(self):
return (self.total - self.paid).quantize(Decimal("0.01"))
Complexity: record_delivery O(log n), get_total_cost and get_total_cost_unpaid O(1), and pay_up_to O(k log n) for k deliveries paid. Each delivery is paid once in total.
3) Follow-ups
- Max simultaneous drivers in the last 24 hours (a common Rippling follow-up): sweep line. Turn each delivery into +1 at start and −1 at end, sort the events, and track the running max within the window.
- Rate changes: store rate history per driver (effective_from), and price a delivery with the rate in effect at its start (or split it across the change).
- Concurrency: one lock around mutations, or a lock per driver plus atomic updates to the totals.
- Persistence: in production, every delivery and payment is a ledger entry (append-only), and totals are materialized views that are reconciled nightly.
Architecture Diagram
flowchart LR
A["add_driver"] --> R[("rates")]
D["record_delivery"] --> T["running total += cost"]
D --> H[("min-heap by end_time - unpaid")]
P["pay_up_to(t)"] --> H
P --> PD["paid += popped costs"]
Q["get_total_cost_unpaid"] --> T
Q --> PD4) Wrap-Up
Store rates as exact decimals (or integer cents), compute each delivery's cost from its duration, and keep a running total so total queries are O(1). Keep unpaid deliveries in a min-heap by end time, so pay_up_to pops and adds to a paid total, and unpaid = total − paid. Extend with a sweep line for concurrency questions, rate history for changing rates, and a ledger for production.