CASE STUDY

Delivery Driver Billing APIs (In-Memory LLD)

2 min read·386 words·Intermediate

Asked at

1 candidate report in Dec 2025

How to use this case study

SDE-2 / Mid

Implement add_driver, record_delivery, get_total_cost with correct money handling (integer cents, hourly rates prorated by seconds).

SDE-3 / Senior

Add pay_up_to(time) and get_unpaid, and make total queries O(1) by maintaining running totals.

Staff / Principal

Discuss concurrency, persistence (a ledger), overlapping deliveries, rate changes over time and how this maps to a real payroll system.


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 before time. 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 use Decimal.
  • O(1) total: keep a running total updated on each record_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 --> PD

4) 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.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →