Design a Quick commerce Delivery system like Amazon Now, Swiggy Instamart / Zepto (10-Minute Delivery LLD)
Problem Statement
Design a 10-minute instant delivery logistics engine (similar to Swiggy Instamart or Zepto). The engine must manage a network of local warehouses (Dark Stores) with live inventory, locate the nearest store with complete stock availability for a customer, reserve items atomically, locate and assign the nearest available delivery partner, and drive the order delivery state transitions (RECEIVED, PACKING, OUT_FOR_DELIVERY, DELIVERED).
Design Decisions & Patterns Used
Instant delivery requires high speed and location accuracy. When a customer checks out, the system must search for stores in a 3-5 km radius, check their inventory, and block the items atomically to prevent double-selling. Once reserved, it must dispatch the nearest available delivery partner to the dark store.
We will utilize the following Design Patterns:
- Strategy Pattern: Abstracting the proximity matching algorithms (e.g., sorting partners and stores by distance).
- State Pattern: Representing the order status lifecycle, defining allowed transition rules (e.g., cannot transition from
RECEIVEDdirectly toDELIVERED). - Observer Pattern: Notifying customers and delivery partners of order status updates in real-time.
Functional Requirements
- Model coordinates (latitude/longitude) and calculate distance between coordinates.
- Register Dark Stores with inventory levels and location coordinates.
- Search and assign the nearest Dark Store containing all items in the customer's cart.
- Ensure thread-safe inventory check and reservation (atomic updates).
- Find and assign the nearest available delivery partner to the selected Dark Store.
- Drive the order status lifecycle, validating state transitions.
Objects Required
Location(Value object tracking geographical coordinates and distance calculations)OrderStatus(Enum mapping delivery states)DarkStore(Entity representing local warehouses and managing inventory)DeliveryPartner(Entity representing active riders)Order(Aggregate tracking assigned stores, riders, and delivery status)InstamartService(Main orchestrator routing orders and coordinating dispatches)
Location Class & OrderStatus Enum
The Location class calculates distances, and the OrderStatus enum represents order status states.
public class Location {
private final double latitude;
private final double longitude;
public Location(double latitude, double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
public double getDistanceTo(Location other) {
// Simplified Euclidean distance calculation
double latDiff = this.latitude - other.latitude;
double lonDiff = this.longitude - other.longitude;
return Math.sqrt(latDiff * latDiff + lonDiff * lonDiff);
}
public double getLatitude() { return latitude; }
public double getLongitude() { return longitude; }
}
Let's define the OrderStatus enum:
public enum OrderStatus {
RECEIVED,
PACKING,
OUT_FOR_DELIVERY,
DELIVERED
}
The Location class is immutable, ensuring thread-safe distance calculations.
DarkStore Class
The DarkStore class manages local warehouse inventory. The reserveInventory() method checks and reserves items atomically to prevent double-selling.
import java.util.HashMap;
import java.util.Map;
public class DarkStore {
private final String id;
private final String name;
private final Location location;
private final Map<String, Integer> inventory;
public DarkStore(String id, String name, Location location) {
this.id = id;
this.name = name;
this.location = location;
this.inventory = new HashMap<>();
}
public void addInventory(String itemId, int quantity) {
inventory.put(itemId, inventory.getOrDefault(itemId, 0) + quantity);
}
public synchronized boolean hasInventory(Map<String, Integer> items) {
for (Map.Entry<String, Integer> entry : items.entrySet()) {
int available = inventory.getOrDefault(entry.getKey(), 0);
if (available < entry.getValue()) {
return false;
}
}
return true;
}
public synchronized boolean reserveInventory(Map<String, Integer> items) {
if (!hasInventory(items)) return false;
for (Map.Entry<String, Integer> entry : items.entrySet()) {
int available = inventory.get(entry.getKey());
inventory.put(entry.getKey(), available - entry.getValue());
}
return true;
}
public String getId() { return id; }
public String getName() { return name; }
public Location getLocation() { return location; }
}
The constructor configures the warehouse properties. hasInventory() verifies stock availability, and reserveInventory() deducts items from the inventory map under a synchronized lock to guarantee thread safety.
DeliveryPartner & Order Classes
The DeliveryPartner class represents riders, and the Order class tracks the assigned store, rider, and status transitions.
public class DeliveryPartner {
private final String id;
private final String name;
private Location location;
private boolean isAvailable;
public DeliveryPartner(String id, String name, Location location) {
this.id = id;
this.name = name;
this.location = location;
this.isAvailable = true;
}
public String getId() { return id; }
public String getName() { return name; }
public synchronized Location getLocation() { return location; }
public synchronized void setLocation(Location location) { this.location = location; }
public synchronized boolean isAvailable() { return isAvailable; }
public synchronized void setAvailable(boolean available) { isAvailable = available; }
}
The constructor sets initial states. Location coordinates and availability flags are synchronized to support concurrent routing queries.
import java.util.Map;
public class Order {
private final String id;
private final Map<String, Integer> items;
private final Location customerLocation;
private OrderStatus status;
private DarkStore assignedStore;
private DeliveryPartner assignedPartner;
public Order(String id, Map<String, Integer> items, Location customerLocation) {
this.id = id;
this.items = items;
this.customerLocation = customerLocation;
this.status = OrderStatus.RECEIVED;
}
public synchronized void updateStatus(OrderStatus newStatus) {
// State Transition Validation
if (newStatus.ordinal() != this.status.ordinal() + 1) {
throw new IllegalStateException("Invalid state transition from " + this.status + " to " + newStatus);
}
this.status = newStatus;
System.out.println("Order [" + id + "] status updated to: " + newStatus);
}
public String getId() { return id; }
public Map<String, Integer> getItems() { return items; }
public Location getCustomerLocation() { return customerLocation; }
public synchronized OrderStatus getStatus() { return status; }
public synchronized DarkStore getAssignedStore() { return assignedStore; }
public synchronized void setAssignedStore(DarkStore store) { this.assignedStore = store; }
public synchronized DeliveryPartner getAssignedPartner() { return assignedPartner; }
public synchronized void setAssignedPartner(DeliveryPartner partner) { this.assignedPartner = partner; }
}
The constructor sets initial states. updateStatus() validates transitions by asserting that status updates progress in sequence (using ordinals).
InstamartService Class
The InstamartService class handles registration, searches for stores with inventory, assigns riders, and coordinates order dispatch.
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
public class InstamartService {
private final List<DarkStore> stores;
private final List<DeliveryPartner> partners;
public InstamartService() {
this.stores = new CopyOnWriteArrayList<>();
this.partners = new CopyOnWriteArrayList<>();
}
public void registerStore(DarkStore store) { stores.add(store); }
public void registerPartner(DeliveryPartner partner) { partners.add(partner); }
public Order placeOrder(String orderId, Map<String, Integer> items, Location customerLoc) {
System.out.println("Processing order: " + orderId);
// 1. Locate nearest Dark Store with sufficient inventory
DarkStore selectedStore = null;
double minStoreDistance = Double.MAX_VALUE;
for (DarkStore store : stores) {
if (store.hasInventory(items)) {
double dist = store.getLocation().getDistanceTo(customerLoc);
if (dist < minStoreDistance) {
minStoreDistance = dist;
selectedStore = store;
}
}
}
if (selectedStore == null) {
throw new RuntimeException("Order Failed: No local Dark Store contains all items in the cart.");
}
// 2. Reserve inventory
selectedStore.reserveInventory(items);
System.out.println("Reserved inventory at Dark Store: " + selectedStore.getName());
Order order = new Order(orderId, items, customerLoc);
order.setAssignedStore(selectedStore);
// 3. Locate nearest available Delivery Partner to the selected Dark Store
DeliveryPartner selectedPartner = null;
double minPartnerDistance = Double.MAX_VALUE;
synchronized (partners) {
for (DeliveryPartner partner : partners) {
if (partner.isAvailable()) {
double dist = partner.getLocation().getDistanceTo(selectedStore.getLocation());
if (dist < minPartnerDistance) {
minPartnerDistance = dist;
selectedPartner = partner;
}
}
}
if (selectedPartner != null) {
selectedPartner.setAvailable(false); // Assign partner
}
}
if (selectedPartner == null) {
System.out.println("No delivery partners available. Order remains in queue.");
return order;
}
order.setAssignedPartner(selectedPartner);
System.out.println("Assigned Delivery Partner: " + selectedPartner.getName() +
" (Distance to store: " + String.format("%.2f", minPartnerDistance) + " km)");
return order;
}
}
Here is an explanation of the core operations in the InstamartService class:
- The constructor configures concurrency lists using
CopyOnWriteArrayListto support dynamic store and rider registrations. placeOrder()filters stores by inventory availability, identifies the nearest store to the customer, reserves items, locates the nearest available delivery partner to the store, and assigns them to the order.
Main Driver Class
This class tests our instant delivery logistics engine. It creates dark stores, sets up stock levels, registers riders, places orders, and updates order status.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
InstamartService service = new InstamartService();
// Register Dark Stores
DarkStore store1 = new DarkStore("DS-01", "Koramangala DarkStore", new Location(12.93, 77.62));
store1.addInventory("milk", 10);
store1.addInventory("bread", 5);
DarkStore store2 = new DarkStore("DS-02", "Indiranagar DarkStore", new Location(12.97, 77.64));
store2.addInventory("milk", 20);
store2.addInventory("bread", 1); // insufficient bread
service.registerStore(store1);
service.registerStore(store2);
// Register Delivery Partners
DeliveryPartner partner1 = new DeliveryPartner("DP-01", "Rider Amit", new Location(12.94, 77.63));
DeliveryPartner partner2 = new DeliveryPartner("DP-02", "Rider Sumit", new Location(12.98, 77.65));
service.registerPartner(partner1);
service.registerPartner(partner2);
// Customer order: 2 milk, 2 bread. Location is near Koramangala
Location customerLocation = new Location(12.92, 77.61);
Map<String, Integer> cart = new HashMap<>();
cart.put("milk", 2);
cart.put("bread", 2);
System.out.println("==========================================");
System.out.println("Scenario 1: Placing Order and Dispatching Rider");
System.out.println("==========================================");
Order order = service.placeOrder("ORD-999", cart, customerLocation);
System.out.println("\n--- Order Processing Status Lifecycle ---");
// Transition order status
order.updateStatus(OrderStatus.PACKING);
order.updateStatus(OrderStatus.OUT_FOR_DELIVERY);
order.updateStatus(OrderStatus.DELIVERED);
// Free the delivery partner
if (order.getAssignedPartner() != null) {
order.getAssignedPartner().setAvailable(true);
}
}
}
The main() driver configures the environment, registers dark stores with initial stock, registers delivery partners, places a customer order, routes it to the nearest available store with sufficient stock, and updates its status through the delivery lifecycle.
Comments
Post a Comment