"Design a food delivery system like DoorDash or Swiggy. Customers order from a restaurant's menu, the order moves through preparation and delivery, and a delivery partner is assigned to carry it. Customers should see live status updates."
Orders and their line-item snapshots are durable, legally significant records — a receipt that changes after the fact or disappears on restart is unacceptable. Delivery-partner availability state is more transient and could reasonably live in a fast in-memory/cache layer, refreshed frequently, while the order and its price snapshot remain firmly repository-backed.
A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.
import java.util.*;
/**
* Problem 18: Restaurant / Food Delivery System (single-threaded -- no concurrency badge)
*
* Key design decisions faithfully reproduced from the class diagram:
* - MenuItem (live, editable catalog) vs OrderLineItem (frozen price/name snapshot at checkout).
* - OrderState hierarchy: PLACED -> CONFIRMED -> PREPARING -> OUT_FOR_DELIVERY -> DELIVERED, each
* state only accepting its own legal next event; illegal/duplicated events are rejected by the state.
* - DeliveryAssignmentStrategy is injected (Strategy); DeliveryDispatcher does the atomic select-then-claim.
* - OrderStatusNotifier is a thin Observer subject fanning out every legal transition.
* - OrderService is the facade: placeOrder() / updateStatus().
*/
public class p18 {
enum OrderStatus { PLACED, CONFIRMED, PREPARING, OUT_FOR_DELIVERY, DELIVERED, CANCELLED }
enum StatusEvent { CONFIRM, PREPARE, PICKUP, DELIVER, CANCEL }
enum PartnerStatus { AVAILABLE, BUSY }
static final class Customer {
final String id, name, address;
Customer(String id, String name, String address) { this.id = id; this.name = name; this.address = address; }
}
static final class Restaurant {
final String id, name, location;
final List<MenuItem> menu = new ArrayList<>();
Restaurant(String id, String name, String location) { this.id = id; this.name = name; this.location = location; }
void addMenuItem(MenuItem item) { menu.add(item); }
}
// ---------- MenuItem (live catalog) vs OrderLineItem (frozen snapshot) ----------
static final class MenuItem {
final String id;
String name;
double price;
boolean available;
MenuItem(String id, String name, double price) { this.id = id; this.name = name; this.price = price; this.available = true; }
}
static final class OrderLineItem {
final String nameAtOrderTime;
final double priceAtOrderTime;
final int quantity;
OrderLineItem(MenuItem item, int quantity) {
// Freeze name/price at checkout -- a later price change must never rewrite a past receipt.
this.nameAtOrderTime = item.name;
this.priceAtOrderTime = item.price;
this.quantity = quantity;
}
double subtotal() { return priceAtOrderTime * quantity; }
}
static final class CartItem {
final MenuItem menuItem;
final int quantity;
CartItem(MenuItem menuItem, int quantity) { this.menuItem = menuItem; this.quantity = quantity; }
}
static final class Cart {
final List<CartItem> items = new ArrayList<>();
void addItem(MenuItem item, int quantity) { items.add(new CartItem(item, quantity)); }
}
// ---------- OrderState (State pattern) ----------
interface OrderState {
OrderState onConfirm();
OrderState onPrepared();
OrderState onPickedUp();
OrderState onDelivered();
OrderStatus status();
}
static final class PlacedState implements OrderState {
static final PlacedState INSTANCE = new PlacedState();
public OrderState onConfirm() { return ConfirmedState.INSTANCE; }
public OrderState onPrepared() { throw new IllegalStateException("Cannot prepare before confirmation"); }
public OrderState onPickedUp() { throw new IllegalStateException("Cannot pick up before confirmation"); }
public OrderState onDelivered() { throw new IllegalStateException("Cannot deliver before confirmation"); }
public OrderStatus status() { return OrderStatus.PLACED; }
}
static final class ConfirmedState implements OrderState {
static final ConfirmedState INSTANCE = new ConfirmedState();
public OrderState onConfirm() { throw new IllegalStateException("Already confirmed"); }
public OrderState onPrepared() { return PreparingState.INSTANCE; }
public OrderState onPickedUp() { throw new IllegalStateException("Cannot pick up before preparation"); }
public OrderState onDelivered() { throw new IllegalStateException("Cannot deliver before preparation"); }
public OrderStatus status() { return OrderStatus.CONFIRMED; }
}
static final class PreparingState implements OrderState {
static final PreparingState INSTANCE = new PreparingState();
public OrderState onConfirm() { throw new IllegalStateException("Already confirmed"); }
public OrderState onPrepared() { throw new IllegalStateException("Already preparing"); }
public OrderState onPickedUp() { return OutForDeliveryState.INSTANCE; }
public OrderState onDelivered() { throw new IllegalStateException("Cannot deliver before pickup"); }
public OrderStatus status() { return OrderStatus.PREPARING; }
}
static final class OutForDeliveryState implements OrderState {
static final OutForDeliveryState INSTANCE = new OutForDeliveryState();
public OrderState onConfirm() { throw new IllegalStateException("Already confirmed"); }
public OrderState onPrepared() { throw new IllegalStateException("Already prepared"); }
public OrderState onPickedUp() { throw new IllegalStateException("Already picked up"); }
public OrderState onDelivered() { return DeliveredState.INSTANCE; }
public OrderStatus status() { return OrderStatus.OUT_FOR_DELIVERY; }
}
static final class DeliveredState implements OrderState {
static final DeliveredState INSTANCE = new DeliveredState();
public OrderState onConfirm() { throw new IllegalStateException("Order already delivered -- terminal state"); }
public OrderState onPrepared() { throw new IllegalStateException("Order already delivered -- terminal state"); }
public OrderState onPickedUp() { throw new IllegalStateException("Order already delivered -- terminal state"); }
public OrderState onDelivered() { throw new IllegalStateException("Order already delivered -- terminal state"); }
public OrderStatus status() { return OrderStatus.DELIVERED; }
}
// ---------- Order ----------
static final class Order {
final String id;
final Customer customer;
final Restaurant restaurant;
final List<OrderLineItem> lineItems;
private OrderState state = PlacedState.INSTANCE;
private final OrderStatusNotifier notifier;
DeliveryPartner assignedPartner;
Order(String id, Customer customer, Restaurant restaurant, List<OrderLineItem> lineItems, OrderStatusNotifier notifier) {
this.id = id; this.customer = customer; this.restaurant = restaurant; this.lineItems = lineItems; this.notifier = notifier;
}
OrderStatus getStatus() { return state.status(); }
double totalPrice() {
double total = 0;
for (OrderLineItem li : lineItems) total += li.subtotal();
return total;
}
/** Delegates transition legality to the current state; illegal events are rejected by the state itself. */
void transitionTo(StatusEvent event) {
OrderState next;
switch (event) {
case CONFIRM: next = state.onConfirm(); break;
case PREPARE: next = state.onPrepared(); break;
case PICKUP: next = state.onPickedUp(); break;
case DELIVER: next = state.onDelivered(); break;
default: throw new IllegalArgumentException("Unsupported event via transitionTo: " + event);
}
state = next;
notifier.notifyStatusChanged(this);
}
void cancel() {
if (state == DeliveredState.INSTANCE) throw new IllegalStateException("Cannot cancel a delivered order");
state = new OrderState() {
public OrderState onConfirm() { throw new IllegalStateException("Order is cancelled"); }
public OrderState onPrepared() { throw new IllegalStateException("Order is cancelled"); }
public OrderState onPickedUp() { throw new IllegalStateException("Order is cancelled"); }
public OrderState onDelivered() { throw new IllegalStateException("Order is cancelled"); }
public OrderStatus status() { return OrderStatus.CANCELLED; }
};
notifier.notifyStatusChanged(this);
}
}
// ---------- Delivery assignment ----------
static final class DeliveryPartner {
final String id, name;
PartnerStatus status = PartnerStatus.AVAILABLE;
String location;
DeliveryPartner(String id, String name, String location) { this.id = id; this.name = name; this.location = location; }
/** Claims this partner for a delivery. Caller (DeliveryDispatcher) must already hold the pool lock. */
void claim() {
if (status != PartnerStatus.AVAILABLE) throw new IllegalStateException("Partner " + id + " is not available");
status = PartnerStatus.BUSY;
}
void release() { status = PartnerStatus.AVAILABLE; }
}
interface DeliveryAssignmentStrategy {
DeliveryPartner selectPartner(Order order, List<DeliveryPartner> candidates);
}
/** Picks the first available partner whose recorded location matches the restaurant's -- a simple "nearest" proxy. */
static final class NearestAvailablePartnerStrategy implements DeliveryAssignmentStrategy {
public DeliveryPartner selectPartner(Order order, List<DeliveryPartner> candidates) {
for (DeliveryPartner p : candidates) {
if (p.location.equals(order.restaurant.location)) return p;
}
if (candidates.isEmpty()) throw new NoSuchElementException("No available delivery partners");
return candidates.get(0); // fall back to any available partner
}
}
/** Atomically selects and claims a partner -- selection and claim happen under the same lock. */
static final class DeliveryDispatcher {
private final List<DeliveryPartner> partnerPool;
private final DeliveryAssignmentStrategy strategy;
DeliveryDispatcher(List<DeliveryPartner> partnerPool, DeliveryAssignmentStrategy strategy) {
this.partnerPool = partnerPool;
this.strategy = strategy;
}
synchronized DeliveryPartner assign(Order order) {
List<DeliveryPartner> candidates = new ArrayList<>();
for (DeliveryPartner p : partnerPool) if (p.status == PartnerStatus.AVAILABLE) candidates.add(p);
DeliveryPartner chosen = strategy.selectPartner(order, candidates);
chosen.claim(); // claim happens inside the same lock as selection -- no double-assignment race
order.assignedPartner = chosen;
return chosen;
}
}
// ---------- OrderStatusNotifier (Observer) ----------
interface OrderStatusObserver {
void onStatusChanged(Order order);
}
static final class OrderStatusNotifier {
private final List<OrderStatusObserver> observers = new ArrayList<>();
void subscribe(OrderStatusObserver observer) { observers.add(observer); }
void notifyStatusChanged(Order order) { for (OrderStatusObserver o : observers) o.onStatusChanged(order); }
}
static final class ConsoleStatusObserver implements OrderStatusObserver {
public void onStatusChanged(Order order) {
System.out.printf("[push] Order %s status -> %s%n", order.id, order.getStatus());
}
}
// ---------- OrderService (facade) ----------
static final class OrderService {
private final DeliveryDispatcher dispatcher;
private final OrderStatusNotifier notifier;
private final Map<String, Order> orders = new HashMap<>();
private int orderSeq = 0;
OrderService(DeliveryDispatcher dispatcher, OrderStatusNotifier notifier) {
this.dispatcher = dispatcher;
this.notifier = notifier;
}
Order placeOrder(Customer customer, Restaurant restaurant, Cart cart) {
List<OrderLineItem> lineItems = new ArrayList<>();
for (CartItem ci : cart.items) lineItems.add(new OrderLineItem(ci.menuItem, ci.quantity)); // freeze price/name now
String id = "ORD-" + (++orderSeq);
Order order = new Order(id, customer, restaurant, lineItems, notifier);
orders.put(id, order);
notifier.notifyStatusChanged(order); // announce the initial PLACED state too
return order;
}
void updateStatus(String orderId, StatusEvent event) {
Order order = orders.get(orderId);
if (order == null) throw new NoSuchElementException("Unknown order " + orderId);
if (event == StatusEvent.CANCEL) { order.cancel(); return; }
if (event == StatusEvent.PICKUP) dispatcher.assign(order); // assign a courier right before pickup
order.transitionTo(event);
}
}
// ---------- Demo ----------
public static void main(String[] args) {
Restaurant restaurant = new Restaurant("R1", "Curry House", "Downtown");
MenuItem biryani = new MenuItem("M1", "Chicken Biryani", 12.50);
MenuItem naan = new MenuItem("M2", "Garlic Naan", 3.00);
restaurant.addMenuItem(biryani);
restaurant.addMenuItem(naan);
OrderStatusNotifier notifier = new OrderStatusNotifier();
notifier.subscribe(new ConsoleStatusObserver());
List<DeliveryPartner> partners = List.of(
new DeliveryPartner("P1", "Sam", "Downtown"),
new DeliveryPartner("P2", "Robin", "Uptown"));
DeliveryDispatcher dispatcher = new DeliveryDispatcher(partners, new NearestAvailablePartnerStrategy());
OrderService orderService = new OrderService(dispatcher, notifier);
Customer customer = new Customer("C1", "Alice", "12 Elm St");
Cart cart = new Cart();
cart.addItem(biryani, 2);
cart.addItem(naan, 1);
Order order = orderService.placeOrder(customer, restaurant, cart);
System.out.printf("Order placed: %s, total=%.2f%n", order.id, order.totalPrice());
// Live menu price changes after checkout must NOT affect this already-placed order's receipt.
biryani.price = 15.00;
System.out.printf("Menu price changed to %.2f, but order total is still %.2f%n", biryani.price, order.totalPrice());
orderService.updateStatus(order.id, StatusEvent.CONFIRM);
orderService.updateStatus(order.id, StatusEvent.PREPARE);
orderService.updateStatus(order.id, StatusEvent.PICKUP); // triggers atomic partner assignment
System.out.println("Assigned partner: " + order.assignedPartner.name);
orderService.updateStatus(order.id, StatusEvent.DELIVER);
// Illegal transition: a duplicated/out-of-order DELIVER event on an already-delivered order must be rejected.
try {
orderService.updateStatus(order.id, StatusEvent.DELIVER);
} catch (IllegalStateException e) {
System.out.println("Rejected duplicate DELIVER event: " + e.getMessage());
}
// A second order competing for the same restaurant-local partner must not double-assign that partner.
Order order2 = orderService.placeOrder(customer, restaurant, cart);
orderService.updateStatus(order2.id, StatusEvent.CONFIRM);
orderService.updateStatus(order2.id, StatusEvent.PREPARE);
// P1 was claimed (BUSY) for order1 and is never auto-released on delivery in this simplified model,
// so the dispatcher correctly falls back to the only remaining available partner, P2.
orderService.updateStatus(order2.id, StatusEvent.PICKUP);
System.out.println("Order2 assigned partner: " + order2.assignedPartner.name);
// Direct PLACED -> DELIVERED must never be allowed.
Order order3 = orderService.placeOrder(customer, restaurant, cart);
try {
orderService.updateStatus(order3.id, StatusEvent.DELIVER);
} catch (IllegalStateException e) {
System.out.println("Rejected out-of-order DELIVER on a freshly placed order: " + e.getMessage());
}
}
}
// Problem 18: Restaurant / Food Delivery System (single-threaded -- no concurrency badge)
//
// MenuItem (live, editable catalog) vs OrderLineItem (frozen price/name snapshot at checkout).
// OrderState hierarchy: PLACED -> CONFIRMED -> PREPARING -> OUT_FOR_DELIVERY -> DELIVERED, each
// state only accepting its own legal next event; illegal/duplicated events are rejected by the state.
// DeliveryAssignmentStrategy is injected (Strategy); DeliveryDispatcher does the atomic select-then-claim.
// OrderStatusNotifier is a thin Observer subject fanning out every legal transition.
// OrderService is the facade: placeOrder() / updateStatus().
#include <iostream>
#include <vector>
#include <memory>
#include <string>
#include <map>
#include <stdexcept>
enum class OrderStatus { PLACED, CONFIRMED, PREPARING, OUT_FOR_DELIVERY, DELIVERED, CANCELLED };
enum class StatusEvent { CONFIRM, PREPARE, PICKUP, DELIVER, CANCEL };
enum class PartnerStatus { AVAILABLE, BUSY };
static const char* statusName(OrderStatus s) {
switch (s) {
case OrderStatus::PLACED: return "PLACED";
case OrderStatus::CONFIRMED: return "CONFIRMED";
case OrderStatus::PREPARING: return "PREPARING";
case OrderStatus::OUT_FOR_DELIVERY: return "OUT_FOR_DELIVERY";
case OrderStatus::DELIVERED: return "DELIVERED";
case OrderStatus::CANCELLED: return "CANCELLED";
}
return "?";
}
struct Customer { std::string id, name, address; };
struct MenuItem {
std::string id, name;
double price;
bool available;
MenuItem(std::string id_, std::string name_, double price_) : id(std::move(id_)), name(std::move(name_)), price(price_), available(true) {}
};
struct Restaurant {
std::string id, name, location;
std::vector<std::shared_ptr<MenuItem>> menu;
void addMenuItem(std::shared_ptr<MenuItem> item) { menu.push_back(std::move(item)); }
};
// Freezes name/price at checkout -- a later price change must never rewrite a past receipt.
struct OrderLineItem {
std::string nameAtOrderTime;
double priceAtOrderTime;
int quantity;
OrderLineItem(const MenuItem& item, int qty) : nameAtOrderTime(item.name), priceAtOrderTime(item.price), quantity(qty) {}
double subtotal() const { return priceAtOrderTime * quantity; }
};
struct CartItem { std::shared_ptr<MenuItem> menuItem; int quantity; };
struct Cart {
std::vector<CartItem> items;
void addItem(std::shared_ptr<MenuItem> item, int quantity) { items.push_back({std::move(item), quantity}); }
};
// ---------- OrderState (State pattern) ----------
class Order;
class OrderState {
public:
virtual ~OrderState() = default;
virtual std::shared_ptr<OrderState> onConfirm() = 0;
virtual std::shared_ptr<OrderState> onPrepared() = 0;
virtual std::shared_ptr<OrderState> onPickedUp() = 0;
virtual std::shared_ptr<OrderState> onDelivered() = 0;
virtual OrderStatus status() const = 0;
};
class ConfirmedState;
class PreparingState;
class OutForDeliveryState;
class DeliveredState;
class PlacedState : public OrderState {
public:
std::shared_ptr<OrderState> onConfirm() override;
std::shared_ptr<OrderState> onPrepared() override { throw std::logic_error("Cannot prepare before confirmation"); }
std::shared_ptr<OrderState> onPickedUp() override { throw std::logic_error("Cannot pick up before confirmation"); }
std::shared_ptr<OrderState> onDelivered() override { throw std::logic_error("Cannot deliver before confirmation"); }
OrderStatus status() const override { return OrderStatus::PLACED; }
};
class ConfirmedState : public OrderState {
public:
std::shared_ptr<OrderState> onConfirm() override { throw std::logic_error("Already confirmed"); }
std::shared_ptr<OrderState> onPrepared() override;
std::shared_ptr<OrderState> onPickedUp() override { throw std::logic_error("Cannot pick up before preparation"); }
std::shared_ptr<OrderState> onDelivered() override { throw std::logic_error("Cannot deliver before preparation"); }
OrderStatus status() const override { return OrderStatus::CONFIRMED; }
};
class PreparingState : public OrderState {
public:
std::shared_ptr<OrderState> onConfirm() override { throw std::logic_error("Already confirmed"); }
std::shared_ptr<OrderState> onPrepared() override { throw std::logic_error("Already preparing"); }
std::shared_ptr<OrderState> onPickedUp() override;
std::shared_ptr<OrderState> onDelivered() override { throw std::logic_error("Cannot deliver before pickup"); }
OrderStatus status() const override { return OrderStatus::PREPARING; }
};
class OutForDeliveryState : public OrderState {
public:
std::shared_ptr<OrderState> onConfirm() override { throw std::logic_error("Already confirmed"); }
std::shared_ptr<OrderState> onPrepared() override { throw std::logic_error("Already prepared"); }
std::shared_ptr<OrderState> onPickedUp() override { throw std::logic_error("Already picked up"); }
std::shared_ptr<OrderState> onDelivered() override;
OrderStatus status() const override { return OrderStatus::OUT_FOR_DELIVERY; }
};
class DeliveredState : public OrderState {
public:
std::shared_ptr<OrderState> onConfirm() override { throw std::logic_error("Order already delivered -- terminal state"); }
std::shared_ptr<OrderState> onPrepared() override { throw std::logic_error("Order already delivered -- terminal state"); }
std::shared_ptr<OrderState> onPickedUp() override { throw std::logic_error("Order already delivered -- terminal state"); }
std::shared_ptr<OrderState> onDelivered() override { throw std::logic_error("Order already delivered -- terminal state"); }
OrderStatus status() const override { return OrderStatus::DELIVERED; }
};
class CancelledState : public OrderState {
public:
std::shared_ptr<OrderState> onConfirm() override { throw std::logic_error("Order is cancelled"); }
std::shared_ptr<OrderState> onPrepared() override { throw std::logic_error("Order is cancelled"); }
std::shared_ptr<OrderState> onPickedUp() override { throw std::logic_error("Order is cancelled"); }
std::shared_ptr<OrderState> onDelivered() override { throw std::logic_error("Order is cancelled"); }
OrderStatus status() const override { return OrderStatus::CANCELLED; }
};
std::shared_ptr<OrderState> PlacedState::onConfirm() { return std::make_shared<ConfirmedState>(); }
std::shared_ptr<OrderState> ConfirmedState::onPrepared() { return std::make_shared<PreparingState>(); }
std::shared_ptr<OrderState> PreparingState::onPickedUp() { return std::make_shared<OutForDeliveryState>(); }
std::shared_ptr<OrderState> OutForDeliveryState::onDelivered() { return std::make_shared<DeliveredState>(); }
// ---------- Delivery assignment ----------
struct DeliveryPartner {
std::string id, name, location;
PartnerStatus status = PartnerStatus::AVAILABLE;
// Claims this partner for a delivery. Caller (DeliveryDispatcher) is the sole atomic gate.
void claim() {
if (status != PartnerStatus::AVAILABLE) throw std::runtime_error("Partner " + id + " is not available");
status = PartnerStatus::BUSY;
}
void release() { status = PartnerStatus::AVAILABLE; }
};
class OrderStatusNotifier;
class Order {
public:
std::string id;
Customer customer;
Restaurant restaurant;
std::vector<OrderLineItem> lineItems;
DeliveryPartner* assignedPartner = nullptr;
Order(std::string id_, Customer customer_, Restaurant restaurant_, std::vector<OrderLineItem> lineItems_, OrderStatusNotifier& notifier_)
: id(std::move(id_)), customer(std::move(customer_)), restaurant(std::move(restaurant_)),
lineItems(std::move(lineItems_)), state_(std::make_shared<PlacedState>()), notifier_(notifier_) {}
OrderStatus getStatus() const { return state_->status(); }
double totalPrice() const {
double total = 0;
for (auto& li : lineItems) total += li.subtotal();
return total;
}
void transitionTo(StatusEvent event); // defined after OrderStatusNotifier below
void cancel();
private:
std::shared_ptr<OrderState> state_;
OrderStatusNotifier& notifier_;
};
class OrderStatusObserver {
public:
virtual ~OrderStatusObserver() = default;
virtual void onStatusChanged(Order& order) = 0;
};
class OrderStatusNotifier {
public:
void subscribe(std::shared_ptr<OrderStatusObserver> observer) { observers_.push_back(std::move(observer)); }
void notifyStatusChanged(Order& order) { for (auto& o : observers_) o->onStatusChanged(order); }
private:
std::vector<std::shared_ptr<OrderStatusObserver>> observers_;
};
void Order::transitionTo(StatusEvent event) {
std::shared_ptr<OrderState> next;
switch (event) {
case StatusEvent::CONFIRM: next = state_->onConfirm(); break;
case StatusEvent::PREPARE: next = state_->onPrepared(); break;
case StatusEvent::PICKUP: next = state_->onPickedUp(); break;
case StatusEvent::DELIVER: next = state_->onDelivered(); break;
default: throw std::invalid_argument("Unsupported event via transitionTo");
}
state_ = next;
notifier_.notifyStatusChanged(*this);
}
void Order::cancel() {
if (state_->status() == OrderStatus::DELIVERED) throw std::logic_error("Cannot cancel a delivered order");
state_ = std::make_shared<CancelledState>();
notifier_.notifyStatusChanged(*this);
}
class ConsoleStatusObserver : public OrderStatusObserver {
public:
void onStatusChanged(Order& order) override {
std::cout << "[push] Order " << order.id << " status -> " << statusName(order.getStatus()) << "\n";
}
};
class DeliveryAssignmentStrategy {
public:
virtual ~DeliveryAssignmentStrategy() = default;
virtual DeliveryPartner* selectPartner(Order& order, std::vector<DeliveryPartner*>& candidates) = 0;
};
// Picks the first available partner whose recorded location matches the restaurant's -- a simple "nearest" proxy.
class NearestAvailablePartnerStrategy : public DeliveryAssignmentStrategy {
public:
DeliveryPartner* selectPartner(Order& order, std::vector<DeliveryPartner*>& candidates) override {
for (auto* p : candidates) if (p->location == order.restaurant.location) return p;
if (candidates.empty()) throw std::runtime_error("No available delivery partners");
return candidates.front(); // fall back to any available partner
}
};
// Atomically selects and claims a partner -- selection and claim happen in the same call, no separate lock needed
// in this single-threaded model, but the shape mirrors the locked select-then-claim used in Problems 11/17.
class DeliveryDispatcher {
public:
DeliveryDispatcher(std::vector<std::shared_ptr<DeliveryPartner>> pool, std::shared_ptr<DeliveryAssignmentStrategy> strategy)
: partnerPool_(std::move(pool)), strategy_(std::move(strategy)) {}
DeliveryPartner* assign(Order& order) {
std::vector<DeliveryPartner*> candidates;
for (auto& p : partnerPool_) if (p->status == PartnerStatus::AVAILABLE) candidates.push_back(p.get());
DeliveryPartner* chosen = strategy_->selectPartner(order, candidates);
chosen->claim();
order.assignedPartner = chosen;
return chosen;
}
private:
std::vector<std::shared_ptr<DeliveryPartner>> partnerPool_;
std::shared_ptr<DeliveryAssignmentStrategy> strategy_;
};
// ---------- OrderService (facade) ----------
class OrderService {
public:
OrderService(std::shared_ptr<DeliveryDispatcher> dispatcher, std::shared_ptr<OrderStatusNotifier> notifier)
: dispatcher_(std::move(dispatcher)), notifier_(std::move(notifier)) {}
std::shared_ptr<Order> placeOrder(Customer customer, Restaurant restaurant, const Cart& cart) {
std::vector<OrderLineItem> lineItems;
for (auto& ci : cart.items) lineItems.emplace_back(*ci.menuItem, ci.quantity); // freeze price/name now
std::string id = "ORD-" + std::to_string(++orderSeq_);
auto order = std::make_shared<Order>(id, std::move(customer), std::move(restaurant), std::move(lineItems), *notifier_);
orders_[id] = order;
notifier_->notifyStatusChanged(*order); // announce the initial PLACED state too
return order;
}
void updateStatus(const std::string& orderId, StatusEvent event) {
auto it = orders_.find(orderId);
if (it == orders_.end()) throw std::runtime_error("Unknown order " + orderId);
auto& order = *it->second;
if (event == StatusEvent::CANCEL) { order.cancel(); return; }
if (event == StatusEvent::PICKUP) dispatcher_->assign(order); // assign a courier right before pickup
order.transitionTo(event);
}
private:
std::shared_ptr<DeliveryDispatcher> dispatcher_;
std::shared_ptr<OrderStatusNotifier> notifier_;
std::map<std::string, std::shared_ptr<Order>> orders_;
int orderSeq_ = 0;
};
int main() {
Restaurant restaurant;
restaurant.id = "R1"; restaurant.name = "Curry House"; restaurant.location = "Downtown";
auto biryani = std::make_shared<MenuItem>("M1", "Chicken Biryani", 12.50);
auto naan = std::make_shared<MenuItem>("M2", "Garlic Naan", 3.00);
restaurant.addMenuItem(biryani);
restaurant.addMenuItem(naan);
auto notifier = std::make_shared<OrderStatusNotifier>();
notifier->subscribe(std::make_shared<ConsoleStatusObserver>());
std::vector<std::shared_ptr<DeliveryPartner>> partners = {
std::make_shared<DeliveryPartner>(DeliveryPartner{"P1", "Sam", "Downtown"}),
std::make_shared<DeliveryPartner>(DeliveryPartner{"P2", "Robin", "Uptown"}),
};
auto dispatcher = std::make_shared<DeliveryDispatcher>(partners, std::make_shared<NearestAvailablePartnerStrategy>());
OrderService orderService(dispatcher, notifier);
Customer customer{"C1", "Alice", "12 Elm St"};
Cart cart;
cart.addItem(biryani, 2);
cart.addItem(naan, 1);
auto order = orderService.placeOrder(customer, restaurant, cart);
std::cout << "Order placed: " << order->id << ", total=" << order->totalPrice() << "\n";
// Live menu price changes after checkout must NOT affect this already-placed order's receipt.
biryani->price = 15.00;
std::cout << "Menu price changed to " << biryani->price << ", but order total is still " << order->totalPrice() << "\n";
orderService.updateStatus(order->id, StatusEvent::CONFIRM);
orderService.updateStatus(order->id, StatusEvent::PREPARE);
orderService.updateStatus(order->id, StatusEvent::PICKUP); // triggers atomic partner assignment
std::cout << "Assigned partner: " << order->assignedPartner->name << "\n";
orderService.updateStatus(order->id, StatusEvent::DELIVER);
// Illegal transition: a duplicated/out-of-order DELIVER event on an already-delivered order must be rejected.
try {
orderService.updateStatus(order->id, StatusEvent::DELIVER);
} catch (const std::exception& e) {
std::cout << "Rejected duplicate DELIVER event: " << e.what() << "\n";
}
// P1 was claimed (BUSY) for order1 and is never auto-released on delivery in this simplified model,
// so the dispatcher correctly falls back to the only remaining available partner, P2.
auto order2 = orderService.placeOrder(customer, restaurant, cart);
orderService.updateStatus(order2->id, StatusEvent::CONFIRM);
orderService.updateStatus(order2->id, StatusEvent::PREPARE);
orderService.updateStatus(order2->id, StatusEvent::PICKUP);
std::cout << "Order2 assigned partner: " << order2->assignedPartner->name << "\n";
// Direct PLACED -> DELIVERED must never be allowed.
auto order3 = orderService.placeOrder(customer, restaurant, cart);
try {
orderService.updateStatus(order3->id, StatusEvent::DELIVER);
} catch (const std::exception& e) {
std::cout << "Rejected out-of-order DELIVER on a freshly placed order: " << e.what() << "\n";
}
return 0;
}
'use strict';
// Problem 18: Restaurant / Food Delivery System (single-threaded -- no concurrency badge)
//
// MenuItem (live, editable catalog) vs OrderLineItem (frozen price/name snapshot at checkout).
// OrderState hierarchy: PLACED -> CONFIRMED -> PREPARING -> OUT_FOR_DELIVERY -> DELIVERED, each
// state only accepting its own legal next event; illegal/duplicated events are rejected by the state.
// DeliveryAssignmentStrategy is injected (Strategy); DeliveryDispatcher does the atomic select-then-claim.
// OrderStatusNotifier is a thin Observer subject fanning out every legal transition.
// OrderService is the facade: placeOrder() / updateStatus().
const OrderStatus = Object.freeze({
PLACED: 'PLACED', CONFIRMED: 'CONFIRMED', PREPARING: 'PREPARING',
OUT_FOR_DELIVERY: 'OUT_FOR_DELIVERY', DELIVERED: 'DELIVERED', CANCELLED: 'CANCELLED',
});
const StatusEvent = Object.freeze({ CONFIRM: 'CONFIRM', PREPARE: 'PREPARE', PICKUP: 'PICKUP', DELIVER: 'DELIVER', CANCEL: 'CANCEL' });
const PartnerStatus = Object.freeze({ AVAILABLE: 'AVAILABLE', BUSY: 'BUSY' });
class Customer {
constructor(id, name, address) { this.id = id; this.name = name; this.address = address; }
}
class MenuItem {
constructor(id, name, price) { this.id = id; this.name = name; this.price = price; this.available = true; }
}
class Restaurant {
constructor(id, name, location) { this.id = id; this.name = name; this.location = location; this.menu = []; }
addMenuItem(item) { this.menu.push(item); }
}
/** Freezes name/price at checkout -- a later price change must never rewrite a past receipt. */
class OrderLineItem {
constructor(menuItem, quantity) {
this.nameAtOrderTime = menuItem.name;
this.priceAtOrderTime = menuItem.price;
this.quantity = quantity;
}
subtotal() { return this.priceAtOrderTime * this.quantity; }
}
class Cart {
constructor() { this.items = []; }
addItem(menuItem, quantity) { this.items.push({ menuItem, quantity }); }
}
// ---------- OrderState (State pattern) ----------
class PlacedState {
onConfirm() { return CONFIRMED_STATE; }
onPrepared() { throw new Error('Cannot prepare before confirmation'); }
onPickedUp() { throw new Error('Cannot pick up before confirmation'); }
onDelivered() { throw new Error('Cannot deliver before confirmation'); }
status() { return OrderStatus.PLACED; }
}
class ConfirmedState {
onConfirm() { throw new Error('Already confirmed'); }
onPrepared() { return PREPARING_STATE; }
onPickedUp() { throw new Error('Cannot pick up before preparation'); }
onDelivered() { throw new Error('Cannot deliver before preparation'); }
status() { return OrderStatus.CONFIRMED; }
}
class PreparingState {
onConfirm() { throw new Error('Already confirmed'); }
onPrepared() { throw new Error('Already preparing'); }
onPickedUp() { return OUT_FOR_DELIVERY_STATE; }
onDelivered() { throw new Error('Cannot deliver before pickup'); }
status() { return OrderStatus.PREPARING; }
}
class OutForDeliveryState {
onConfirm() { throw new Error('Already confirmed'); }
onPrepared() { throw new Error('Already prepared'); }
onPickedUp() { throw new Error('Already picked up'); }
onDelivered() { return DELIVERED_STATE; }
status() { return OrderStatus.OUT_FOR_DELIVERY; }
}
class DeliveredState {
onConfirm() { throw new Error('Order already delivered -- terminal state'); }
onPrepared() { throw new Error('Order already delivered -- terminal state'); }
onPickedUp() { throw new Error('Order already delivered -- terminal state'); }
onDelivered() { throw new Error('Order already delivered -- terminal state'); }
status() { return OrderStatus.DELIVERED; }
}
class CancelledState {
onConfirm() { throw new Error('Order is cancelled'); }
onPrepared() { throw new Error('Order is cancelled'); }
onPickedUp() { throw new Error('Order is cancelled'); }
onDelivered() { throw new Error('Order is cancelled'); }
status() { return OrderStatus.CANCELLED; }
}
const PLACED_STATE = new PlacedState();
const CONFIRMED_STATE = new ConfirmedState();
const PREPARING_STATE = new PreparingState();
const OUT_FOR_DELIVERY_STATE = new OutForDeliveryState();
const DELIVERED_STATE = new DeliveredState();
class Order {
constructor(id, customer, restaurant, lineItems, notifier) {
this.id = id;
this.customer = customer;
this.restaurant = restaurant;
this.lineItems = lineItems;
this._state = PLACED_STATE;
this._notifier = notifier;
this.assignedPartner = null;
}
getStatus() { return this._state.status(); }
totalPrice() { return this.lineItems.reduce((sum, li) => sum + li.subtotal(), 0); }
/** Delegates transition legality to the current state; illegal events are rejected by the state itself. */
transitionTo(event) {
let next;
switch (event) {
case StatusEvent.CONFIRM: next = this._state.onConfirm(); break;
case StatusEvent.PREPARE: next = this._state.onPrepared(); break;
case StatusEvent.PICKUP: next = this._state.onPickedUp(); break;
case StatusEvent.DELIVER: next = this._state.onDelivered(); break;
default: throw new Error(`Unsupported event via transitionTo: ${event}`);
}
this._state = next;
this._notifier.notifyStatusChanged(this);
}
cancel() {
if (this._state === DELIVERED_STATE) throw new Error('Cannot cancel a delivered order');
this._state = new CancelledState();
this._notifier.notifyStatusChanged(this);
}
}
// ---------- Delivery assignment ----------
class DeliveryPartner {
constructor(id, name, location) { this.id = id; this.name = name; this.location = location; this.status = PartnerStatus.AVAILABLE; }
claim() {
if (this.status !== PartnerStatus.AVAILABLE) throw new Error(`Partner ${this.id} is not available`);
this.status = PartnerStatus.BUSY;
}
release() { this.status = PartnerStatus.AVAILABLE; }
}
/** @interface DeliveryAssignmentStrategy: selectPartner(order, candidates) -> DeliveryPartner */
class NearestAvailablePartnerStrategy {
selectPartner(order, candidates) {
const match = candidates.find((p) => p.location === order.restaurant.location);
if (match) return match;
if (candidates.length === 0) throw new Error('No available delivery partners');
return candidates[0]; // fall back to any available partner
}
}
/** Atomically selects and claims a partner -- selection and claim happen in the same call. */
class DeliveryDispatcher {
constructor(partnerPool, strategy) { this._partnerPool = partnerPool; this._strategy = strategy; }
assign(order) {
const candidates = this._partnerPool.filter((p) => p.status === PartnerStatus.AVAILABLE);
const chosen = this._strategy.selectPartner(order, candidates);
chosen.claim(); // claim happens as part of the same selection step -- no double-assignment race
order.assignedPartner = chosen;
return chosen;
}
}
// ---------- OrderStatusNotifier (Observer) ----------
class OrderStatusNotifier {
constructor() { this._observers = []; }
subscribe(observer) { this._observers.push(observer); }
notifyStatusChanged(order) { this._observers.forEach((o) => o.onStatusChanged(order)); }
}
class ConsoleStatusObserver {
onStatusChanged(order) { console.log(`[push] Order ${order.id} status -> ${order.getStatus()}`); }
}
// ---------- OrderService (facade) ----------
class OrderService {
constructor(dispatcher, notifier) {
this._dispatcher = dispatcher;
this._notifier = notifier;
this._orders = new Map();
this._orderSeq = 0;
}
placeOrder(customer, restaurant, cart) {
const lineItems = cart.items.map((ci) => new OrderLineItem(ci.menuItem, ci.quantity)); // freeze price/name now
const id = `ORD-${++this._orderSeq}`;
const order = new Order(id, customer, restaurant, lineItems, this._notifier);
this._orders.set(id, order);
this._notifier.notifyStatusChanged(order); // announce the initial PLACED state too
return order;
}
updateStatus(orderId, event) {
const order = this._orders.get(orderId);
if (!order) throw new Error(`Unknown order ${orderId}`);
if (event === StatusEvent.CANCEL) { order.cancel(); return; }
if (event === StatusEvent.PICKUP) this._dispatcher.assign(order); // assign a courier right before pickup
order.transitionTo(event);
}
}
function main() {
const restaurant = new Restaurant('R1', 'Curry House', 'Downtown');
const biryani = new MenuItem('M1', 'Chicken Biryani', 12.50);
const naan = new MenuItem('M2', 'Garlic Naan', 3.00);
restaurant.addMenuItem(biryani);
restaurant.addMenuItem(naan);
const notifier = new OrderStatusNotifier();
notifier.subscribe(new ConsoleStatusObserver());
const partners = [new DeliveryPartner('P1', 'Sam', 'Downtown'), new DeliveryPartner('P2', 'Robin', 'Uptown')];
const dispatcher = new DeliveryDispatcher(partners, new NearestAvailablePartnerStrategy());
const orderService = new OrderService(dispatcher, notifier);
const customer = new Customer('C1', 'Alice', '12 Elm St');
const cart = new Cart();
cart.addItem(biryani, 2);
cart.addItem(naan, 1);
const order = orderService.placeOrder(customer, restaurant, cart);
console.log(`Order placed: ${order.id}, total=${order.totalPrice().toFixed(2)}`);
// Live menu price changes after checkout must NOT affect this already-placed order's receipt.
biryani.price = 15.00;
console.log(`Menu price changed to ${biryani.price.toFixed(2)}, but order total is still ${order.totalPrice().toFixed(2)}`);
orderService.updateStatus(order.id, StatusEvent.CONFIRM);
orderService.updateStatus(order.id, StatusEvent.PREPARE);
orderService.updateStatus(order.id, StatusEvent.PICKUP); // triggers atomic partner assignment
console.log('Assigned partner:', order.assignedPartner.name);
orderService.updateStatus(order.id, StatusEvent.DELIVER);
// Illegal transition: a duplicated/out-of-order DELIVER event on an already-delivered order must be rejected.
try {
orderService.updateStatus(order.id, StatusEvent.DELIVER);
} catch (e) {
console.log('Rejected duplicate DELIVER event:', e.message);
}
// P1 was claimed (BUSY) for order1 and is never auto-released on delivery in this simplified model,
// so the dispatcher correctly falls back to the only remaining available partner, P2.
const order2 = orderService.placeOrder(customer, restaurant, cart);
orderService.updateStatus(order2.id, StatusEvent.CONFIRM);
orderService.updateStatus(order2.id, StatusEvent.PREPARE);
orderService.updateStatus(order2.id, StatusEvent.PICKUP);
console.log('Order2 assigned partner:', order2.assignedPartner.name);
// Direct PLACED -> DELIVERED must never be allowed.
const order3 = orderService.placeOrder(customer, restaurant, cart);
try {
orderService.updateStatus(order3.id, StatusEvent.DELIVER);
} catch (e) {
console.log('Rejected out-of-order DELIVER on a freshly placed order:', e.message);
}
}
if (require.main === module) {
main();
}
module.exports = {
OrderStatus, StatusEvent, PartnerStatus, Customer, MenuItem, Restaurant, OrderLineItem, Cart,
Order, DeliveryPartner, NearestAvailablePartnerStrategy, DeliveryDispatcher,
OrderStatusNotifier, ConsoleStatusObserver, OrderService,
};
// Problem 18: Restaurant / Food Delivery System (single-threaded -- no concurrency badge)
//
// MenuItem (live, editable catalog) vs OrderLineItem (frozen price/name snapshot at checkout).
// OrderState hierarchy: PLACED -> CONFIRMED -> PREPARING -> OUT_FOR_DELIVERY -> DELIVERED, each
// state only accepting its own legal next event; illegal/duplicated events are rejected by the state.
// DeliveryAssignmentStrategy is injected (Strategy); DeliveryDispatcher does the atomic select-then-claim.
// OrderStatusNotifier is a thin Observer subject fanning out every legal transition.
// OrderService is the facade: placeOrder() / updateStatus().
enum OrderStatus { PLACED = 'PLACED', CONFIRMED = 'CONFIRMED', PREPARING = 'PREPARING', OUT_FOR_DELIVERY = 'OUT_FOR_DELIVERY', DELIVERED = 'DELIVERED', CANCELLED = 'CANCELLED' }
enum StatusEvent { CONFIRM = 'CONFIRM', PREPARE = 'PREPARE', PICKUP = 'PICKUP', DELIVER = 'DELIVER', CANCEL = 'CANCEL' }
enum PartnerStatus { AVAILABLE = 'AVAILABLE', BUSY = 'BUSY' }
class Customer {
constructor(public readonly id: string, public readonly name: string, public readonly address: string) {}
}
class MenuItem {
available = true;
constructor(public readonly id: string, public name: string, public price: number) {}
}
class Restaurant {
menu: MenuItem[] = [];
constructor(public readonly id: string, public readonly name: string, public readonly location: string) {}
addMenuItem(item: MenuItem): void { this.menu.push(item); }
}
/** Freezes name/price at checkout -- a later price change must never rewrite a past receipt. */
class OrderLineItem {
readonly nameAtOrderTime: string;
readonly priceAtOrderTime: number;
constructor(menuItem: MenuItem, public readonly quantity: number) {
this.nameAtOrderTime = menuItem.name;
this.priceAtOrderTime = menuItem.price;
}
subtotal(): number { return this.priceAtOrderTime * this.quantity; }
}
interface CartEntry { menuItem: MenuItem; quantity: number }
class Cart {
items: CartEntry[] = [];
addItem(menuItem: MenuItem, quantity: number): void { this.items.push({ menuItem, quantity }); }
}
// ---------- OrderState (State pattern) ----------
interface OrderState {
onConfirm(): OrderState;
onPrepared(): OrderState;
onPickedUp(): OrderState;
onDelivered(): OrderState;
status(): OrderStatus;
}
class PlacedState implements OrderState {
onConfirm(): OrderState { return CONFIRMED_STATE; }
onPrepared(): OrderState { throw new Error('Cannot prepare before confirmation'); }
onPickedUp(): OrderState { throw new Error('Cannot pick up before confirmation'); }
onDelivered(): OrderState { throw new Error('Cannot deliver before confirmation'); }
status(): OrderStatus { return OrderStatus.PLACED; }
}
class ConfirmedState implements OrderState {
onConfirm(): OrderState { throw new Error('Already confirmed'); }
onPrepared(): OrderState { return PREPARING_STATE; }
onPickedUp(): OrderState { throw new Error('Cannot pick up before preparation'); }
onDelivered(): OrderState { throw new Error('Cannot deliver before preparation'); }
status(): OrderStatus { return OrderStatus.CONFIRMED; }
}
class PreparingState implements OrderState {
onConfirm(): OrderState { throw new Error('Already confirmed'); }
onPrepared(): OrderState { throw new Error('Already preparing'); }
onPickedUp(): OrderState { return OUT_FOR_DELIVERY_STATE; }
onDelivered(): OrderState { throw new Error('Cannot deliver before pickup'); }
status(): OrderStatus { return OrderStatus.PREPARING; }
}
class OutForDeliveryState implements OrderState {
onConfirm(): OrderState { throw new Error('Already confirmed'); }
onPrepared(): OrderState { throw new Error('Already prepared'); }
onPickedUp(): OrderState { throw new Error('Already picked up'); }
onDelivered(): OrderState { return DELIVERED_STATE; }
status(): OrderStatus { return OrderStatus.OUT_FOR_DELIVERY; }
}
class DeliveredState implements OrderState {
onConfirm(): OrderState { throw new Error('Order already delivered -- terminal state'); }
onPrepared(): OrderState { throw new Error('Order already delivered -- terminal state'); }
onPickedUp(): OrderState { throw new Error('Order already delivered -- terminal state'); }
onDelivered(): OrderState { throw new Error('Order already delivered -- terminal state'); }
status(): OrderStatus { return OrderStatus.DELIVERED; }
}
class CancelledState implements OrderState {
onConfirm(): OrderState { throw new Error('Order is cancelled'); }
onPrepared(): OrderState { throw new Error('Order is cancelled'); }
onPickedUp(): OrderState { throw new Error('Order is cancelled'); }
onDelivered(): OrderState { throw new Error('Order is cancelled'); }
status(): OrderStatus { return OrderStatus.CANCELLED; }
}
const PLACED_STATE: OrderState = new PlacedState();
const CONFIRMED_STATE: OrderState = new ConfirmedState();
const PREPARING_STATE: OrderState = new PreparingState();
const OUT_FOR_DELIVERY_STATE: OrderState = new OutForDeliveryState();
const DELIVERED_STATE: OrderState = new DeliveredState();
interface OrderStatusObserver {
onStatusChanged(order: Order): void;
}
class OrderStatusNotifier {
private observers: OrderStatusObserver[] = [];
subscribe(observer: OrderStatusObserver): void { this.observers.push(observer); }
notifyStatusChanged(order: Order): void { this.observers.forEach((o) => o.onStatusChanged(order)); }
}
class Order {
private state: OrderState = PLACED_STATE;
assignedPartner: DeliveryPartner | null = null;
constructor(
public readonly id: string,
public readonly customer: Customer,
public readonly restaurant: Restaurant,
public readonly lineItems: OrderLineItem[],
private readonly notifier: OrderStatusNotifier
) {}
getStatus(): OrderStatus { return this.state.status(); }
totalPrice(): number { return this.lineItems.reduce((sum, li) => sum + li.subtotal(), 0); }
/** Delegates transition legality to the current state; illegal events are rejected by the state itself. */
transitionTo(event: StatusEvent): void {
let next: OrderState;
switch (event) {
case StatusEvent.CONFIRM: next = this.state.onConfirm(); break;
case StatusEvent.PREPARE: next = this.state.onPrepared(); break;
case StatusEvent.PICKUP: next = this.state.onPickedUp(); break;
case StatusEvent.DELIVER: next = this.state.onDelivered(); break;
default: throw new Error(`Unsupported event via transitionTo: ${event}`);
}
this.state = next;
this.notifier.notifyStatusChanged(this);
}
cancel(): void {
if (this.state === DELIVERED_STATE) throw new Error('Cannot cancel a delivered order');
this.state = new CancelledState();
this.notifier.notifyStatusChanged(this);
}
}
class ConsoleStatusObserver implements OrderStatusObserver {
onStatusChanged(order: Order): void {
console.log(`[push] Order ${order.id} status -> ${order.getStatus()}`);
}
}
// ---------- Delivery assignment ----------
class DeliveryPartner {
status: PartnerStatus = PartnerStatus.AVAILABLE;
constructor(public readonly id: string, public readonly name: string, public readonly location: string) {}
claim(): void {
if (this.status !== PartnerStatus.AVAILABLE) throw new Error(`Partner ${this.id} is not available`);
this.status = PartnerStatus.BUSY;
}
release(): void { this.status = PartnerStatus.AVAILABLE; }
}
interface DeliveryAssignmentStrategy {
selectPartner(order: Order, candidates: DeliveryPartner[]): DeliveryPartner;
}
/** Picks the first available partner whose recorded location matches the restaurant's -- a simple "nearest" proxy. */
class NearestAvailablePartnerStrategy implements DeliveryAssignmentStrategy {
selectPartner(order: Order, candidates: DeliveryPartner[]): DeliveryPartner {
const match = candidates.find((p) => p.location === order.restaurant.location);
if (match) return match;
if (candidates.length === 0) throw new Error('No available delivery partners');
return candidates[0]; // fall back to any available partner
}
}
/** Atomically selects and claims a partner -- selection and claim happen in the same call. */
class DeliveryDispatcher {
constructor(private partnerPool: DeliveryPartner[], private strategy: DeliveryAssignmentStrategy) {}
assign(order: Order): DeliveryPartner {
const candidates = this.partnerPool.filter((p) => p.status === PartnerStatus.AVAILABLE);
const chosen = this.strategy.selectPartner(order, candidates);
chosen.claim(); // claim happens as part of the same selection step -- no double-assignment race
order.assignedPartner = chosen;
return chosen;
}
}
/** Facade: top-level orchestration. */
class OrderService {
private orders = new Map<string, Order>();
private orderSeq = 0;
constructor(private dispatcher: DeliveryDispatcher, private notifier: OrderStatusNotifier) {}
placeOrder(customer: Customer, restaurant: Restaurant, cart: Cart): Order {
const lineItems = cart.items.map((ci) => new OrderLineItem(ci.menuItem, ci.quantity)); // freeze price/name now
const id = `ORD-${++this.orderSeq}`;
const order = new Order(id, customer, restaurant, lineItems, this.notifier);
this.orders.set(id, order);
this.notifier.notifyStatusChanged(order); // announce the initial PLACED state too
return order;
}
updateStatus(orderId: string, event: StatusEvent): void {
const order = this.orders.get(orderId);
if (!order) throw new Error(`Unknown order ${orderId}`);
if (event === StatusEvent.CANCEL) { order.cancel(); return; }
if (event === StatusEvent.PICKUP) this.dispatcher.assign(order); // assign a courier right before pickup
order.transitionTo(event);
}
}
function main(): void {
const restaurant = new Restaurant('R1', 'Curry House', 'Downtown');
const biryani = new MenuItem('M1', 'Chicken Biryani', 12.50);
const naan = new MenuItem('M2', 'Garlic Naan', 3.00);
restaurant.addMenuItem(biryani);
restaurant.addMenuItem(naan);
const notifier = new OrderStatusNotifier();
notifier.subscribe(new ConsoleStatusObserver());
const partners = [new DeliveryPartner('P1', 'Sam', 'Downtown'), new DeliveryPartner('P2', 'Robin', 'Uptown')];
const dispatcher = new DeliveryDispatcher(partners, new NearestAvailablePartnerStrategy());
const orderService = new OrderService(dispatcher, notifier);
const customer = new Customer('C1', 'Alice', '12 Elm St');
const cart = new Cart();
cart.addItem(biryani, 2);
cart.addItem(naan, 1);
const order = orderService.placeOrder(customer, restaurant, cart);
console.log(`Order placed: ${order.id}, total=${order.totalPrice().toFixed(2)}`);
// Live menu price changes after checkout must NOT affect this already-placed order's receipt.
biryani.price = 15.00;
console.log(`Menu price changed to ${biryani.price.toFixed(2)}, but order total is still ${order.totalPrice().toFixed(2)}`);
orderService.updateStatus(order.id, StatusEvent.CONFIRM);
orderService.updateStatus(order.id, StatusEvent.PREPARE);
orderService.updateStatus(order.id, StatusEvent.PICKUP); // triggers atomic partner assignment
console.log('Assigned partner:', order.assignedPartner!.name);
orderService.updateStatus(order.id, StatusEvent.DELIVER);
// Illegal transition: a duplicated/out-of-order DELIVER event on an already-delivered order must be rejected.
try {
orderService.updateStatus(order.id, StatusEvent.DELIVER);
} catch (e) {
console.log('Rejected duplicate DELIVER event:', (e as Error).message);
}
// P1 was claimed (BUSY) for order1 and is never auto-released on delivery in this simplified model,
// so the dispatcher correctly falls back to the only remaining available partner, P2.
const order2 = orderService.placeOrder(customer, restaurant, cart);
orderService.updateStatus(order2.id, StatusEvent.CONFIRM);
orderService.updateStatus(order2.id, StatusEvent.PREPARE);
orderService.updateStatus(order2.id, StatusEvent.PICKUP);
console.log('Order2 assigned partner:', order2.assignedPartner!.name);
// Direct PLACED -> DELIVERED must never be allowed.
const order3 = orderService.placeOrder(customer, restaurant, cart);
try {
orderService.updateStatus(order3.id, StatusEvent.DELIVER);
} catch (e) {
console.log('Rejected out-of-order DELIVER on a freshly placed order:', (e as Error).message);
}
}
main();