0) Problem Restatement
A common machine coding round (Flipkart and Cleartrip): build a simplified Swiggy/Zomato in about 90 minutes, as working code with clean object-oriented design. Typical requirements:
- Onboard restaurants with a menu (item, price) and a max number of orders they can process at once (capacity).
- Update menus (add items, change prices).
- A user places an order with several items. The system selects a restaurant that has all the items and free capacity, using a strategy such as lowest total price or highest rating.
- Restaurants mark orders as completed, which frees capacity.
- Follow-up round: concurrency. Two orders racing for the last capacity slot, and thread-safe status updates.
1) Entities and Patterns
Architecture Diagram
classDiagram
class Restaurant {
+String id
+String name
+double rating
+int capacity
+int activeOrders
+Map menu
+tryReserve() bool
+release() void
}
class Order {
+String id
+String userId
+Map items
+Restaurant restaurant
+OrderStatus status
+int totalCents
}
class SelectionStrategy {
<<interface>>
+select(candidates, items) Restaurant
}
class LowestPriceStrategy
class HighestRatingStrategy
class OrderService {
+placeOrder(userId, items, strategy) Order
+completeOrder(orderId) void
}
class OrderObserver {
<<interface>>
+onStatusChange(order) void
}
SelectionStrategy <|.. LowestPriceStrategy
SelectionStrategy <|.. HighestRatingStrategy
OrderService --> SelectionStrategy
OrderService --> Restaurant
OrderService --> Order
OrderService --> OrderObserver- Strategy pattern for restaurant selection: new rules (fastest, nearest) are new classes, with no edits to
OrderService. - State of an order as an enum with allowed transitions:
PLACED → ACCEPTED → PREPARING → OUT_FOR_DELIVERY → DELIVERED, plusCANCELLED. Invalid moves throw. - Observer for notifications: the user, the restaurant dashboard and the logger subscribe to status changes.
2) Code (Python)
import threading, uuid
from enum import Enum
class Status(Enum):
PLACED = 1; ACCEPTED = 2; PREPARING = 3; OUT_FOR_DELIVERY = 4; DELIVERED = 5; CANCELLED = 6
ALLOWED = {
Status.PLACED: {Status.ACCEPTED, Status.CANCELLED},
Status.ACCEPTED: {Status.PREPARING, Status.CANCELLED},
Status.PREPARING: {Status.OUT_FOR_DELIVERY},
Status.OUT_FOR_DELIVERY: {Status.DELIVERED},
}
class Restaurant:
def __init__(self, name, capacity, rating, menu):
self.id, self.name, self.capacity, self.rating = str(uuid.uuid4()), name, capacity, rating
self.menu = dict(menu) # item -> price in cents
self.active = 0
self.lock = threading.Lock()
def has_all(self, items):
return all(i in self.menu for i in items)
def price(self, items):
return sum(self.menu[i] * q for i, q in items.items())
def try_reserve(self):
with self.lock: # atomic check-and-increment
if self.active < self.capacity:
self.active += 1
return True
return False
def release(self):
with self.lock:
self.active -= 1
class LowestPrice:
def rank(self, candidates, items):
return sorted(candidates, key=lambda r: (r.price(items), r.name))
class HighestRating:
def rank(self, candidates, items):
return sorted(candidates, key=lambda r: (-r.rating, r.name))
class Order:
def __init__(self, user, items, restaurant):
self.id, self.user, self.items, self.restaurant = str(uuid.uuid4()), user, items, restaurant
self.total = restaurant.price(items)
self.status = Status.PLACED
self.lock = threading.Lock()
class OrderService:
def __init__(self):
self.restaurants, self.orders, self.observers = [], {}, []
def add_restaurant(self, r): self.restaurants.append(r)
def place_order(self, user, items, strategy):
candidates = [r for r in self.restaurants if r.has_all(items)]
for r in strategy.rank(candidates, items): # best first, fall back if full
if r.try_reserve():
order = Order(user, items, r)
self.orders[order.id] = order
self._notify(order)
return order
raise RuntimeError("No restaurant can take this order right now")
def update_status(self, order_id, new_status):
order = self.orders[order_id]
with order.lock:
if new_status not in ALLOWED.get(order.status, set()):
raise ValueError(f"{order.status.name} -> {new_status.name} not allowed")
order.status = new_status
if new_status in (Status.DELIVERED, Status.CANCELLED):
order.restaurant.release() # free capacity
self._notify(order)
def _notify(self, order):
for obs in self.observers: obs(order)
3) Concurrency Follow-up Explained
- Race on the last slot: two threads see
active = capacity - 1and both increment → over capacity.try_reserve()does check and increment under one lock, so only one wins. The loser falls back to the next-best restaurant. - Status updates: a per-order lock plus the allowed-transitions table stops two threads from applying conflicting changes (e.g., cancel and accept at once).
- Release exactly once: capacity is released only on the transition into DELIVERED or CANCELLED, which can happen once because of the state rules.
- No lock-ordering deadlocks: we never hold two restaurant locks at the same time.
4) SOLID and Extensibility (what reviewers ask)
- Single responsibility:
Restaurantmanages its own menu and capacity,OrderServiceorchestrates, and strategies only rank. - Open/closed: add a
FastestDeliveryStrategywithout touching existing code. - Dependency inversion:
OrderServicedepends on the strategy interface, not concrete classes. - Extensions: menu updates (lock per restaurant), ratings updates, splitting one order across restaurants, persistence via a repository interface, and idempotent
place_orderwith a client request ID.
5) Mapping to a Real Service
In production: restaurants and orders live in a DB, capacity is reserved with an atomic conditional update (UPDATE ... SET active = active + 1 WHERE active < capacity), status changes are events, and observers become consumers (notifications, analytics).
6) Wrap-Up
Model restaurants (menu, capacity with an atomic reserve and release), orders (items, total, status with an explicit transition table) and an order service that ranks candidate restaurants through a pluggable strategy and falls back when one is full. Handle concurrency with check-and-increment under a lock and per-order locks for status, notify through observers, and keep the design SOLID so new strategies and features plug in cleanly.