Hard system-design java

How would you design a delivery system from the driver's perspective?

The 30 second answer

I model a delivery as a guarded workflow: assignment, acceptance, pickup and completion. A driver can have only one active job in the first version. I keep assignment changes durable and transactional, but handle frequent location updates separately. Arrival estimates combine travel time, remaining preparation and handoff time. I would implement the state transitions first, then test retries, competing assignments and out-of-order location updates before adding more services.

Clarify scope and invariants. A driver goes online, sees an assigned task, accepts or declines it, navigates to pickup, confirms pickup, shares progress, and confirms delivery. Ask about batching, reassignment, proof of delivery, cancellation after pickup, poor connectivity and location retention. Assume one active delivery per driver initially; do not accidentally hard-code that rule if batching is required. Agree freshness and availability targets rather than inventing a latency guarantee. Task transitions must remain correct during retries and competing assignments; a temporarily stale map is tolerable but must be labeled stale.

Entities and responsibilities. Driver(id, availability), Delivery(id, driverId, status, version, pickup, destination), Location(driverId, sessionId, sequence, acquiredAt, receivedAt, coordinates, accuracy) and DeliveryEvent(id, deliveryId, version, type). DeliveryService owns transitions; AssignmentRepository owns the durable atomic claim; LocationService owns ordering/freshness; RouteProvider returns travel-duration estimates. Keep these as modules of one Spring Boot service first if scale and ownership do not justify independent deployment.

API and persistence contract. Illustrative routes are POST /drivers/me/availability, POST /deliveries/{id}/accept, /decline, /pickup and /complete, PUT /drivers/me/location and GET /drivers/me/active-delivery. Authenticate the driver rather than trusting a driverId supplied in a body. Commands carry a request id and expected delivery version. In a database transaction, claim the driver's free slot and the unassigned delivery together, then increment the delivery version and insert an outbox event. If either claim fails, roll back both. Store a durable command result keyed by driver and request id so a reconnecting client gets the original response; reject reusing the key with different content. A single aggregate lock does not enforce the cross-delivery driver-slot invariant: lock the driver row and delivery row in an agreed order or use conditional updates with checked row counts.

Executable core, deliberately in-memory. This Java program demonstrates one-job-per-driver, legal transitions and rejection of conflicting assignments. It is a single-process correctness fixture, not a distributed repository or a complete HTTP application. The global monitor serializes the fixture only; replace it with the transaction described above for multiple instances. Idempotent command storage, authentication and location ingestion belong at the service boundary, not in this tiny domain demo.

import java.util.HashMap;
import java.util.Map;

public class DeliveryDemo {
    enum State { ASSIGNED, ACCEPTED, PICKED_UP, DELIVERED }
    record Task(long id, long driver, State state, int version) {}
    static final class Service {
        private final Map<Long, Task> tasks = new HashMap<>();
        private final Map<Long, Long> activeByDriver = new HashMap<>();

        synchronized Task assign(long id, long driver) {
            if (tasks.containsKey(id) || activeByDriver.containsKey(driver))
                throw new IllegalStateException("already assigned");
            Task task = new Task(id, driver, State.ASSIGNED, 0);
            tasks.put(id, task);
            activeByDriver.put(driver, id);
            return task;
        }
        synchronized Task advance(long id, long driver, int version, State next) {
            Task old = tasks.get(id);
            if (old == null || old.driver() != driver || old.version() != version)
                throw new IllegalStateException("missing, forbidden or stale");
            boolean legal = (old.state() == State.ASSIGNED && next == State.ACCEPTED)
                || (old.state() == State.ACCEPTED && next == State.PICKED_UP)
                || (old.state() == State.PICKED_UP && next == State.DELIVERED);
            if (!legal) throw new IllegalStateException("illegal transition");
            Task updated = new Task(id, driver, next, version + 1);
            tasks.put(id, updated);
            if (next == State.DELIVERED) activeByDriver.remove(driver);
            return updated;
        }
    }
    public static void main(String[] args) {
        Service s = new Service();
        s.assign(1, 7);
        s.advance(1, 7, 0, State.ACCEPTED);
        s.advance(1, 7, 1, State.PICKED_UP);
        if (s.advance(1, 7, 2, State.DELIVERED).version() != 3)
            throw new AssertionError();
        s.assign(2, 7); // driver is free again
        try {
            s.assign(3, 7);
            throw new AssertionError("double assignment accepted");
        } catch (IllegalStateException expected) { }
        System.out.println("delivery transitions passed");
    }
}

Extend the demo's state model explicitly for decline and cancellation: before pickup a guarded release clears the driver's slot and returns the task to dispatch or cancels it; after pickup, route to an exception/return workflow rather than pretending the parcel vanished. A failed transition must not release the driver's slot. Lock both records consistently in production so an old completion cannot clear a newer assignment.

Location path. Send authenticated samples with a server-issued driver session and a monotonic sequence within that session. Atomically replace latest location only when the active session matches and sequence increases; do not compare sequence numbers across device sessions. Retain acquiredAt and receivedAt to distinguish delayed upload from ingest backlog, but do not trust device clocks as ordering authorities. Validate coordinate bounds, plausible speed and accuracy, deduplicate retries, and display last-updated time. Keep latest location in a fast store with a freshness expiry; retain history only for an explicit business purpose and duration. Push updates to authorized viewers through a streaming channel, coalescing intermediate samples for slow clients and resending a snapshot after reconnect. Do not let a slow consumer create an unbounded buffer.

For a browser prototype, W3C Geolocation provides permission-gated watchPosition samples with timestamp and accuracy. It does not promise exact position or continuous background updates: hidden/inactive pages may not receive updates. A production driver mobile app needs platform-appropriate background-location behavior and permission handling. Stop tracking off duty; restrict customer access to the active delivery rather than exposing every driver's coordinates.

ETA, not a constant-speed distance division. Before pickup, proposed remaining time is max(driver-to-pickup travel, remaining preparation) + pickup handoff + pickup-to-customer travel + drop-off handoff, assuming driver travel and preparation overlap. After pickup, remove the completed legs. A routing API can incorporate historical/live traffic; it cannot supply your merchant's preparation delay. Recompute after meaningful route or preparation changes, bound calls by a budget, and show a range with timestamp. During routing failure use a clearly degraded last estimate or an unavailable estimate, never a fabricated precise arrival time. Calibrate prediction errors against actual completion times by stage.

Failure diagnosis and tests. If the map jumps backwards, check session/sequence handling and offline batches. If the map is fresh but ETA is late, compare preparation and handoff duration against route error. For duplicate assignment, reproduce two dispatchers claiming the same driver and assert exactly one commit. Test completion after reassignment, stale-version pickup, a lost response followed by retry, unauthorized driver commands, no GPS permission, old samples after reconnect and crash after DB commit before event publication. The outbox relay may redeliver; downstream notification consumers deduplicate event ids. Measure assignment conflict rate, location age, ingest lag, transition failures and stage-specific ETA error.

Sources (4)
  1. PostgreSQL 18: Row-level locks and consistent lock ordering
  2. Chris Richardson: Transactional outbox and duplicate relay publication
  3. W3C Geolocation: coordinates, timestamps, permissions and visibility limitations
  4. Google Routes API: historical/live traffic and travel-duration options