Below is a comprehensive Low-Level Design (LLD) for a Food Delivery System implemented in Java. This design models key entities (restaurants, menus, orders, delivery agents, and customers), manages order processing and delivery assignment, and provides a seamless user experience with secure authentication, payment processing, and real‑time order tracking. The design makes use of multiple design patterns (Singleton, Factory, Strategy, and Observer) to achieve modularity, scalability, and maintainability.
1. Overview
Modeling Entities
Restaurant & Menu:
Restaurant: Represents a food outlet with attributes such as name, address, and its menu.
Menu & MenuItem: Each restaurant has a menu consisting of various menu items with details (name, description, price).
Order & OrderItem:
Order: Captures an order placed by a customer, including order items, status, timestamp, and the assigned delivery agent.
OrderItem: Each order item is linked to a specific menu item and specifies the quantity ordered.
Customer & DeliveryAgent:
Customer: Represents a user of the system with authentication credentials and order history.
DeliveryAgent: Represents a delivery person; orders will be assigned to the nearest available agent using an order assignment algorithm (via the Strategy Pattern).
Payment:
Represents the payment details for an order.
Order Management
Order Assignment & Routing:
The system employs a DeliveryAssignmentStrategy interface (Strategy Pattern) so that different algorithms can be used to assign orders to delivery agents and calculate the optimal route for timely delivery. (For simplicity, our sample chooses the first available agent.)
OrderManager (Singleton & Factory):
Centralizes order creation, inventory deduction from restaurants, and updating order status. It also notifies observers of order updates.
Order Tracking (Observer Pattern):
Observers (for example, mobile notifications) are registered to receive real‑time updates when an order is placed, assigned, or delivered.
2. Detailed Java Code
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
// --------------------- ENUMS ---------------------
// Enum for order status.
enum OrderStatus {
PLACED,
ASSIGNED,
DELIVERING,
DELIVERED,
CANCELLED
}
// --------------------- ENTITIES ---------------------
// Restaurant entity represents a food outlet.
class Restaurant {
private String restaurantId;
private String name;
private String address;
private Menu menu;
public Restaurant(String restaurantId, String name, String address, Menu menu) {
this.restaurantId = restaurantId;
this.name = name;
this.address = address;
this.menu = menu;
}
public String getRestaurantId() { return restaurantId; }
public String getName() { return name; }
public String getAddress() { return address; }
public Menu getMenu() { return menu; }
}
// Menu entity represents a collection of menu items.
class Menu {
private List<MenuItem> items;
public Menu() {
this.items = new ArrayList<>();
}
public void addItem(MenuItem item) {
items.add(item);
}
public List<MenuItem> getItems() {
return items;
}
public MenuItem getItemById(String itemId) {
for (MenuItem item : items) {
if (item.getItemId().equals(itemId)) {
return item;
}
}
return null;
}
}
// MenuItem represents an individual food item.
class MenuItem {
private String itemId;
private String name;
private String description;
private double price;
public MenuItem(String itemId, String name, String description, double price) {
this.itemId = itemId;
this.name = name;
this.description = description;
this.price = price;
}
public String getItemId() { return itemId; }
public String getName() { return name; }
public String getDescription() { return description; }
public double getPrice() { return price; }
}
// Customer represents a user placing orders.
class Customer {
private String customerId;
private String name;
private String email;
private String password; // In production, securely hashed.
private List<Order> orders;
public Customer(String customerId, String name, String email, String password) {
this.customerId = customerId;
this.name = name;
this.email = email;
this.password = password;
this.orders = new ArrayList<>();
}
public String getCustomerId() { return customerId; }
public String getName() { return name; }
public String getEmail() { return email; }
public String getPassword() { return password; }
public List<Order> getOrders() { return orders; }
public void addOrder(Order order) {
orders.add(order);
}
}
// DeliveryAgent represents a delivery person.
class DeliveryAgent {
private String agentId;
private String name;
private String currentLocation; // For simplicity, a string representation.
private boolean isAvailable;
public DeliveryAgent(String agentId, String name, String currentLocation) {
this.agentId = agentId;
this.name = name;
this.currentLocation = currentLocation;
this.isAvailable = true;
}
public String getAgentId() { return agentId; }
public String getName() { return name; }
public String getCurrentLocation() { return currentLocation; }
public boolean isAvailable() { return isAvailable; }
public void setAvailable(boolean available) {
isAvailable = available;
}
// Simulate updating location.
public void updateLocation(String newLocation) {
this.currentLocation = newLocation;
}
}
// OrderItem represents an item ordered from a restaurant.
class OrderItem {
private MenuItem menuItem;
private int quantity;
private double unitPrice;
public OrderItem(MenuItem menuItem, int quantity) {
this.menuItem = menuItem;
this.quantity = quantity;
this.unitPrice = menuItem.getPrice();
}
public MenuItem getMenuItem() { return menuItem; }
public int getQuantity() { return quantity; }
public double getUnitPrice() { return unitPrice; }
public double getTotalPrice() { return unitPrice * quantity; }
}
// Order represents a food order.
class Order {
private String orderId;
private Customer customer;
private Restaurant restaurant;
private List<OrderItem> items;
private OrderStatus status;
private LocalDateTime orderTime;
private double totalAmount;
private DeliveryAgent assignedAgent; // The delivery agent assigned to this order.
public Order(String orderId, Customer customer, Restaurant restaurant) {
this.orderId = orderId;
this.customer = customer;
this.restaurant = restaurant;
this.items = new ArrayList<>();
this.status = OrderStatus.PLACED;
this.orderTime = LocalDateTime.now();
this.totalAmount = 0.0;
}
public String getOrderId() { return orderId; }
public Customer getCustomer() { return customer; }
public Restaurant getRestaurant() { return restaurant; }
public List<OrderItem> getItems() { return items; }
public OrderStatus getStatus() { return status; }
public LocalDateTime getOrderTime() { return orderTime; }
public double getTotalAmount() { return totalAmount; }
public DeliveryAgent getAssignedAgent() { return assignedAgent; }
public void addItem(OrderItem item) {
items.add(item);
totalAmount += item.getTotalPrice();
}
public void setStatus(OrderStatus status) {
this.status = status;
}
public void assignAgent(DeliveryAgent agent) {
this.assignedAgent = agent;
setStatus(OrderStatus.ASSIGNED);
}
public void markAsDelivered() {
setStatus(OrderStatus.DELIVERED);
}
}
// Payment represents payment details for an order.
class Payment {
private String paymentId;
private double amount;
private String paymentMethod;
private boolean isSuccessful;
public Payment(String paymentId, double amount, String paymentMethod) {
this.paymentId = paymentId;
this.amount = amount;
this.paymentMethod = paymentMethod;
this.isSuccessful = false;
}
public String getPaymentId() { return paymentId; }
public double getAmount() { return amount; }
public String getPaymentMethod() { return paymentMethod; }
public boolean isSuccessful() { return isSuccessful; }
public void markAsSuccessful() { isSuccessful = true; }
}
// --------------------- RESTAURANT CATALOG ---------------------
/*
* Singleton Pattern: RestaurantCatalog centralizes management of restaurants.
*/
class RestaurantCatalog {
private static RestaurantCatalog instance;
private Map<String, Restaurant> restaurants;
private RestaurantCatalog() {
restaurants = new ConcurrentHashMap<>();
// Pre-load some dummy restaurants with menus.
Menu menu1 = new Menu();
menu1.addItem(new MenuItem("M001", "Burger", "Beef burger with cheese", 8.99));
menu1.addItem(new MenuItem("M002", "Fries", "Crispy french fries", 3.49));
Restaurant r1 = new Restaurant("R001", "FastBite", "123 Main St", menu1);
Menu menu2 = new Menu();
menu2.addItem(new MenuItem("M003", "Sushi Roll", "Fresh salmon sushi roll", 12.99));
menu2.addItem(new MenuItem("M004", "Miso Soup", "Traditional miso soup", 4.99));
Restaurant r2 = new Restaurant("R002", "SushiWorld", "456 Elm St", menu2);
addRestaurant(r1);
addRestaurant(r2);
}
public static synchronized RestaurantCatalog getInstance() {
if (instance == null) {
instance = new RestaurantCatalog();
}
return instance;
}
public void addRestaurant(Restaurant restaurant) {
restaurants.put(restaurant.getRestaurantId(), restaurant);
}
public Restaurant getRestaurantById(String restaurantId) {
return restaurants.get(restaurantId);
}
public Collection<Restaurant> getAllRestaurants() {
return restaurants.values();
}
}
// --------------------- AUTHENTICATION SERVICE ---------------------
/*
* Singleton Pattern: AuthenticationService handles customer login.
*/
class AuthenticationService {
private static AuthenticationService instance;
private Map<String, Customer> customerDB;
private AuthenticationService() {
customerDB = new ConcurrentHashMap<>();
// Pre-load dummy customers.
customerDB.put("alice@example.com", new Customer("C001", "Alice", "alice@example.com", "pass123"));
customerDB.put("bob@example.com", new Customer("C002", "Bob", "bob@example.com", "pass456"));
}
public static synchronized AuthenticationService getInstance() {
if (instance == null) {
instance = new AuthenticationService();
}
return instance;
}
public Customer authenticate(String email, String password) {
Customer customer = customerDB.get(email);
if (customer != null && customer.getPassword().equals(password)) {
return customer;
}
return null;
}
}
// --------------------- PAYMENT PROCESSING (STRATEGY PATTERN) ---------------------
/*
* Strategy Pattern: PaymentStrategy defines how to process a payment.
*/
interface PaymentStrategy {
Payment processPayment(double amount);
}
// Concrete strategy for Credit Card payments.
class CreditCardPaymentStrategy implements PaymentStrategy {
@Override
public Payment processPayment(double amount) {
Payment payment = new Payment(UUID.randomUUID().toString(), amount, "CreditCard");
payment.markAsSuccessful();
System.out.println("Processed Credit Card payment for $" + amount);
return payment;
}
}
// Concrete strategy for PayPal payments.
class PayPalPaymentStrategy implements PaymentStrategy {
@Override
public Payment processPayment(double amount) {
Payment payment = new Payment(UUID.randomUUID().toString(), amount, "PayPal");
payment.markAsSuccessful();
System.out.println("Processed PayPal payment for $" + amount);
return payment;
}
}
/*
* Singleton Pattern: PaymentService uses a PaymentStrategy.
*/
class PaymentService {
private static PaymentService instance;
private PaymentStrategy paymentStrategy;
private PaymentService() {
paymentStrategy = new CreditCardPaymentStrategy();
}
public static synchronized PaymentService getInstance() {
if (instance == null) {
instance = new PaymentService();
}
return instance;
}
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public Payment processPayment(double amount) {
return paymentStrategy.processPayment(amount);
}
}
// --------------------- DELIVERY ASSIGNMENT (STRATEGY PATTERN) ---------------------
/*
* Strategy Pattern: DeliveryAssignmentStrategy defines how to assign orders to delivery agents.
*/
interface DeliveryAssignmentStrategy {
DeliveryAgent assignDeliveryAgent(Order order, List<DeliveryAgent> agents);
}
// Default strategy: assign the first available delivery agent.
class FirstAvailableDeliveryAssignmentStrategy implements DeliveryAssignmentStrategy {
@Override
public DeliveryAgent assignDeliveryAgent(Order order, List<DeliveryAgent> agents) {
for (DeliveryAgent agent : agents) {
if (agent.isAvailable()) {
// Mark agent as busy.
agent.setAvailable(false);
return agent;
}
}
return null;
}
}
// --------------------- DELIVERY AGENT MANAGEMENT ---------------------
/*
* Singleton Pattern: DeliveryAgentManager manages available delivery agents.
*/
class DeliveryAgentManager {
private static DeliveryAgentManager instance;
private List<DeliveryAgent> agents;
private DeliveryAgentManager() {
agents = new ArrayList<>();
// Pre-load dummy delivery agents.
agents.add(new DeliveryAgent("D001", "Dave", "LocationA"));
agents.add(new DeliveryAgent("D002", "Eva", "LocationB"));
}
public static synchronized DeliveryAgentManager getInstance() {
if (instance == null) {
instance = new DeliveryAgentManager();
}
return instance;
}
public List<DeliveryAgent> getAgents() {
return agents;
}
// For simplicity, agents remain static in this demo.
}
// --------------------- OBSERVER PATTERN FOR ORDER TRACKING ---------------------
/*
* Observer Pattern: OrderObserver defines a method for receiving order updates.
*/
interface OrderObserver {
void update(Order order, String message);
}
// Concrete observer that simulates sending mobile notifications.
class MobileOrderObserver implements OrderObserver {
@Override
public void update(Order order, String message) {
System.out.println("Mobile Notification to " + order.getCustomer().getEmail() +
": Order " + order.getOrderId() + " - " + message);
}
}
// --------------------- ORDER MANAGEMENT ---------------------
/*
* Singleton & Factory Pattern: OrderManager handles order creation, processing,
* and notifies observers about order events (Observer Pattern).
*/
class OrderManager {
private static OrderManager instance;
private List<OrderObserver> observers;
private DeliveryAssignmentStrategy assignmentStrategy;
private OrderManager() {
observers = new ArrayList<>();
assignmentStrategy = new FirstAvailableDeliveryAssignmentStrategy();
}
public static synchronized OrderManager getInstance() {
if (instance == null) {
instance = new OrderManager();
}
return instance;
}
public void registerObserver(OrderObserver observer) {
observers.add(observer);
}
private void notifyObservers(Order order, String message) {
for (OrderObserver observer : observers) {
observer.update(order, message);
}
}
// Create an order for a customer from a restaurant with items to order.
public Order createOrder(Customer customer, Restaurant restaurant, Map<String, Integer> itemsToOrder) {
Order order = new Order(UUID.randomUUID().toString(), customer, restaurant);
Menu menu = restaurant.getMenu();
// Process each item in the order.
for (Map.Entry<String, Integer> entry : itemsToOrder.entrySet()) {
String itemId = entry.getKey();
int quantity = entry.getValue();
MenuItem menuItem = menu.getItemById(itemId);
if (menuItem != null) {
OrderItem orderItem = new OrderItem(menuItem, quantity);
order.addItem(orderItem);
} else {
System.out.println("Menu item with id " + itemId + " not found in restaurant " + restaurant.getName());
}
}
customer.addOrder(order);
notifyObservers(order, "Order placed successfully.");
return order;
}
// Assign a delivery agent to an order using the assignment strategy.
public void assignOrder(Order order) {
DeliveryAgentManager agentManager = DeliveryAgentManager.getInstance();
DeliveryAgent agent = assignmentStrategy.assignDeliveryAgent(order, agentManager.getAgents());
if (agent != null) {
order.assignAgent(agent);
notifyObservers(order, "Order assigned to delivery agent " + agent.getName());
} else {
System.out.println("No available delivery agents at the moment.");
}
}
// Mark an order as delivered.
public void markOrderDelivered(Order order) {
order.markAsDelivered();
// Release the delivery agent.
if (order.getAssignedAgent() != null) {
order.getAssignedAgent().setAvailable(true);
}
notifyObservers(order, "Order delivered successfully.");
}
}
// --------------------- MAIN APPLICATION ---------------------
public class FoodDeliveryApp {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// --------------------- User Authentication ---------------------
AuthenticationService authService = AuthenticationService.getInstance();
System.out.println("Welcome to FoodExpress!");
System.out.println("Please log in with your email:");
String email = scanner.nextLine();
System.out.println("Enter your password:");
String password = scanner.nextLine();
Customer customer = authService.authenticate(email, password);
if (customer == null) {
System.out.println("Authentication failed. Exiting...");
scanner.close();
return;
}
System.out.println("Welcome, " + customer.getName() + "!");
// --------------------- Restaurant Catalog Display ---------------------
RestaurantCatalog restaurantCatalog = RestaurantCatalog.getInstance();
System.out.println("\nAvailable Restaurants:");
for (Restaurant restaurant : restaurantCatalog.getAllRestaurants()) {
System.out.println(restaurant.getRestaurantId() + " - " + restaurant.getName() + " | Address: " + restaurant.getAddress());
}
// For demo, let the user select a restaurant.
System.out.println("\nEnter the Restaurant ID to view menu:");
String restId = scanner.nextLine();
Restaurant selectedRestaurant = restaurantCatalog.getRestaurantById(restId);
if (selectedRestaurant == null) {
System.out.println("Invalid Restaurant ID. Exiting...");
scanner.close();
return;
}
// Display the restaurant's menu.
System.out.println("\nMenu for " + selectedRestaurant.getName() + ":");
for (MenuItem item : selectedRestaurant.getMenu().getItems()) {
System.out.println(item.getItemId() + " - " + item.getName() + " | Price: $" + item.getPrice());
}
// --------------------- Order Processing ---------------------
OrderManager orderManager = OrderManager.getInstance();
// Register an observer for order notifications.
orderManager.registerObserver(new MobileOrderObserver());
Map<String, Integer> itemsToOrder = new HashMap<>();
System.out.println("\nEnter Menu Item IDs and quantities to order (e.g., M001 2). Type 'done' when finished:");
while (true) {
String input = scanner.nextLine();
if (input.equalsIgnoreCase("done")) break;
String[] parts = input.split(" ");
if (parts.length != 2) {
System.out.println("Invalid input. Please enter MenuItem ID and quantity separated by a space.");
continue;
}
String itemId = parts[0];
int qty;
try {
qty = Integer.parseInt(parts[1]);
} catch (NumberFormatException e) {
System.out.println("Invalid quantity. Please try again.");
continue;
}
itemsToOrder.put(itemId, qty);
}
Order order = orderManager.createOrder(customer, selectedRestaurant, itemsToOrder);
if (order.getItems().isEmpty()) {
System.out.println("No valid items in order. Exiting.");
scanner.close();
return;
}
System.out.println("Order placed successfully! Order ID: " + order.getOrderId());
System.out.println("Order Total: $" + order.getTotalAmount());
// --------------------- Payment Processing ---------------------
PaymentService paymentService = PaymentService.getInstance();
System.out.println("\nProcessing payment...");
Payment payment = paymentService.processPayment(order.getTotalAmount());
if (payment.isSuccessful()) {
System.out.println("Payment successful! Payment ID: " + payment.getPaymentId());
} else {
System.out.println("Payment failed!");
}
// --------------------- Order Assignment & Delivery ---------------------
orderManager.assignOrder(order);
// For demo purposes, simulate delivery after assignment.
System.out.println("\nPress Enter to mark the order as delivered.");
scanner.nextLine();
orderManager.markOrderDelivered(order);
// --------------------- Order Summary ---------------------
System.out.println("\nOrder Summary:");
for (OrderItem item : order.getItems()) {
System.out.println(item.getMenuItem().getName() + " x " + item.getQuantity() +
" | Unit Price: $" + item.getUnitPrice() +
" | Total: $" + item.getTotalPrice());
}
scanner.close();
}
}
3. Explanation of Key Points & Design Patterns
Modeling Entities
Restaurants, Menus, and Orders:
Restaurant holds its Menu (composed of MenuItem objects).
Order is linked to both a Customer and a Restaurant and contains multiple OrderItem entries.
Delivery & Payment:
DeliveryAgent represents a delivery person.
Payment records transaction details.
Order Management
OrderManager (Singleton & Factory Pattern):
Centralizes order creation and processing while updating inventory and order status.
DeliveryAssignmentStrategy (Strategy Pattern):
Enables different algorithms for order assignment (default strategy assigns the first available delivery agent). This mechanism can later be extended to incorporate routing and proximity calculations.
Observer Pattern:
The OrderObserver interface and its implementation (MobileOrderObserver) are used to send real‑time notifications (e.g., via mobile) on order status changes.
PaymentService & PaymentStrategy (Strategy Pattern):
Process payments with interchangeable strategies (e.g., Credit Card, PayPal).
Real-Time Order Tracking (Observer Pattern):
Observers are notified of order placement, assignment, and delivery updates, ensuring a seamless user experience.
4. Conclusion
This detailed LLD for a Food Delivery System demonstrates:
How major entities (restaurants, menus, orders, customers, delivery agents) and their relationships are modeled.
How order management is handled using an order manager with a pluggable delivery assignment strategy to ensure timely delivery.
How user interaction is streamlined through secure authentication, flexible payment processing (Strategy Pattern), and real‑time order tracking using the Observer Pattern.
By employing multiple design patterns (Singleton, Factory, Strategy, and Observer), the design is modular, extensible, and robust—providing a solid blueprint suitable for technical interviews and real‑world implementations.