Skip to content
advancedPhase 52 · HLD Case Studies

Food Delivery (HLD)

Design a food delivery platform end-to-end.

2h
0 problems
Topic Progress0%

Requirements & Scope

Functional Requirements

  • Browse restaurants: Users can view restaurants based on location, cuisine, ratings, delivery time
  • View menus: Display restaurant menus with categories, items, prices, customization options
  • Place orders: Add items to cart, apply coupons, select delivery address, choose payment method
  • Track delivery: Real-time tracking of driver location and order status
  • Rate restaurants/delivery: Post-delivery rating and review system

Non-Functional Requirements

  • Real-time order tracking: Driver location updates every 5 seconds, sub-second latency to customer
  • High availability: 99.99% uptime, failover across regions
  • Consistent pricing: No price mismatches between menu and checkout
  • Peak hour handling: Support 10x traffic during lunch/dinner rushes
  • Low latency: API responses under 200ms for browsing, 500ms for order placement

Scale Estimation

Metric Daily Per Second
Orders 5M ~60
Restaurant updates 100K ~1
Driver location updates 500M ~6K
Active drivers 500K -
API requests 500M ~6K

Core Entities

  • Restaurant, Menu, MenuItem, Order, OrderItem, Delivery, Driver, Payment, User, Address

Restaurant & Menu System

Restaurant Service

@Service
public class RestaurantService {
    @Autowired private RestaurantRepository restaurantRepo;
    @Autowired private MenuRepository menuRepo;
    @Autowired private CacheService cacheService;
    
    public List<Restaurant> searchRestaurants(Location location, String cuisine, 
                                              double maxDeliveryTime) {
        String cacheKey = String.format("restaurants:%s:%s:%.1f", 
            location.getHash(), cuisine, maxDeliveryTime);
        
        return cacheService.getOrLoad(cacheKey, () -> {
            List<Restaurant> nearby = restaurantRepo
                .findByLocationWithin(location, MAX_RADIUS_KM);
            
            return nearby.stream()
                .filter(r -> cuisine == null || r.getCuisines().contains(cuisine))
                .filter(r -> r.isOpen())
                .filter(r -> r.getEstimatedDeliveryTime() <= maxDeliveryTime)
                .sorted(Comparator.comparingDouble(Restaurant::getRating).reversed())
                .collect(Collectors.toList());
        }, Duration.ofMinutes(5));
    }
}

Menu Management

  • Dynamic pricing: Restaurants can set time-based prices (lunch specials, happy hour)
  • Availability: Real-time item availability tracking (sold out, 86'd items)
  • Customizations: Support for add-ons, sides, special instructions
  • Categories: Organized menu with categories, subcategories

Menu Data Model

CREATE TABLE restaurants (
    id BIGINT PRIMARY KEY,
    name VARCHAR(255),
    address TEXT,
    location GEOGRAPHY(POINT, 4326),
    cuisine_types JSON,
    rating DECIMAL(2,1),
    total_ratings INT,
    avg_delivery_time INT,
    is_open BOOLEAN,
    operating_hours JSON
);

CREATE TABLE menu_items (
    id BIGINT PRIMARY KEY,
    restaurant_id BIGINT REFERENCES restaurants(id),
    category VARCHAR(100),
    name VARCHAR(255),
    description TEXT,
    price DECIMAL(10,2),
    image_url VARCHAR(500),
    is_available BOOLEAN,
    customization_options JSON,
    INDEX idx_restaurant_category (restaurant_id, category)
);

Order Flow & State Machine

Order State Machine

PLACED → CONFIRMED → PREPARING → READY → PICKED_UP → DELIVERED → COMPLETED
   ↓          ↓           ↓         ↓         ↓           ↓          ↓
 CANCELLED  REJECTED   CANCELLED  -      CANCELLED   FAILED    RATED

State Transitions

From To Trigger Actor
PLACED CONFIRMED Restaurant accepts order Restaurant
PLACED REJECTED Restaurant rejects (capacity, items unavailable) Restaurant
CONFIRMED PREPARING Restaurant starts preparing Restaurant
PREPARING READY Food ready for pickup Restaurant
READY PICKED_UP Driver picks up order Driver
PICKED_UP DELIVERED Driver delivers to customer Driver
DELIVERED COMPLETED Customer confirms delivery System (auto 30min)
Any CANCELLED Customer/restaurant cancels Customer/Restaurant

Order Service Implementation

@Service
public class OrderService {
    @Autowired private OrderRepository orderRepo;
    @Autowired private PaymentService paymentService;
    @Autowired private DeliveryService deliveryService;
    @Autowired private NotificationService notificationService;
    
    @Transactional
    public Order placeOrder(CreateOrderRequest request) {
        // 1. Validate restaurant is open
        Restaurant restaurant = restaurantService.getRestaurant(request.getRestaurantId());
        if (!restaurant.isOpen()) {
            throw new RestaurantClosedException();
        }
        
        // 2. Validate menu items and prices
        List<OrderItem> items = validateAndGetItems(request.getItems());
        BigDecimal total = calculateTotal(items, request.getCouponCode());
        
        // 3. Create order
        Order order = Order.builder()
            .userId(request.getUserId())
            .restaurantId(request.getRestaurantId())
            .items(items)
            .totalAmount(total)
            .deliveryAddress(request.getAddress())
            .status(OrderStatus.PLACED)
            .createdAt(Instant.now())
            .build();
        
        orderRepo.save(order);
        
        // 4. Process payment (with idempotency key)
        paymentService.charge(order.getId(), total, request.getPaymentMethod());
        
        // 5. Notify restaurant
        notificationService.notifyRestaurant(restaurant, order);
        
        // 6. Start order expiry timer (15 min for restaurant to accept)
        scheduler.schedule(() -> expireOrder(order.getId()), 15, TimeUnit.MINUTES);
        
        return order;
    }
    
    public void updateOrderStatus(Long orderId, OrderStatus newStatus) {
        Order order = orderRepo.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));
        
        validateStateTransition(order.getStatus(), newStatus);
        
        order.setStatus(newStatus);
        order.setUpdatedAt(Instant.now());
        orderRepo.save(order);
        
        // Trigger side effects based on status
        switch (newStatus) {
            case CONFIRMED:
                deliveryService.findAndAssignDriver(order);
                notificationService.notifyOrderConfirmed(order);
                break;
            case READY:
                deliveryService.notifyDriverForPickup(order);
                break;
            case PICKED_UP:
                notificationService.notifyOrderPickedUp(order);
                break;
            case DELIVERED:
                paymentService.capturePayment(order.getId());
                notificationService.notifyOrderDelivered(order);
                break;
        }
    }
}

Order Data Model

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    user_id BIGINT REFERENCES users(id),
    restaurant_id BIGINT REFERENCES restaurants(id),
    status ENUM('PLACED','CONFIRMED','PREPARING','READY',
                'PICKED_UP','DELIVERED','COMPLETED','CANCELLED'),
    total_amount DECIMAL(10,2),
    delivery_address JSON,
    special_instructions TEXT,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    estimated_delivery_time TIMESTAMP,
    INDEX idx_user_orders (user_id, created_at),
    INDEX idx_restaurant_orders (restaurant_id, status)
);

CREATE TABLE order_items (
    id BIGINT PRIMARY KEY,
    order_id BIGINT REFERENCES orders(id),
    menu_item_id BIGINT REFERENCES menu_items(id),
    quantity INT,
    unit_price DECIMAL(10,2),
    customizations JSON,
    special_instructions TEXT
);

Delivery & Dispatch

Dispatch Algorithm

@Service
public class DispatchService {
    private static final double MAX_ASSIGNMENT_RADIUS_KM = 5.0;
    private static final int MAX_RETRIES = 3;
    
    public DriverAssignment assignDriver(Order order) {
        Restaurant restaurant = restaurantService.getRestaurant(order.getRestaurantId());
        
        // Find nearby available drivers
        List<Driver> candidates = driverService.findNearbyDrivers(
            restaurant.getLocation(), MAX_ASSIGNMENT_RADIUS_KM);
        
        // Score and rank drivers
        List<DriverScore> scored = candidates.stream()
            .map(driver -> new DriverScore(driver, calculateScore(driver, restaurant, order)))
            .sorted(Comparator.comparingDouble(DriverScore::getScore).reversed())
            .collect(Collectors.toList());
        
        // Try assigning to top drivers
        for (DriverScore driverScore : scored) {
            if (tryAssign(driverScore.getDriver(), order)) {
                return new DriverAssignment(driverScore.getDriver(), order);
            }
        }
        
        // If no driver found, expand search or use surge pricing
        return expandSearchAndAssign(order);
    }
    
    private double calculateScore(Driver driver, Restaurant restaurant, Order order) {
        double distanceScore = 1.0 / (driver.getLocation()
            .distanceTo(restaurant.getLocation()) + 0.1);
        double ratingScore = driver.getRating() / 5.0;
        double acceptanceScore = driver.getAcceptanceRate();
        double experienceScore = driver.getTotalDeliveries() / 10000.0;
        
        return (distanceScore * 0.4) + (ratingScore * 0.3) + 
               (acceptanceScore * 0.2) + (experienceScore * 0.1);
    }
    
    @Retryable(maxAttempts = MAX_RETRIES)
    private boolean tryAssign(Driver driver, Order order) {
        // Send assignment notification to driver
        boolean accepted = notificationService.sendAssignment(driver, order, 
            Duration.ofSeconds(30));
        
        if (!accepted) {
            driver.incrementRejectionCount();
            return false;
        }
        
        deliveryService.createDelivery(order, driver);
        return true;
    }
}

Real-Time Tracking

@Service
public class TrackingService {
    @Autowired private WebSocketService webSocketService;
    @Autowired private DriverLocationRepository locationRepo;
    
    public void updateDriverLocation(String driverId, Location location) {
        // Store location
        locationRepo.save(DriverLocation.builder()
            .driverId(driverId)
            .location(location)
            .timestamp(Instant.now())
            .build());
        
        // Find active delivery for this driver
        Delivery delivery = deliveryService.getActiveDeliveryByDriver(driverId);
        if (delivery != null) {
            // Calculate ETA
            int etaMinutes = calculateETA(location, delivery.getDeliveryAddress());
            
            // Push update to customer via WebSocket
            TrackingUpdate update = TrackingUpdate.builder()
                .deliveryId(delivery.getId())
                .driverLocation(location)
                .etaMinutes(etaMinutes)
                .build();
            
            webSocketService.sendToCustomer(delivery.getOrderId(), update);
        }
    }
    
    private int calculateETA(Location driverLocation, Address deliveryAddress) {
        // Use Google Maps API or similar for ETA calculation
        double distanceKm = driverLocation.distanceTo(deliveryAddress.getLocation());
        // Assume average speed of 20 km/h in urban area
        return (int) Math.ceil((distanceKm / 20.0) * 60);
    }
}

Surge Pricing

  • Demand detection: Monitor order volume vs available drivers per region
  • Dynamic pricing: Increase delivery fee when driver-to-order ratio drops below threshold
  • Driver incentives: Bonus per delivery during high-demand periods
  • Customer communication: Show estimated delivery time and surge multiplier

Practice Problems

0/3solved
Design Food Delivery (Design DoorDash/UberEats) System

Design a scalable Food Delivery (Design DoorDash/UberEats) 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 & reliability
Food Delivery (Design DoorDash/UberEats) Scaling

How would you scale Food Delivery (Design DoorDash/UberEats) 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 decomposition
Food Delivery (Design DoorDash/UberEats) Failure Modes

Analyze potential failure modes for Food Delivery (Design DoorDash/UberEats) 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 degradation

Quiz

1. What is the recommended approach to handle restaurant order acceptance timeout?

Question 1 options

2. How should driver location updates be handled for real-time tracking?

Question 2 options

3. What happens when a driver rejects an order assignment?

Question 3 options

4. How should menu prices be handled during peak hours?

Question 4 options

5. What is the best approach for handling order state transitions?

Question 5 options

Flashcards

Question

What is the order state machine for food delivery?

Answer

PLACED → CONFIRMED → PREPARING → READY → PICKED_UP → DELIVERED → COMPLETED. Each transition has specific triggers and actors.

Question

How does the dispatch algorithm assign drivers?

Answer

Scores drivers based on distance (40%), rating (30%), acceptance rate (20%), and experience (10%). Tries top-scored drivers first with retry logic.

Question

What protocol should be used for real-time order tracking?

Answer

WebSocket for push-based updates from server to client. Driver location stored in Redis, broadcast to customers via WebSocket channels.

Question

How should menu data be cached?

Answer

Cache menu in Redis with 5-minute TTL. Invalidate cache when restaurant updates menu. Use cache-aside pattern for menu reads.

Question

What is surge pricing and when is it triggered?

Answer

Dynamic delivery fee increase when driver-to-order ratio drops below threshold in a region. Incentivizes more drivers and manages demand.

Question

How do you handle order timeout?

Answer

Set 15-minute timer when order is PLACED. If restaurant doesn't accept within timeout, auto-cancel order and refund payment.

Question

What data model is needed for food delivery?

Answer

Restaurants, MenuItems, Orders, OrderItems, Deliveries, Drivers, DriverLocations, Payments, Users, Addresses.

Revision Notes

Key Takeaways

  • 1.Order state machine is critical for managing order lifecycle
  • 2.Real-time tracking requires WebSocket and efficient location storage
  • 3.Dispatch algorithm must balance distance, driver quality, and availability
  • 4.Surge pricing helps balance supply and demand during peak hours
  • 5.Menu caching reduces database load for frequent read operations
  • 6.Idempotency is essential for payment and order operations

Interview Tips

  • Start with functional requirements and scale estimation
  • Draw the high-level architecture with all services before diving deep
  • Explain the order state machine clearly with all transitions
  • Discuss real-time tracking approach (WebSocket vs polling)
  • Explain the dispatch algorithm and how it handles driver rejection
  • Mention how you handle peak hours and surge pricing
  • Be ready to discuss database schema and indexing strategy

Cheat Sheet

Food Delivery System - Key Points

Architecture Components

  • Restaurant Service: Menu management, availability, pricing
  • Order Service: Order lifecycle, state machine
  • Delivery Service: Driver dispatch, tracking
  • Payment Service: Charges, refunds, idempotency
  • Notification Service: Push notifications, SMS, email

Order State Machine

PLACED → CONFIRMED → PREPARING → READY → PICKED_UP → DELIVERED → COMPLETED

Key Design Decisions

  1. Real-time tracking: WebSocket + Redis for location storage
  2. Dispatch: Score-based algorithm (distance, rating, acceptance)
  3. Caching: Menu cached in Redis (5min TTL)
  4. Scaling: Database sharding by region, queue for order processing
  5. Reliability: Idempotent operations, retry logic, timeouts

Scale Numbers

  • 5M daily orders (~60/sec)
  • 500K active drivers
  • 6K location updates/second
  • 500M API requests/day