Requirements & Scope
Functional Requirements
- Browse & Search: Customers search restaurants by location, cuisine, rating; view menus with items, prices, availability
- Place Order: Add items to cart, select delivery address, choose payment method, apply promo codes, place order
- Order Tracking: Real-time order status updates (PLACED → CONFIRMED → PREPARING → READY → PICKED_UP → DELIVERED); live map with driver location
- Driver App: Accept/reject delivery requests, navigate to restaurant and customer, update order status
- Restaurant App: Receive orders, confirm/reject, mark items as ready, manage menu and availability
- Payments: Hold payment on order, charge on delivery, handle tips, refunds on cancellation
Non-Functional Requirements
- Latency: Order placement < 500ms; location updates every 5 seconds
- Throughput: 10K orders/minute peak; 100K concurrent active orders
- Availability: 99.99% for order placement; 99.9% for tracking
- Consistency: Strong consistency for payment; eventual consistency for location
- Scalability: Support 100K restaurants, 50K drivers per city
Core Entities
| Entity | Key Fields |
|---|---|
| Customer | id, name, addresses, paymentMethods |
| Restaurant | id, name, location, menu, rating, isOpen |
| MenuItem | id, name, price, description, category, isAvailable |
| Order | id, customerId, restaurantId, items, status, total, deliveryAddress, paymentInfo |
| Driver | id, name, location, status, rating, vehicleInfo |
| Delivery | id, orderId, driverId, pickupLocation, dropoffLocation, eta, status |
Order Flow & State Machine
Order State Machine
The order lifecycle is a classic finite state machine. Each transition is triggered by an event and is idempotent.
PLACED ──→ CONFIRMED ──→ PREPARING ──→ READY ──→ PICKED_UP ──→ DELIVERED
│ │ │ │ │ │
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
CANCELLED CANCELLED CANCELLED CANCELLED CANCELLED (terminal)
State Transitions
| From | To | Trigger | Actor |
|---|---|---|---|
| null | PLACED | Customer places order | System |
| PLACED | CONFIRMED | Restaurant accepts order | Restaurant |
| PLACED | CANCELLED | Restaurant rejects / timeout | Restaurant/System |
| CONFIRMED | PREPARING | Restaurant starts prep | Restaurant |
| PREPARING | READY | Restaurant marks items ready | Restaurant |
| READY | PICKED_UP | Driver picks up order | Driver |
| PICKED_UP | DELIVERED | Driver completes delivery | Driver |
| Any active | CANCELLED | Customer cancels (before pickup) | Customer |
Java Implementation — Order with State Pattern
// State interface
public interface OrderState {
void next(Order order);
void cancel(Order order);
String getStatus();
}
// Concrete states
public class PlacedState implements OrderState {
@Override
public void next(Order order) {
order.setState(new ConfirmedState());
}
@Override
public void cancel(Order order) {
order.setState(new CancelledState());
}
@Override
public String getStatus() { return "PLACED"; }
}
public class ConfirmedState implements OrderState {
@Override
public void next(Order order) {
order.setState(new PreparingState());
}
@Override
public void cancel(Order order) {
order.setState(new CancelledState());
}
@Override
public String getStatus() { return "CONFIRMED"; }
}
public class PreparingState implements OrderState {
@Override
public void next(Order order) {
order.setState(new ReadyState());
}
@Override
public void cancel(Order order) {
order.setState(new CancelledState());
}
@Override
public String getStatus() { return "PREPARING"; }
}
public class ReadyState implements OrderState {
@Override
public void next(Order order) {
order.setState(new PickedUpState());
}
@Override
public void cancel(Order order) {
// Cannot cancel after restaurant is ready — driver assigned
throw new IllegalStateException("Order already ready for pickup");
}
@Override
public String getStatus() { return "READY"; }
}
public class PickedUpState implements OrderState {
@Override
public void next(Order order) {
order.setState(new DeliveredState());
}
@Override
public void cancel(Order order) {
throw new IllegalStateException("Cannot cancel order in transit");
}
@Override
public String getStatus() { return "PICKED_UP"; }
}
public class DeliveredState implements OrderState {
@Override
public void next(Order order) {
throw new IllegalStateException("Order already delivered");
}
@Override
public void cancel(Order order) {
throw new IllegalStateException("Cannot cancel delivered order");
}
@Override
public String getStatus() { return "DELIVERED"; }
}
public class CancelledState implements OrderState {
@Override
public void next(Order order) {
throw new IllegalStateException("Cancelled order cannot advance");
}
@Override
public void cancel(Order order) {
throw new IllegalStateException("Order already cancelled");
}
@Override
public String getStatus() { return "CANCELLED"; }
}
Order Class
public class Order {
private String id;
private String customerId;
private String restaurantId;
private List<OrderItem> items;
private DeliveryAddress deliveryAddress;
private PaymentInfo paymentInfo;
private OrderState state;
private double totalAmount;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Order(String id, String customerId, String restaurantId,
List<OrderItem> items, DeliveryAddress address, PaymentInfo payment) {
this.id = id;
this.customerId = customerId;
this.restaurantId = restaurantId;
this.items = items;
this.deliveryAddress = address;
this.paymentInfo = payment;
this.state = new PlacedState();
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
this.totalAmount = calculateTotal();
}
public void nextState() {
state.next(this);
this.updatedAt = LocalDateTime.now();
}
public void cancel() {
state.cancel(this);
this.updatedAt = LocalDateTime.now();
}
public String getStatus() {
return state.getStatus();
}
private double calculateTotal() {
return items.stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
}
}
OrderService
@Service
public class OrderService {
private final OrderRepository orderRepo;
private final PaymentService paymentService;
private final DriverMatchingService driverService;
private final NotificationService notificationService;
private final EventBus eventBus;
public Order placeOrder(PlaceOrderRequest request) {
// Validate restaurant is open
Restaurant restaurant = restaurantRepo.findById(request.getRestaurantId());
if (!restaurant.isOpen()) {
throw new RestaurantClosedException();
}
// Validate all items are available
for (OrderItem item : request.getItems()) {
MenuItem menuItem = menuRepo.findById(item.getItemId());
if (!menuItem.isAvailable()) {
throw new ItemUnavailableException(item.getName());
}
}
// Hold payment
PaymentInfo payment = paymentService.holdPayment(
request.getPaymentMethodId(), calculateTotal(request.getItems())
);
// Create order
Order order = new Order(
UUID.randomUUID().toString(),
request.getCustomerId(),
request.getRestaurantId(),
request.getItems(),
request.getDeliveryAddress(),
payment
);
orderRepo.save(order);
// Notify restaurant
eventBus.publish(new OrderPlacedEvent(order));
notificationService.notifyRestaurant(restaurant.getId(), order);
return order;
}
public void confirmOrder(String orderId, String restaurantId) {
Order order = orderRepo.findById(orderId);
validateOwnership(order, restaurantId);
order.nextState(); // PLACED → CONFIRMED
orderRepo.save(order);
// Trigger driver matching
driverService.findAndAssignDriver(order);
eventBus.publish(new OrderConfirmedEvent(order));
}
public void markReady(String orderId, String restaurantId) {
Order order = orderRepo.findById(orderId);
validateOwnership(order, restaurantId);
order.nextState(); // CONFIRMED → PREPARING
order.nextState(); // PREPARING → READY
orderRepo.save(order);
eventBus.publish(new OrderReadyEvent(order));
notificationService.notifyCustomer(order.getCustomerId(),
"Your order is ready for pickup!");
}
public void pickupOrder(String orderId, String driverId) {
Order order = orderRepo.findById(orderId);
Driver driver = driverRepo.findById(driverId);
if (!driver.getCurrentOrderId().equals(orderId)) {
throw new UnauthorizedDriverException();
}
order.nextState(); // READY → PICKED_UP
orderRepo.save(order);
driver.setStatus(DrivingStatus.DELIVERING);
driverRepo.save(driver);
eventBus.publish(new OrderPickedUpEvent(order, driver));
}
public void completeDelivery(String orderId, String driverId) {
Order order = orderRepo.findById(orderId);
order.nextState(); // PICKED_UP → DELIVERED
orderRepo.save(order);
// Charge payment (release hold + charge)
paymentService.chargePayment(order.getPaymentInfo());
// Update driver
Driver driver = driverRepo.findById(driverId);
driver.setStatus(DrivingStatus.AVAILABLE);
driver.setCurrentOrderId(null);
driverRepo.save(driver);
eventBus.publish(new OrderDeliveredEvent(order));
}
private void validateOwnership(Order order, String entityId) {
if (order == null) throw new OrderNotFoundException();
}
}
Delivery Tracking & Driver Matching
Driver Matching Algorithm
When an order reaches READY state, we must find the nearest available driver.
Strategy Pattern for Matching:
public interface DriverMatchingStrategy {
Driver findBestDriver(List<Driver> availableDrivers, Delivery delivery);
}
public class NearestDriverStrategy implements DriverMatchingStrategy {
@Override
public Driver findBestDriver(List<Driver> availableDrivers, Delivery delivery) {
return availableDrivers.stream()
.min(Comparator.comparingDouble(d ->
calculateDistance(d.getLocation(), delivery.getPickupLocation())))
.orElse(null);
}
private double calculateDistance(Location a, Location b) {
// Haversine formula for lat/lng distance
double R = 6371; // Earth radius in km
double dLat = Math.toRadians(b.getLat() - a.getLat());
double dLon = Math.toRadians(b.getLng() - a.getLng());
double x = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(Math.toRadians(a.getLat())) *
Math.cos(Math.toRadians(b.getLat())) *
Math.sin(dLon/2) * Math.sin(dLon/2);
return R * 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1-x));
}
}
public class RatingWeightedStrategy implements DriverMatchingStrategy {
@Override
public Driver findBestDriver(List<Driver> availableDrivers, Delivery delivery) {
return availableDrivers.stream()
.max(Comparator.comparingDouble(d -> {
double dist = calculateDistance(d.getLocation(), delivery.getPickupLocation());
// Score: higher rating + closer distance = better
return d.getRating() / (dist + 0.1);
}))
.orElse(null);
}
}
DriverMatchingService:
@Service
public class DriverMatchingService {
private final DriverRepository driverRepo;
private final DeliveryRepository deliveryRepo;
private DriverMatchingStrategy strategy;
private final GeoService geoService;
public DriverMatchingService() {
this.strategy = new NearestDriverStrategy(); // default
}
public void setStrategy(DriverMatchingStrategy strategy) {
this.strategy = strategy;
}
@Async
public CompletableFuture<Driver> findAndAssignDriver(Order order) {
// Find available drivers within 5km radius
Location restaurantLoc = geoService.getLocation(order.getRestaurantId());
List<Driver> nearby = driverRepo.findAvailableDriversWithinRadius(
restaurantLoc.getLat(), restaurantLoc.getLng(), 5.0
);
if (nearby.isEmpty()) {
// Retry with larger radius or queue for later
return retryWithExpandedRadius(order);
}
Delivery delivery = createDelivery(order, restaurantLoc);
Driver selected = strategy.findBestDriver(nearby, delivery);
if (selected == null) {
return CompletableFuture.completedFuture(null);
}
// Assign driver to order
selected.setCurrentOrderId(order.getId());
selected.setStatus(DrivingStatus.HEADING_TO_RESTAURANT);
driverRepo.save(selected);
delivery.setDriverId(selected.getId());
deliveryRepo.save(delivery);
// Notify driver and customer
notifyDriver(selected, order);
notifyCustomer(order.getCustomerId(), selected);
return CompletableFuture.completedFuture(selected);
}
private Delivery createDelivery(Order order, Location pickup) {
Delivery d = new Delivery();
d.setOrderId(order.getId());
d.setPickupLocation(pickup);
d.setDropoffLocation(order.getDeliveryAddress().toLocation());
d.setStatus(DeliveryStatus.ASSIGNED);
return d;
}
}
Real-Time Location Tracking
Location Update Flow:
Driver App ──→ Location Service ──→ Message Queue (Kafka) ──→ Location Processor
│
▼
Redis (cache)
WebSocket ──→ Customer App
Location Service:
@Service
public class LocationService {
private final RedisTemplate<String, String> redis;
private final KafkaTemplate<String, LocationUpdate> kafka;
private final SimpMessagingTemplate wsTemplate;
// Driver reports location every 5 seconds
public void updateLocation(String driverId, LocationUpdate update) {
// Store in Redis for fast reads (TTL = 30s)
String key = "driver:location:" + driverId;
Map<String, String> fields = Map.of(
"lat", String.valueOf(update.getLat()),
"lng", String.valueOf(update.getLng()),
"timestamp", String.valueOf(update.getTimestamp())
);
redis.opsForHash().putAll(key, fields);
redis.expire(key, Duration.ofSeconds(30));
// Publish to Kafka for processing
kafka.send("driver-location-updates", driverId, update);
}
public Location getDriverLocation(String driverId) {
String key = "driver:location:" + driverId;
Map<Object, Object> data = redis.opsForHash().entries(key);
if (data.isEmpty()) return null;
return new Location(
Double.parseDouble((String) data.get("lat")),
Double.parseDouble((String) data.get("lng"))
);
}
// Customer subscribes to order tracking
public void subscribeToOrder(String orderId, String customerId) {
// WebSocket subscription — driver location pushed to customer
wsTemplate.convertAndSendToUser(
customerId,
"/topic/order/" + orderId + "/location",
getDeliveryStatus(orderId)
);
}
}
@Component
public class LocationProcessor {
@KafkaListener(topics = "driver-location-updates")
public void processLocationUpdate(ConsumerRecord<String, LocationUpdate> record) {
String driverId = record.key();
LocationUpdate update = record.value();
// Find active order for this driver
Delivery delivery = deliveryRepo.findByDriverId(driverId)
.filter(d -> d.getStatus() == DeliveryStatus.IN_TRANSIT)
.orElse(null);
if (delivery != null) {
// Recalculate ETA
double remainingDist = calculateRemainingDistance(
update, delivery.getDropoffLocation()
);
int etaMinutes = (int) (remainingDist / AVERAGE_SPEED_KM_PER_MIN);
// Push to customer via WebSocket
Map<String, Object> payload = Map.of(
"driverLocation", update,
"etaMinutes", etaMinutes,
"remainingDistanceKm", remainingDist
);
messagingTemplate.convertAndSendToUser(
delivery.getCustomerId(),
"/topic/order/" + delivery.getOrderId() + "/location",
payload
);
}
}
}
ETA Calculation
@Service
public class ETAService {
private final TrafficService trafficService;
private final MapService mapService;
public int calculateETA(Location from, Location to, String orderId) {
// Base distance
double distanceKm = mapService.getDistance(from, to);
// Apply traffic multiplier
double trafficFactor = trafficService.getTrafficMultiplier(from, to);
// Apply weather multiplier (optional)
double weatherFactor = 1.0; // simplified
// Average speed: 20 km/h in city, adjusted for traffic
double effectiveSpeed = 20.0 / (trafficFactor * weatherFactor);
double timeHours = distanceKm / effectiveSpeed;
return (int) Math.ceil(timeHours * 60); // minutes
}
}
Follow-ups & Advanced Topics
Payment Flow — Hold & Charge
┌─────────────────────────────────────────────────────────┐
│ PAYMENT FLOW │
├─────────────────────────────────────────────────────────┤
│ │
│ Order Placed Order Confirmed Delivered │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌───────────┐ ┌────────┐ │
│ │ HOLD │ │ (keep │ │ CHARGE │ │
│ │ $30.00 │──────────│ hold) │─────────│ $32.50│ │
│ └─────────┘ └───────────┘ └────────┘ │
│ │
│ Tip added: ┌────────────┐ │
│ $2.50 │CHARGE TIP │ │
│ │ $2.50 │ │
│ └────────────┘ │
└─────────────────────────────────────────────────────────┘
PaymentService:
@Service
public class PaymentService {
private final PaymentGateway gateway;
private final PaymentRepository paymentRepo;
public PaymentInfo holdPayment(String paymentMethodId, double amount) {
// Authorize (hold) the amount
AuthorizationResult result = gateway.authorize(paymentMethodId, amount);
if (!result.isSuccess()) {
throw new PaymentDeclinedException(result.getReason());
}
PaymentInfo info = new PaymentInfo();
info.setAuthorizationId(result.getAuthorizationId());
info.setAmount(amount);
info.setStatus(PaymentStatus.HELD);
return info;
}
public void chargePayment(PaymentInfo paymentInfo) {
// Capture the held amount + any adjustments
gateway.capture(paymentInfo.getAuthorizationId(), paymentInfo.getAmount());
paymentInfo.setStatus(PaymentStatus.CHARGED);
paymentRepo.save(paymentInfo);
}
public void chargeTip(String authorizationId, double tipAmount) {
// Separate charge for tip
gateway.captureAdditional(authorizationId, tipAmount);
}
public void refund(PaymentInfo paymentInfo, String reason) {
if (paymentInfo.getStatus() == PaymentStatus.HELD) {
gateway.voidAuth(paymentInfo.getAuthorizationId());
} else {
gateway.refund(paymentInfo.getAuthorizationId(), paymentInfo.getAmount());
}
paymentInfo.setStatus(PaymentStatus.REFUNDED);
paymentRepo.save(paymentInfo);
}
}
Observer Pattern — Event System
// Event bus for decoupled communication
public interface EventListener {
void onEvent(Event event);
}
public class EventBus {
private final Map<String, List<EventListener>> listeners = new ConcurrentHashMap<>();
public void subscribe(String eventType, EventListener listener) {
listeners.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>())
.add(listener);
}
public void publish(Event event) {
List<EventListener> eventListeners = listeners.get(event.getType());
if (eventListeners != null) {
eventListeners.forEach(l -> l.onEvent(event));
}
}
}
// Concrete listeners
public class NotificationEventListener implements EventListener {
@Override
public void onEvent(Event event) {
if (event instanceof OrderPlacedEvent e) {
notificationService.send(e.getCustomerId(),
"Order placed! Status: " + e.getOrder().getStatus());
}
}
}
public class AnalyticsEventListener implements EventListener {
@Override
public void onEvent(Event event) {
analyticsService.track(event);
}
}
public class DriverReassignmentListener implements EventListener {
@Override
public void onEvent(Event event) {
if (event instanceof DriverCancelledEvent e) {
driverMatchingService.findAndAssignDriver(e.getOrder());
}
}
}
Database Schema
CREATE TABLE orders (
id VARCHAR(36) PRIMARY KEY,
customer_id VARCHAR(36) NOT NULL,
restaurant_id VARCHAR(36) NOT NULL,
status VARCHAR(20) NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
delivery_address JSON,
payment_info JSON,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
INDEX idx_customer (customer_id),
INDEX idx_restaurant (restaurant_id),
INDEX idx_status (status)
);
CREATE TABLE order_items (
id VARCHAR(36) PRIMARY KEY,
order_id VARCHAR(36) NOT NULL,
menu_item_id VARCHAR(36) NOT NULL,
quantity INT NOT NULL,
price DECIMAL(10,2) NOT NULL,
special_instructions TEXT,
FOREIGN KEY (order_id) REFERENCES orders(id)
);
CREATE TABLE drivers (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL,
rating DECIMAL(3,2) DEFAULT 5.00,
current_lat DECIMAL(10,7),
current_lng DECIMAL(10,7),
current_order_id VARCHAR(36),
vehicle_type VARCHAR(50),
INDEX idx_status_location (status, current_lat, current_lng)
);
CREATE TABLE restaurants (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
lat DECIMAL(10,7) NOT NULL,
lng DECIMAL(10,7) NOT NULL,
is_open BOOLEAN DEFAULT TRUE,
rating DECIMAL(3,2),
delivery_fee DECIMAL(10,2),
min_order DECIMAL(10,2)
);
CREATE TABLE menu_items (
id VARCHAR(36) PRIMARY KEY,
restaurant_id VARCHAR(36) NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
category VARCHAR(100),
is_available BOOLEAN DEFAULT TRUE,
FOREIGN KEY (restaurant_id) REFERENCES restaurants(id)
);
Common Interview Follow-Up Questions
Q: How do you handle driver cancellation?
- Listen for DriverCancelledEvent → trigger reassignment with expanded radius
- Notify customer of delay, offer cancellation option
- If no driver found within 10 min, auto-cancel and refund
Q: How do you handle restaurant taking too long?
- Timeout: if not READY within 30 min, notify customer
- Allow customer to cancel with full refund before pickup
- Auto-release held payment after 2 hours
Q: How do you handle高峰期 surge pricing?
- Count active orders per area tile (geohash)
- If demand/supply ratio > threshold, apply surge multiplier
- Show estimated total before order placement
Q: How to prevent double assignment?
- Optimistic locking on driver record (version column)
- Redis distributed lock:
SETNX driver:assign:{driverId} orderId EX 30 - If lock fails, skip to next driver
Q: How do you handle order splitting across restaurants?
- One parent order, multiple child orders per restaurant
- Each child has independent state machine
- Parent status = aggregate of children (all delivered → delivered)
- Separate deliveries, one payment
Practice Problems
Design a scalable Food Delivery (LLD) system. Cover high-level architecture, data model, and API design.
Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliabilityHow would you scale Food Delivery (LLD) to handle 10x the current load? Identify bottlenecks and solutions.
Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decompositionAnalyze potential failure modes for Food Delivery (LLD) and design mitigation strategies.
Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradationQuiz
1. What is the correct order state transition when a restaurant marks food as ready?
2. Which design pattern is most appropriate for order state transitions?
3. Why use a payment HOLD instead of charging immediately when the order is placed?
4. What is the purpose of the Strategy Pattern in driver matching?
5. How should location updates be stored for real-time tracking?
Flashcards
Question
What are the 6 states in a food delivery order lifecycle?
Click to reveal answer
Answer
PLACED → CONFIRMED → PREPARING → READY → PICKED_UP → DELIVERED
Question
Which design pattern encapsulates state-specific behavior and transitions?
Click to reveal answer
Answer
State Pattern — each state class (PlacedState, ConfirmedState, etc.) defines its own next() and cancel() methods
Question
Why use a payment HOLD instead of immediate charge?
Click to reveal answer
Answer
Hold authorizes funds without capturing. On cancellation, void the hold (no refund needed). Charge only on delivery completion.
Question
How do you find the nearest available driver?
Click to reveal answer
Answer
Query drivers within radius (e.g., 5km) using geospatial index, then use Haversine formula to sort by distance. Use Strategy Pattern for swappable matching logic.
Question
What database is best for real-time driver location storage?
Click to reveal answer
Answer
Redis — O(1) reads/writes, TTL for auto-expiration, in-memory for low latency. Drivers report every ~5 seconds, so speed is critical.
Question
How do you prevent double-assigning a driver to two orders?
Click to reveal answer
Answer
Optimistic locking (version column) + Redis distributed lock (SETNX). If lock fails, try next driver.
Question
What pattern decouples order events from side effects (notifications, analytics)?
Click to reveal answer
Answer
Observer Pattern via EventBus. Listeners subscribe to events (OrderPlacedEvent, etc.) and react independently.
Question
How is ETA calculated for a delivery?
Click to reveal answer
Answer
ETA = remaining distance / (base speed × traffic factor × weather factor). Base speed ~20 km/h city driving. Use Haversine for straight-line, road network for accurate.
Revision Notes
Key Takeaways
- 1.Order lifecycle is a finite state machine — use State pattern for clean transitions
- 2.Payment flow: HOLD on placement → CAPTURE on delivery → VOID on cancellation
- 3.Driver matching: find nearby (geospatial query) → sort (distance/rating) → assign (optimistic lock)
- 4.Real-time tracking: driver reports to Redis (fast) + Kafka (stream) → WebSocket to customer
- 5.Event-driven architecture: EventBus decouples order events from side effects
- 6.Concurrency: Redis distributed locks + optimistic locking prevent double-assignment
Interview Tips
- •Start with requirements: clarify functional vs non-functional, ask about scale
- •Draw the order state machine first — it drives the entire design
- •Use design patterns where they add value (State for orders, Strategy for matching)
- •Discuss payment hold/release — interviewers love this detail
- •Handle edge cases: driver cancellation, restaurant timeout, no drivers available
- •Mention real-time tracking with WebSocket, not HTTP polling
- •Be ready for follow-ups: surge pricing, order splitting, group orders
- •Know the trade-offs: consistency (strong for payments) vs performance (eventual for location)
Cheat Sheet
Food Delivery LLD Cheat Sheet
Order States
PLACED → CONFIRMED → PREPARING → READY → PICKED_UP → DELIVERED
└→ CANCELLED (any active state)
Core Classes
- Order: id, customer, restaurant, items, state, total, payment
- Restaurant: id, location, menu, isOpen, rating
- Driver: id, location, status, rating, currentOrderId
- Delivery: id, orderId, driverId, pickup, dropoff, eta
Design Patterns Used
| Pattern | Where | Why |
|---|---|---|
| State | Order states | Clean state transitions, each state defines behavior |
| Strategy | Driver matching | Swappable algorithms (nearest vs rating-weighted) |
| Observer | Event system | Decouple order events from notifications/analytics |
| Template Method | Delivery pipeline | Common flow with customizable steps |
Key Algorithms
- Driver Matching: Haversine distance, query by geohash radius, sort by distance
- ETA: distance / (speed × traffic_factor)
- Geospatial: Redis GEOSEARCH or PostGIS ST_DWithin
Payment Flow
- HOLD on order placement (authorize)
- KEEP HOLD during preparation
- CAPTURE on delivery (charge)
- VOID on cancellation (no refund needed)
Scalability
- Order Service: Stateless, horizontal scaling, partition by orderId
- Driver Location: Redis for hot data, Kafka for event streaming
- Matching: Partition by city/geohash, each region handles own matching
- Notifications: Async via message queue, per-channel retry logic
Common Pitfalls
- Don't charge before delivery — use holds
- Handle driver cancellation → auto-reassign
- Restaurant timeout → notify customer, allow cancel
- Optimistic locking on driver to prevent double-assign
- WebSocket for real-time tracking (not polling)