Design Uber


Patterns, Technologies, and Concepts

Uber

Key Takeaways

Requirements

You demonstrated strong system design fundamentals for ride matching, correctly identifying consistency requirements, latency thresholds, and using back-of-envelope math to quantify write volume from driver location updates.

  • Strong consistency is critical for ride matching because it prevents two drivers from being matched to the same rider simultaneously. Use a distributed lock or a single authoritative service to enforce a 1-to-1 pairing guarantee.

  • When estimating write-heavy systems like driver location updates, back-of-envelope math is a powerful tool. For example, if 1 million active drivers send a GPS update every 4 seconds, that is 250,000 writes per second, which immediately tells you that you need a write-optimized storage solution like Cassandra or a time-series database.

Core Entities

You demonstrated a strong grasp of core entity modeling for a ride-sharing system, correctly identifying the key entities and showing thoughtful separation of concerns.

  • Separating Location into its own entity (rather than embedding it in Driver) is a best practice for ride-sharing systems because driver location updates happen constantly and independently from other driver data. This makes it easier to scale location updates without touching the rest of the Driver record.

  • Keeping Fare as its own entity rather than a field on Ride gives you flexibility to store fare estimates before a ride starts, support dynamic pricing history, and run financial analytics independently from ride data.

API

You demonstrated strong API design fundamentals across both attempts, and your second response addressed the minor gaps cleanly, showing good responsiveness to feedback.

  • Never put a user or driver ID in the URL path when that identity can be inferred from an auth token. Use POST /drivers/locations instead of POST /drivers/:driverId/locations. This is safer because it avoids leaking identity in logs or URLs, and it relies on the JWT or session cookie as the single source of truth for who is making the request.

  • Always include a response body on state-changing endpoints like PATCH or POST. Returning the updated resource (for example the updated Ride object) or at minimum a success flag lets the caller confirm the new state without making a second GET request. This is standard REST practice and shows maturity in API design.

High Level Design

You demonstrated a strong grasp of core service decomposition and data modeling across all four attempts, with only minor gaps around validation logic, state transitions, and making implicit design decisions more explicit.

  • Fare estimates should always have an expiration timestamp in your schema. A fare quote is a snapshot of pricing at a moment in time, not a permanent price. Including an expires_at field communicates this clearly and prevents rides from being created against stale pricing data.

  • When a ride request comes in with a fare ID, the ride service must validate that the fare ID exists and has not expired before creating the ride record. Skipping this step means your system could create rides from missing or outdated estimates, which breaks pricing guarantees.

  • When describing a status change in a ride lifecycle, always name the exact transition rather than saying ‘update the database accordingly’. For example, say the ride moves from requested to accepted or requested to declined. This makes your design easier to reason about and shows you have thought through the state machine.

  • When selecting a driver during matching, proximity alone is not enough. The match service should filter for nearby drivers who are also available. Picking the closest driver without checking availability could assign a driver who is already on a ride.

Deep Dives

You demonstrated strong geospatial and distributed systems intuition throughout, and you iterated well on feedback, but you had a key gap around atomic Redis lock release that required an extra attempt to fully nail.

  • When releasing a Redis distributed lock safely, the check-and-delete operation must be atomic. A simple read-then-delete has a race condition where another process could acquire the lock between your read and delete. The standard fix is a Lua script run inside Redis: ‘if redis.call(get, key) == requestId then return redis.call(del, key) end’. Redis executes Lua scripts atomically, so no other command can run in between.

  • When using a queue like SQS for work distribution, a message should only be deleted from the queue after the consumer successfully finishes processing it. If the consumer fails mid-way, the message becomes visible again after the visibility timeout and gets retried. This is what prevents dropped work during failures or crashes.

  • When geohash-partitioned data sits near a partition boundary, a nearby search needs to fan out to adjacent partition workers. You can find those neighbors by stepping up one level in the geohash prefix hierarchy, which gives you all overlapping cells. Limit the fan-out to just two or three adjacent regions so coordination overhead stays constant and does not grow with system scale.

  • For surge handling in a matching system, partition your queue by geography rather than using a single global queue. This means a demand spike in one city only backs up that region’s queue and does not slow down matching in other areas. Pair this with auto-scaling Match Service workers based on per-partition queue depth so each region can drain its own backlog independently.