Definition
- Actors – Rider (requests and pays), driver (goes online, accepts trips, gets paid), and admin/operations (monitors and intervenes).
- Core services – User/Auth, Driver/Location, Trip, Matching/Dispatch, Pricing (including surge), Payment, Notification, and Ratings, each owning its own database.
- Real-time location – High-frequency GPS pings consumed through a stream into an in-memory geospatial index partitioned by region for millisecond "who is nearest" queries.
- Matching engine – Queries the geo-index within a radius of the pickup, ranks candidates by ETA, and claims the driver atomically with a compare-and-set.
- Communication – Synchronous REST/gRPC for immediate answers, async Kafka events for high-volume and eventual flows like notifications and analytics.
Think of National Bank of India running a fleet of mobile cash vans. Customers request a van, van drivers constantly radio in their location, a dispatcher keeps a live map and assigns the nearest free van to each request, a pricing desk calculates the fare, and the payment desk moves money from the customer to the driver and takes the bank's commission. The dispatcher must answer "who is nearest" in a fraction of a second even with thousands of vans.
Understanding the Concepts
What are the actors and core use cases?
The actors are the rider (requests trips, pays), the driver (goes online, accepts trips, drives, gets paid), and admin/operations (monitors, intervenes). The core flows define the services: driver goes online and streams location, rider requests a ride, the system matches a nearby driver, the trip runs with live tracking, the fare is computed and charged, and both parties are notified and rated.
package com.bank.ridehailing.model;
import java.math.BigDecimal;
public class TripRequest {
private String riderId;
private double pickupLat;
private double pickupLng;
private double dropLat;
private double dropLng;
private BigDecimal estimatedFare;
public TripRequest(String riderId, double pickupLat, double pickupLng,
double dropLat, double dropLng) {
this.riderId = riderId;
this.pickupLat = pickupLat;
this.pickupLng = pickupLng;
this.dropLat = dropLat;
this.dropLng = dropLng;
}
// Getters omitted for brevity
public String getRiderId() { return riderId; }
public double getPickupLat() { return pickupLat; }
public double getPickupLng() { return pickupLng; }
}
How do you decompose into microservices?
Each service owns its data following database-per-service. The Location service uses a geo-store optimized for spatial queries. The Payment service uses a strongly consistent relational database with idempotency.
| Service | Responsibility | Database Type |
|---|---|---|
| User/Auth | Profiles, tokens | Relational (PostgreSQL) |
| Location | GPS stream, current positions | In-memory geo (Redis Geo) |
| Trip | Trip lifecycle | Relational (PostgreSQL) |
| Matching | Nearest driver, dispatch | Reads from Location's geo-store |
| Pricing | Fare + surge calculation | Relational + cache |
| Payment | Charge rider, pay driver | Relational (strong consistency) |
| Notification | Push updates | Queue-backed |
| Ratings | Feedback | Relational |
package com.bank.ridehailing.service;
import java.math.BigDecimal;
public class PaymentService {
public PaymentResult chargeRider(String riderId, String tripId,
BigDecimal fare, String idempotencyKey) {
// Idempotency: if this key was already processed, return stored result
// Outbox pattern: write payment + event in one transaction
return new PaymentResult(tripId, "SUCCESS", idempotencyKey);
}
}
How does real-time location work?
Drivers send GPS coordinates every few seconds. This is a very high write rate of small updates. The location service consumes pings through a message stream and keeps each driver's current position in a fast in-memory store with a geospatial index. Only the latest position per driver is kept for matching, while raw trails go to cheaper storage for analytics.
package com.bank.ridehailing.location;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.GeoCoordinate;
public class DriverLocationStore {
private final Jedis jedis;
private static final String GEO_KEY = "drivers:active";
public DriverLocationStore(Jedis jedis) {
this.jedis = jedis;
}
public void updateLocation(String driverId, double lat, double lng) {
jedis.geoadd(GEO_KEY, lng, lat, driverId);
}
public java.util.List<String> findNearby(double lat, double lng,
double radiusKm) {
return jedis.georadiusByMember(GEO_KEY, "pickup-point", radiusKm,
redis.clients.jedis.params.geo.GeoRadiusParam.geoRadiusParam()
.sortAscending().count(10))
.stream()
.map(r -> r.getMemberByString())
.collect(java.util.stream.Collectors.toList());
}
}
# Driver D101 at 28.6139, 77.2090 (New Delhi)
# Driver D102 at 28.6150, 77.2100
# findNearby(28.6145, 77.2095, 2.0) returns [D102, D101]
How does the matching engine work?
When a rider requests a trip, the matching service queries the geo-index for candidate drivers near the pickup, filters by availability and vehicle type, ranks by estimated time of arrival, and sends a trip offer. If the driver declines or times out, it offers the next. Crucially, it claims the driver atomically so two concurrent requests cannot grab the same driver.
package com.bank.ridehailing.matching;
import java.util.List;
import java.util.Optional;
public class MatchingEngine {
private final DriverLocationStore locationStore;
private final DriverAvailabilityStore availabilityStore;
public MatchingEngine(DriverLocationStore locationStore,
DriverAvailabilityStore availabilityStore) {
this.locationStore = locationStore;
this.availabilityStore = availabilityStore;
}
public Optional<String> findAndClaimDriver(double pickupLat, double pickupLng) {
List<String> candidates = locationStore.findNearby(pickupLat, pickupLng, 5.0);
for (String driverId : candidates) {
// Atomic claim: compare-and-set from AVAILABLE to ASSIGNED
boolean claimed = availabilityStore.tryClaimDriver(driverId);
if (claimed) {
return Optional.of(driverId);
}
}
return Optional.empty();
}
}
How do you design the database layer?
The trip database is relational, storing trips with status, rider, driver, pickup, drop, timestamps, and fare. The payment database is relational and strongly consistent. The location "database" is the in-memory geo-store for current positions plus a time-series store for historical trails.
package com.bank.ridehailing.model;
import java.math.BigDecimal;
import java.time.Instant;
public class Trip {
private String tripId;
private String riderId;
private String driverId;
private String status; // REQUESTED, MATCHED, IN_PROGRESS, COMPLETED, CANCELLED
private double pickupLat;
private double pickupLng;
private double dropLat;
private double dropLng;
private BigDecimal fare;
private Instant createdAt;
private Instant completedAt;
// Constructor and getters omitted for brevity
}
How does communication work between services?
Synchronous REST or gRPC is used where an immediate answer is needed: creating a trip or getting a fare estimate. Asynchronous events over Kafka are used for everything high-volume or eventual: location pings, trip-state changes fanning out to notifications and analytics. Live trip tracking uses WebSocket.
package com.bank.ridehailing.event;
public class TripStateChangedEvent {
private String tripId;
private String previousStatus;
private String newStatus;
private long timestamp;
public TripStateChangedEvent(String tripId, String previousStatus,
String newStatus) {
this.tripId = tripId;
this.previousStatus = previousStatus;
this.newStatus = newStatus;
this.timestamp = System.currentTimeMillis();
}
// Published to Kafka topic "trip-events"
// Consumed by: notification-service, analytics-service, billing-service
}
How do you handle security and monitoring?
Authentication uses tokens with scoped access. The API gateway validates every request, rate-limits, and terminates TLS. Service-to-service traffic uses mTLS. Authorization ensures a rider sees only their own trips. Payment details are encrypted and handled by the payment service alone.
package com.bank.ridehailing.security;
public class ApiGatewayFilter {
public boolean authenticate(String token) {
// Validate JWT, check expiry, extract rider/driver role
return true; // simplified
}
public boolean authorize(String role, String resource) {
// Rider can only access own trips
// Driver can only access own offers
return true; // simplified
}
}
Observability includes metrics on match latency and trip success rate, structured logs keyed by a trip correlation ID, and distributed tracing so a single trip's journey across services is visible.
How do you scale the system?
Stateless services scale by adding instances behind a load balancer. The location pipeline scales by partitioning the stream and geo-store by region using geohash or H3 cells. The in-memory geo-store is clustered and partitioned by geo cell. Capacity is planned from peak rush-hour surges.
# Geographic partitioning example:
# Region: Delhi -> partition-0 -> Redis Geo shard-0
# Region: Mumbai -> partition-1 -> Redis Geo shard-1
# Region: Bangalore -> partition-2 -> Redis Geo shard-2
# A busy city's load stays within its own shard
What are common interview follow-up questions?
How would you handle surge pricing without touching the matching service? How do you prevent double-assigning a driver? What happens if the location service goes down? How do you handle a trip where the payment fails after the ride is completed? How do you ensure the outbox pattern works with Kafka? These all test whether you understand the isolation of concerns and the resilience patterns.
Visual Overview
┌────────────────────────────────────────────────────────────┐ │ UBER-LIKE SYSTEM │ ├────────────────────────────────────────────────────────────┤ │ │ │ Rider App Driver App │ │ │ │ │ │ │ REST/gRPC │ GPS pings (every 3s) │ │ ▼ ▼ │ │ ┌──────────┐ ┌─────────────────┐ │ │ │API Gateway│ │ Location Svc │ │ │ │(auth,rate)│ │ (Kafka → Redis │ │ │ └────┬─────┘ │ Geo index) │ │ │ │ └────────┬────────┘ │ │ ▼ │ │ │ ┌──────────┐ ┌────────▼────────┐ │ │ │ Trip Svc │───▶│ Matching Svc │ │ │ │ (CRUD, │ │ (geo query + │ │ │ │ status) │ │ atomic claim) │ │ │ └────┬─────┘ └─────────────────┘ │ │ │ │ │ ┌────▼─────┐ ┌─────────────────┐ │ │ │Pricing Svc│ │ Payment Svc │ │ │ │(fare+surge│ │ (idempotent, │ │ │ │ calc) │ │ outbox) │ │ │ └──────────┘ └─────────────────┘ │ │ │ │ ┌─────────────────────────────────┐ │ │ │ Kafka: trip-events, location │ │ │ │ → Notification, Analytics │ │ │ └─────────────────────────────────┘ │ └────────────────────────────────────────────────────────────┘
Key Takeaway
Designing a system like Uber starts from the actors and use cases, then decomposes into focused microservices each owning its own data. The actors are rider, driver, and operations, and the services are user/auth, location, matching, trip, pricing, payment, notification, and ratings. Database-per-service lets the location service use Redis Geo optimized for spatial queries while the payment service uses PostgreSQL with strong consistency and idempotency keys.
The defining challenge is real-time location. Drivers send GPS pings every few seconds, which is a massive stream of tiny writes. The location service consumes them through Kafka and keeps only each driver's latest position in a geographically partitioned in-memory geo-store. The matching engine reads candidates within a radius, ranks by ETA, and claims the driver atomically with a compare-and-set so two riders never get the same driver. Pankaj designed this for National Bank of India's mobile cash-van fleet, and the atomic claim was what prevented two customers being assigned the same van.
Communication is a deliberate mix: synchronous for immediate answers like fare estimates, and async Kafka events for high-volume flows like location pings and trip-state changes that fan out to notifications and analytics. The payment service uses the outbox pattern so the debit and the event commit atomically, and an idempotency key ensures retried charges never double-debit Rajesh's account. Everything is partitioned geographically so a busy city stays within its own shards and failures stay local.
Remember one sentence: Design Uber as data-owning microservices around a geographically partitioned in-memory geo-index for real-time location, a matching engine that claims drivers atomically, event-driven communication for high-volume flows, and an idempotent payment service for the money.
---