Skip to content
intermediatePhase 50 · LLD Practice

Food Delivery

Design a food delivery system with restaurants, orders, and delivery.

2h
0 problems
Topic Progress0%

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

0/3solved
Design Food Delivery (LLD) System

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 & reliability
Food Delivery (LLD) Scaling

How 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 decomposition
Food Delivery (LLD) Failure Modes

Analyze 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 degradation

Quiz

1. What is the correct order state transition when a restaurant marks food as ready?

Question 1 options

2. Which design pattern is most appropriate for order state transitions?

Question 2 options

3. Why use a payment HOLD instead of charging immediately when the order is placed?

Question 3 options

4. What is the purpose of the Strategy Pattern in driver matching?

Question 4 options

5. How should location updates be stored for real-time tracking?

Question 5 options

Flashcards

Question

What are the 6 states in a food delivery order lifecycle?

Answer

PLACED → CONFIRMED → PREPARING → READY → PICKED_UP → DELIVERED

Question

Which design pattern encapsulates state-specific behavior and transitions?

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?

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?

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?

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?

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)?

Answer

Observer Pattern via EventBus. Listeners subscribe to events (OrderPlacedEvent, etc.) and react independently.

Question

How is ETA calculated for a delivery?

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

  1. HOLD on order placement (authorize)
  2. KEEP HOLD during preparation
  3. CAPTURE on delivery (charge)
  4. 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)