Order Management System
Difficulty: Intermediate Patterns: State, Builder, Observer, Strategy Asked at: Amazon, Flipkart, Swiggy, Razorpay, PhonePe
Functional Requirements
- Place orders - create orders with stock validation and atomic deduction
- Cancel orders - cancel with stock restoration (only from PLACED/CONFIRMED)
- Status transitions - validated state machine (PLACEDβCONFIRMEDβSHIPPEDβDELIVERED)
- Notifications - observers fire on every state change
- Query orders - retrieve by ID or by customer
Non-Functional Requirements
- Thread-safety - order placement and cancellation lock stock deductions
- State integrity - invalid transitions are rejected (no skipping states)
- Extensibility - new discount strategies and notification channels without modifying service
- Correctness - stock deducted on place, restored on cancel, always consistent
Core Entities
| Entity | Description |
|---|---|
OrderStatus |
Enum - PLACED, CONFIRMED, SHIPPED, DELIVERED, CANCELLED |
Customer |
Immutable - id, name, email |
Product |
SKU with price and thread-safe stock management |
OrderItem |
Links a product to a quantity with price snapshot at order time |
Order |
Aggregates customer, items, status, total; built via Builder pattern |
OrderStateValidator |
Encodes valid state transitions as a directed graph |
OrderObserver |
Interface for notification on status changes |
DiscountStrategy |
Interface for pluggable pricing/discount logic |
OrderService |
Facade - orchestrates placement, cancellation, transitions, queries |
Class Diagram
classDiagram
class OrderStatus {
<<enumeration>>
PLACED
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
class Customer {
-String id
-String name
-String email
+getId() String
+getName() String
+getEmail() String
}
class Product {
-String sku
-String name
-double price
-int stock
+deductStock(int qty)
+restoreStock(int qty)
+getStock() int
}
class OrderItem {
-Product product
-int quantity
-double priceAtOrder
+getSubtotal() double
}
class Order {
-String id
-Customer customer
-List~OrderItem~ items
-OrderStatus status
-double totalAmount
-LocalDateTime createdAt
-LocalDateTime updatedAt
+getStatus() OrderStatus
+transitionTo(OrderStatus newStatus)
}
class OrderStateValidator {
+canTransition(OrderStatus from to OrderStatus to) boolean
+getValidTransitions(OrderStatus from) Set of OrderStatus
}
class OrderObserver {
<<interface>>
+onStatusChange(Order order and OrderStatus oldStatus and OrderStatus newStatus)
}
class EmailNotifier {
+onStatusChange(Order and OrderStatus and OrderStatus)
}
class SMSNotifier {
+onStatusChange(Order and OrderStatus and OrderStatus)
}
class DiscountStrategy {
<<interface>>
+applyDiscount(double amount) double
}
class NoDiscount {
+applyDiscount(double amount) double
}
class PercentageDiscount {
-double percentage
+applyDiscount(double amount) double
}
class OrderService {
-Map~String and Order~ orders
-List~OrderObserver~ observers
-OrderStateValidator validator
-DiscountStrategy discountStrategy
-ReentrantLock lock
+placeOrder(Customer and List~OrderItem~) Order
+cancelOrder(String orderId)
+updateStatus(String orderId and OrderStatus newStatus)
+getOrder(String orderId) Order
+getOrdersByCustomer(String customerId) List~Order~
}
Order --> OrderStatus
Order --> Customer
Order --> OrderItem
OrderItem --> Product
OrderService --> Order
OrderService --> OrderStateValidator
OrderService --> OrderObserver
OrderService --> DiscountStrategy
OrderObserver <|.. EmailNotifier
OrderObserver <|.. SMSNotifier
DiscountStrategy <|.. NoDiscount
DiscountStrategy <|.. PercentageDiscount
Design Patterns
| Pattern | Where | Why |
|---|---|---|
| State | OrderStateValidator encodes transitions |
Enforces valid lifecycle paths - no illegal state jumps |
| Builder | Order.Builder |
Assembles complex orders fluently with validation on build() |
| Observer | OrderObserver with Email/SMS notifiers |
Decouple notifications from order logic; add channels without modifying service |
| Strategy | DiscountStrategy with NoDiscount/PercentageDiscount |
Swap pricing logic at runtime without changing placement code |
How It All Fits Together
Hereβs what happens when placeOrder is called:
- OrderService acquires the order lock - ensures no concurrent order can race on stock
- Validates stock availability for ALL items before any deduction - fail fast if any product has insufficient stock
- Deducts stock atomically for each item - if validation passed, all deductions will succeed
- Applies the discount strategy to compute the final total from item subtotals
- Builds the Order using the Builder pattern - sets id, customer, items, and computed total
- Stores the order and notifies all registered observers - EmailNotifier, SMSNotifier, etc. fire
- Returns the created order; lock is released - other threads can now proceed
For cancellation, the flow is reversed: validate transition is legal (only from PLACED/CONFIRMED), restore stock for each item, set status to CANCELLED, notify observers.
Complete Code
OrderStatus and Domain Models
OrderStatus is the enum defining all possible states. Customer, Product, and OrderItem are the foundational domain objects. Product has thread-safe stock operations with its own lock for fine-grained concurrency.
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.time.LocalDateTime;
// --- Enums ---
enum OrderStatus {
PLACED, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}
// --- Domain Models ---
class Customer {
private final String id;
private final String name;
private final String email;
public Customer(String id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public String getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
}
class Product {
private final String sku;
private final String name;
private final double price;
private int stock;
private final ReentrantLock lock = new ReentrantLock();
public Product(String sku, String name, double price, int stock) {
this.sku = sku;
this.name = name;
this.price = price;
this.stock = stock;
}
public void deductStock(int qty) {
lock.lock();
try {
if (stock < qty) {
throw new IllegalStateException("Insufficient stock for " + sku
+ ". Available=" + stock + " Requested=" + qty);
}
stock -= qty;
} finally {
lock.unlock();
}
}
public void restoreStock(int qty) {
lock.lock();
try {
stock += qty;
} finally {
lock.unlock();
}
}
public int getStock() {
lock.lock();
try { return stock; } finally { lock.unlock(); }
}
public String getSku() { return sku; }
public String getName() { return name; }
public double getPrice() { return price; }
}
class OrderItem {
private final Product product;
private final int quantity;
private final double priceAtOrder;
public OrderItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
this.priceAtOrder = product.getPrice();
}
public double getSubtotal() { return priceAtOrder * quantity; }
public Product getProduct() { return product; }
public int getQuantity() { return quantity; }
public double getPriceAtOrder() { return priceAtOrder; }
}
import threading
from enum import Enum
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
from abc import ABC, abstractmethod
# --- Enums ---
class OrderStatus(Enum):
PLACED = "PLACED"
CONFIRMED = "CONFIRMED"
SHIPPED = "SHIPPED"
DELIVERED = "DELIVERED"
CANCELLED = "CANCELLED"
# --- Domain Models ---
@dataclass
class Customer:
id: str
name: str
email: str
class Product:
def __init__(self, sku: str, name: str, price: float, stock: int):
self.sku = sku
self.name = name
self.price = price
self._stock = stock
self._lock = threading.Lock()
def deduct_stock(self, qty: int) -> None:
with self._lock:
if self._stock < qty:
raise ValueError(f"Insufficient stock for {self.sku}. "
f"Available={self._stock} Requested={qty}")
self._stock -= qty
def restore_stock(self, qty: int) -> None:
with self._lock:
self._stock += qty
@property
def stock(self) -> int:
with self._lock:
return self._stock
@dataclass
class OrderItem:
product: Product
quantity: int
price_at_order: float = 0.0
def __post_init__(self):
self.price_at_order = self.product.price
@property
def subtotal(self) -> float:
return self.price_at_order * self.quantity
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <mutex>
#include <memory>
#include <stdexcept>
#include <algorithm>
#include <numeric>
#include <functional>
// --- Enums ---
enum class OrderStatus { PLACED, CONFIRMED, SHIPPED, DELIVERED, CANCELLED };
std::string statusToString(OrderStatus s) {
switch (s) {
case OrderStatus::PLACED: return "PLACED";
case OrderStatus::CONFIRMED: return "CONFIRMED";
case OrderStatus::SHIPPED: return "SHIPPED";
case OrderStatus::DELIVERED: return "DELIVERED";
case OrderStatus::CANCELLED: return "CANCELLED";
}
return "UNKNOWN";
}
// --- Domain Models ---
struct Customer {
std::string id;
std::string name;
std::string email;
};
class Product {
private:
std::string sku_;
std::string name_;
double price_;
int stock_;
mutable std::mutex mtx_;
public:
Product(const std::string& sku, const std::string& name, double price, int stock)
: sku_(sku), name_(name), price_(price), stock_(stock) {}
void deductStock(int qty) {
std::lock_guard<std::mutex> guard(mtx_);
if (stock_ < qty)
throw std::runtime_error("Insufficient stock for " + sku_);
stock_ -= qty;
}
void restoreStock(int qty) {
std::lock_guard<std::mutex> guard(mtx_);
stock_ += qty;
}
int getStock() const {
std::lock_guard<std::mutex> guard(mtx_);
return stock_;
}
const std::string& getSku() const { return sku_; }
const std::string& getName() const { return name_; }
double getPrice() const { return price_; }
};
struct OrderItem {
Product* product;
int quantity;
double priceAtOrder;
OrderItem(Product* p, int qty) : product(p), quantity(qty), priceAtOrder(p->getPrice()) {}
double getSubtotal() const { return priceAtOrder * quantity; }
};
// --- Enums ---
const OrderStatus = Object.freeze({
PLACED: 'PLACED',
CONFIRMED: 'CONFIRMED',
SHIPPED: 'SHIPPED',
DELIVERED: 'DELIVERED',
CANCELLED: 'CANCELLED'
});
// --- Domain Models ---
class Customer {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
}
}
class Product {
#sku; #name; #price; #stock;
constructor(sku, name, price, stock) {
this.#sku = sku;
this.#name = name;
this.#price = price;
this.#stock = stock;
}
deductStock(qty) {
if (this.#stock < qty) {
throw new Error(`Insufficient stock for ${this.#sku}. Available=${this.#stock} Requested=${qty}`);
}
this.#stock -= qty;
}
restoreStock(qty) { this.#stock += qty; }
get stock() { return this.#stock; }
get sku() { return this.#sku; }
get name() { return this.#name; }
get price() { return this.#price; }
}
class OrderItem {
constructor(product, quantity) {
this.product = product;
this.quantity = quantity;
this.priceAtOrder = product.price;
}
get subtotal() { return this.priceAtOrder * this.quantity; }
}
Order
The Order class uses the Builder pattern to construct complex order objects with validation. The builder ensures that an order always has an id, customer, and at least one item before it can be built.
π‘ Builder pattern = separate the construction of a complex object from its representation. Here it prevents partially-constructed orders from escaping into the system. The fluent API makes order assembly readable and the build() method acts as a final validation gate.
class Order {
private final String id;
private final Customer customer;
private final List<OrderItem> items;
private OrderStatus status;
private final double totalAmount;
private final LocalDateTime createdAt;
private LocalDateTime updatedAt;
private Order(Builder builder) {
this.id = builder.id;
this.customer = builder.customer;
this.items = Collections.unmodifiableList(builder.items);
this.status = OrderStatus.PLACED;
this.totalAmount = builder.totalAmount;
this.createdAt = LocalDateTime.now();
this.updatedAt = this.createdAt;
}
public void setStatus(OrderStatus status) {
this.status = status;
this.updatedAt = LocalDateTime.now();
}
public String getId() { return id; }
public Customer getCustomer() { return customer; }
public List<OrderItem> getItems() { return items; }
public OrderStatus getStatus() { return status; }
public double getTotalAmount() { return totalAmount; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
@Override
public String toString() {
return "Order{id=" + id + " status=" + status + " total=$" + totalAmount + "}";
}
// Builder pattern
static class Builder {
private String id;
private Customer customer;
private List<OrderItem> items = new ArrayList<>();
private double totalAmount;
public Builder id(String id) { this.id = id; return this; }
public Builder customer(Customer customer) { this.customer = customer; return this; }
public Builder addItem(OrderItem item) { this.items.add(item); return this; }
public Builder totalAmount(double amount) { this.totalAmount = amount; return this; }
public Order build() {
if (id == null || customer == null || items.isEmpty()) {
throw new IllegalStateException("Order requires id, customer, and at least one item");
}
return new Order(this);
}
}
}
class Order:
def __init__(self, order_id: str, customer: Customer, items: list,
total_amount: float):
self.id = order_id
self.customer = customer
self.items = list(items)
self.status = OrderStatus.PLACED
self.total_amount = total_amount
self.created_at = datetime.now()
self.updated_at = self.created_at
def set_status(self, status: OrderStatus) -> None:
self.status = status
self.updated_at = datetime.now()
def __str__(self) -> str:
return f"Order{{id={self.id} status={self.status.value} total=${self.total_amount:.2f}}}"
class Order {
public:
std::string id;
Customer customer;
std::vector<OrderItem> items;
OrderStatus status;
double totalAmount;
Order(const std::string& id, const Customer& customer,
const std::vector<OrderItem>& items, double total)
: id(id), customer(customer), items(items), status(OrderStatus::PLACED),
totalAmount(total) {}
std::string toString() const {
return "Order{id=" + id + " status=" + statusToString(status)
+ " total=$" + std::to_string(totalAmount) + "}";
}
};
class Order {
constructor(id, customer, items, totalAmount) {
this.id = id;
this.customer = customer;
this.items = [...items];
this.status = OrderStatus.PLACED;
this.totalAmount = totalAmount;
this.createdAt = new Date();
this.updatedAt = this.createdAt;
}
setStatus(status) {
this.status = status;
this.updatedAt = new Date();
}
toString() {
return `Order{id=${this.id} status=${this.status} total=$${this.totalAmount.toFixed(2)}}`;
}
}
OrderStateValidator
The state machine is encoded as a map from each status to its set of valid next statuses. This makes it trivial to add new states or modify transitions without touching order logic.
π‘ Encoding the state machine as a data structure (map of transitions) rather than a chain of if/else means adding a new state like REFUNDED only requires adding one map entry β zero changes to existing validation logic.
class OrderStateValidator {
private static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = new EnumMap<>(OrderStatus.class);
static {
TRANSITIONS.put(OrderStatus.PLACED, EnumSet.of(OrderStatus.CONFIRMED, OrderStatus.CANCELLED));
TRANSITIONS.put(OrderStatus.CONFIRMED, EnumSet.of(OrderStatus.SHIPPED, OrderStatus.CANCELLED));
TRANSITIONS.put(OrderStatus.SHIPPED, EnumSet.of(OrderStatus.DELIVERED));
TRANSITIONS.put(OrderStatus.DELIVERED, EnumSet.noneOf(OrderStatus.class));
TRANSITIONS.put(OrderStatus.CANCELLED, EnumSet.noneOf(OrderStatus.class));
}
public boolean canTransition(OrderStatus from, OrderStatus to) {
Set<OrderStatus> valid = TRANSITIONS.get(from);
return valid != null && valid.contains(to);
}
public Set<OrderStatus> getValidTransitions(OrderStatus from) {
return Collections.unmodifiableSet(TRANSITIONS.getOrDefault(from, EnumSet.noneOf(OrderStatus.class)));
}
}
class OrderStateValidator:
_TRANSITIONS: dict[OrderStatus, set[OrderStatus]] = {
OrderStatus.PLACED: {OrderStatus.CONFIRMED, OrderStatus.CANCELLED},
OrderStatus.CONFIRMED: {OrderStatus.SHIPPED, OrderStatus.CANCELLED},
OrderStatus.SHIPPED: {OrderStatus.DELIVERED},
OrderStatus.DELIVERED: set(),
OrderStatus.CANCELLED: set(),
}
def can_transition(self, from_status: OrderStatus, to_status: OrderStatus) -> bool:
valid = self._TRANSITIONS.get(from_status, set())
return to_status in valid
def get_valid_transitions(self, from_status: OrderStatus) -> set[OrderStatus]:
return self._TRANSITIONS.get(from_status, set())
class OrderStateValidator {
private:
std::unordered_map<int, std::vector<OrderStatus>> transitions_;
public:
OrderStateValidator() {
transitions_[(int)OrderStatus::PLACED] = {OrderStatus::CONFIRMED, OrderStatus::CANCELLED};
transitions_[(int)OrderStatus::CONFIRMED] = {OrderStatus::SHIPPED, OrderStatus::CANCELLED};
transitions_[(int)OrderStatus::SHIPPED] = {OrderStatus::DELIVERED};
transitions_[(int)OrderStatus::DELIVERED] = {};
transitions_[(int)OrderStatus::CANCELLED] = {};
}
bool canTransition(OrderStatus from, OrderStatus to) const {
auto it = transitions_.find((int)from);
if (it == transitions_.end()) return false;
auto& v = it->second;
return std::find(v.begin(), v.end(), to) != v.end();
}
};
class OrderStateValidator {
#transitions = {
[OrderStatus.PLACED]: new Set([OrderStatus.CONFIRMED, OrderStatus.CANCELLED]),
[OrderStatus.CONFIRMED]: new Set([OrderStatus.SHIPPED, OrderStatus.CANCELLED]),
[OrderStatus.SHIPPED]: new Set([OrderStatus.DELIVERED]),
[OrderStatus.DELIVERED]: new Set(),
[OrderStatus.CANCELLED]: new Set()
};
canTransition(from, to) {
const valid = this.#transitions[from];
return valid ? valid.has(to) : false;
}
getValidTransitions(from) {
return this.#transitions[from] || new Set();
}
}
Observer Interface and Implementations
OrderObserver defines the contract for reacting to status changes. EmailNotifier and SMSNotifier are two implementations - adding a PushNotifier or WebhookNotifier requires zero changes to OrderService.
interface OrderObserver {
void onStatusChange(Order order, OrderStatus oldStatus, OrderStatus newStatus);
}
class EmailNotifier implements OrderObserver {
@Override
public void onStatusChange(Order order, OrderStatus oldStatus, OrderStatus newStatus) {
System.out.println(" [Email] Order " + order.getId() + " moved from "
+ oldStatus + " to " + newStatus
+ " | Notifying: " + order.getCustomer().getEmail());
}
}
class SMSNotifier implements OrderObserver {
@Override
public void onStatusChange(Order order, OrderStatus oldStatus, OrderStatus newStatus) {
System.out.println(" [SMS] Order " + order.getId() + ": " + oldStatus + " -> " + newStatus);
}
}
class OrderObserver(ABC):
@abstractmethod
def on_status_change(self, order: Order, old_status: Optional[OrderStatus],
new_status: OrderStatus) -> None:
pass
class EmailNotifier(OrderObserver):
def on_status_change(self, order, old_status, new_status):
print(f" [Email] Order {order.id} moved from {old_status} to {new_status.value}"
f" | Notifying: {order.customer.email}")
class SMSNotifier(OrderObserver):
def on_status_change(self, order, old_status, new_status):
print(f" [SMS] Order {order.id}: {old_status} -> {new_status.value}")
class OrderObserver {
public:
virtual ~OrderObserver() = default;
virtual void onStatusChange(const Order& order, OrderStatus oldStatus, OrderStatus newStatus) = 0;
};
class EmailNotifier : public OrderObserver {
public:
void onStatusChange(const Order& order, OrderStatus oldStatus, OrderStatus newStatus) override {
std::cout << " [Email] Order " << order.id << " moved from "
<< statusToString(oldStatus) << " to " << statusToString(newStatus)
<< " | Notifying: " << order.customer.email << std::endl;
}
};
class SMSNotifier : public OrderObserver {
public:
void onStatusChange(const Order& order, OrderStatus oldStatus, OrderStatus newStatus) override {
std::cout << " [SMS] Order " << order.id << ": "
<< statusToString(oldStatus) << " -> " << statusToString(newStatus) << std::endl;
}
};
class EmailNotifier {
onStatusChange(order, oldStatus, newStatus) {
console.log(` [Email] Order ${order.id} moved from ${oldStatus} to ${newStatus}` +
` | Notifying: ${order.customer.email}`);
}
}
class SMSNotifier {
onStatusChange(order, oldStatus, newStatus) {
console.log(` [SMS] Order ${order.id}: ${oldStatus} -> ${newStatus}`);
}
}
DiscountStrategy
The Strategy interface for pricing. NoDiscount passes through the amount unchanged; PercentageDiscount applies a percentage reduction. You can add FlatDiscount, CouponDiscount, or TieredDiscount without touching OrderService.
π‘ Strategy pattern = define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. OrderService doesnβt know or care which discount is active β it just calls applyDiscount() and gets back the final price.
interface DiscountStrategy {
double applyDiscount(double amount);
}
class NoDiscount implements DiscountStrategy {
@Override
public double applyDiscount(double amount) { return amount; }
}
class PercentageDiscount implements DiscountStrategy {
private final double percentage;
public PercentageDiscount(double percentage) {
if (percentage < 0 || percentage > 100) throw new IllegalArgumentException("Invalid percentage");
this.percentage = percentage;
}
@Override
public double applyDiscount(double amount) {
return amount * (1 - percentage / 100.0);
}
}
class DiscountStrategy(ABC):
@abstractmethod
def apply_discount(self, amount: float) -> float:
pass
class NoDiscount(DiscountStrategy):
def apply_discount(self, amount: float) -> float:
return amount
class PercentageDiscount(DiscountStrategy):
def __init__(self, percentage: float):
if not 0 <= percentage <= 100:
raise ValueError("Invalid percentage")
self._percentage = percentage
def apply_discount(self, amount: float) -> float:
return amount * (1 - self._percentage / 100.0)
class DiscountStrategy {
public:
virtual ~DiscountStrategy() = default;
virtual double applyDiscount(double amount) = 0;
};
class NoDiscount : public DiscountStrategy {
public:
double applyDiscount(double amount) override { return amount; }
};
class PercentageDiscount : public DiscountStrategy {
double percentage_;
public:
PercentageDiscount(double pct) : percentage_(pct) {}
double applyDiscount(double amount) override {
return amount * (1.0 - percentage_ / 100.0);
}
};
class NoDiscount {
applyDiscount(amount) { return amount; }
}
class PercentageDiscount {
#percentage;
constructor(percentage) { this.#percentage = percentage; }
applyDiscount(amount) { return amount * (1 - this.#percentage / 100); }
}
OrderService
The main service that orchestrates order placement, cancellation, status updates, and queries. Uses a ReentrantLock (or mutex/threading lock) to ensure thread-safe stock operations. Delegates state validation to OrderStateValidator and notifications to registered observers.
class OrderService {
private final Map<String, Order> orders = new ConcurrentHashMap<>();
private final List<OrderObserver> observers = new CopyOnWriteArrayList<>();
private final OrderStateValidator validator = new OrderStateValidator();
private DiscountStrategy discountStrategy;
private final ReentrantLock orderLock = new ReentrantLock();
private int orderCounter = 0;
public OrderService(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void setDiscountStrategy(DiscountStrategy strategy) {
this.discountStrategy = strategy;
}
public Order placeOrder(Customer customer, List<OrderItem> items) {
if (customer == null) throw new IllegalArgumentException("Customer is required");
if (items == null || items.isEmpty()) throw new IllegalArgumentException("At least one item required");
orderLock.lock();
try {
// Validate stock for all items first
for (OrderItem item : items) {
if (item.getProduct().getStock() < item.getQuantity()) {
throw new IllegalStateException("Insufficient stock for product: "
+ item.getProduct().getSku());
}
}
// Deduct stock atomically
for (OrderItem item : items) {
item.getProduct().deductStock(item.getQuantity());
}
// Calculate total with discount
double subtotal = items.stream().mapToDouble(OrderItem::getSubtotal).sum();
double total = discountStrategy.applyDiscount(subtotal);
// Build order
String orderId = "ORD-" + (++orderCounter);
Order order = new Order.Builder()
.id(orderId)
.customer(customer)
.totalAmount(total)
.build();
// Add items via builder workaround (items already in builder)
Order finalOrder = new Order.Builder()
.id(orderId)
.customer(customer)
.totalAmount(total)
.build();
// Rebuild with items
Order.Builder builder = new Order.Builder().id(orderId).customer(customer).totalAmount(total);
for (OrderItem item : items) {
builder.addItem(item);
}
finalOrder = builder.build();
orders.put(orderId, finalOrder);
notifyObservers(finalOrder, null, OrderStatus.PLACED);
return finalOrder;
} finally {
orderLock.unlock();
}
}
public void cancelOrder(String orderId) {
orderLock.lock();
try {
Order order = getOrderOrThrow(orderId);
OrderStatus oldStatus = order.getStatus();
if (!validator.canTransition(oldStatus, OrderStatus.CANCELLED)) {
throw new IllegalStateException("Cannot cancel order in state: " + oldStatus
+ ". Cancellation only allowed from PLACED or CONFIRMED.");
}
// Restore stock
for (OrderItem item : order.getItems()) {
item.getProduct().restoreStock(item.getQuantity());
}
order.setStatus(OrderStatus.CANCELLED);
notifyObservers(order, oldStatus, OrderStatus.CANCELLED);
} finally {
orderLock.unlock();
}
}
public void updateStatus(String orderId, OrderStatus newStatus) {
orderLock.lock();
try {
Order order = getOrderOrThrow(orderId);
OrderStatus oldStatus = order.getStatus();
if (!validator.canTransition(oldStatus, newStatus)) {
throw new IllegalStateException("Invalid transition: " + oldStatus + " -> " + newStatus
+ ". Valid transitions: " + validator.getValidTransitions(oldStatus));
}
order.setStatus(newStatus);
notifyObservers(order, oldStatus, newStatus);
} finally {
orderLock.unlock();
}
}
public Order getOrder(String orderId) {
return getOrderOrThrow(orderId);
}
public List<Order> getOrdersByCustomer(String customerId) {
List<Order> result = new ArrayList<>();
for (Order order : orders.values()) {
if (order.getCustomer().getId().equals(customerId)) {
result.add(order);
}
}
return result;
}
private Order getOrderOrThrow(String orderId) {
Order order = orders.get(orderId);
if (order == null) throw new IllegalArgumentException("Order not found: " + orderId);
return order;
}
private void notifyObservers(Order order, OrderStatus oldStatus, OrderStatus newStatus) {
for (OrderObserver observer : observers) {
observer.onStatusChange(order, oldStatus, newStatus);
}
}
}
class OrderService:
def __init__(self, discount_strategy: DiscountStrategy):
self._orders: dict[str, Order] = {}
self._observers: list[OrderObserver] = []
self._validator = OrderStateValidator()
self._discount_strategy = discount_strategy
self._lock = threading.Lock()
self._counter = 0
def add_observer(self, observer: OrderObserver) -> None:
self._observers.append(observer)
def set_discount_strategy(self, strategy: DiscountStrategy) -> None:
self._discount_strategy = strategy
def place_order(self, customer: Customer, items: list[OrderItem]) -> Order:
if not customer:
raise ValueError("Customer is required")
if not items:
raise ValueError("At least one item required")
with self._lock:
# Validate stock
for item in items:
if item.product.stock < item.quantity:
raise ValueError(f"Insufficient stock for {item.product.sku}")
# Deduct stock
for item in items:
item.product.deduct_stock(item.quantity)
# Calculate total
subtotal = sum(item.subtotal for item in items)
total = self._discount_strategy.apply_discount(subtotal)
# Build order
self._counter += 1
order_id = f"ORD-{self._counter}"
order = Order(order_id, customer, items, total)
self._orders[order_id] = order
self._notify_observers(order, None, OrderStatus.PLACED)
return order
def cancel_order(self, order_id: str) -> None:
with self._lock:
order = self._get_order_or_throw(order_id)
old_status = order.status
if not self._validator.can_transition(old_status, OrderStatus.CANCELLED):
raise ValueError(f"Cannot cancel order in state: {old_status.value}. "
f"Cancellation only allowed from PLACED or CONFIRMED.")
# Restore stock
for item in order.items:
item.product.restore_stock(item.quantity)
order.set_status(OrderStatus.CANCELLED)
self._notify_observers(order, old_status, OrderStatus.CANCELLED)
def update_status(self, order_id: str, new_status: OrderStatus) -> None:
with self._lock:
order = self._get_order_or_throw(order_id)
old_status = order.status
if not self._validator.can_transition(old_status, new_status):
valid = self._validator.get_valid_transitions(old_status)
raise ValueError(f"Invalid transition: {old_status.value} -> {new_status.value}. "
f"Valid: {[s.value for s in valid]}")
order.set_status(new_status)
self._notify_observers(order, old_status, new_status)
def get_order(self, order_id: str) -> Order:
return self._get_order_or_throw(order_id)
def get_orders_by_customer(self, customer_id: str) -> list[Order]:
return [o for o in self._orders.values() if o.customer.id == customer_id]
def _get_order_or_throw(self, order_id: str) -> Order:
order = self._orders.get(order_id)
if not order:
raise ValueError(f"Order not found: {order_id}")
return order
def _notify_observers(self, order: Order, old_status: Optional[OrderStatus],
new_status: OrderStatus) -> None:
for observer in self._observers:
observer.on_status_change(order, old_status, new_status)
class OrderService {
private:
std::unordered_map<std::string, std::unique_ptr<Order>> orders_;
std::vector<OrderObserver*> observers_;
OrderStateValidator validator_;
DiscountStrategy* discountStrategy_;
std::mutex lock_;
int counter_ = 0;
void notifyObservers(const Order& order, OrderStatus oldStatus, OrderStatus newStatus) {
for (auto* obs : observers_) {
obs->onStatusChange(order, oldStatus, newStatus);
}
}
public:
OrderService(DiscountStrategy* strategy) : discountStrategy_(strategy) {}
void addObserver(OrderObserver* obs) { observers_.push_back(obs); }
void setDiscountStrategy(DiscountStrategy* s) { discountStrategy_ = s; }
Order* placeOrder(const Customer& customer, std::vector<OrderItem> items) {
std::lock_guard<std::mutex> guard(lock_);
for (auto& item : items) {
if (item.product->getStock() < item.quantity)
throw std::runtime_error("Insufficient stock for " + item.product->getSku());
}
for (auto& item : items) {
item.product->deductStock(item.quantity);
}
double subtotal = 0;
for (auto& item : items) subtotal += item.getSubtotal();
double total = discountStrategy_->applyDiscount(subtotal);
std::string orderId = "ORD-" + std::to_string(++counter_);
auto order = std::make_unique<Order>(orderId, customer, items, total);
Order* ptr = order.get();
orders_[orderId] = std::move(order);
notifyObservers(*ptr, OrderStatus::PLACED, OrderStatus::PLACED);
return ptr;
}
void cancelOrder(const std::string& orderId) {
std::lock_guard<std::mutex> guard(lock_);
auto* order = getOrderOrThrow(orderId);
OrderStatus oldStatus = order->status;
if (!validator_.canTransition(oldStatus, OrderStatus::CANCELLED)) {
throw std::runtime_error("Cannot cancel order in state: " + statusToString(oldStatus));
}
for (auto& item : order->items) {
item.product->restoreStock(item.quantity);
}
order->status = OrderStatus::CANCELLED;
notifyObservers(*order, oldStatus, OrderStatus::CANCELLED);
}
void updateStatus(const std::string& orderId, OrderStatus newStatus) {
std::lock_guard<std::mutex> guard(lock_);
auto* order = getOrderOrThrow(orderId);
OrderStatus oldStatus = order->status;
if (!validator_.canTransition(oldStatus, newStatus)) {
throw std::runtime_error("Invalid transition: " + statusToString(oldStatus)
+ " -> " + statusToString(newStatus));
}
order->status = newStatus;
notifyObservers(*order, oldStatus, newStatus);
}
Order* getOrder(const std::string& orderId) { return getOrderOrThrow(orderId); }
private:
Order* getOrderOrThrow(const std::string& orderId) {
auto it = orders_.find(orderId);
if (it == orders_.end()) throw std::runtime_error("Order not found: " + orderId);
return it->second.get();
}
};
// JS is single-threaded; operations within one event loop tick are atomic.
class OrderService {
#orders = new Map();
#observers = [];
#validator = new OrderStateValidator();
#discountStrategy;
#counter = 0;
constructor(discountStrategy) {
this.#discountStrategy = discountStrategy;
}
addObserver(observer) { this.#observers.push(observer); }
setDiscountStrategy(strategy) { this.#discountStrategy = strategy; }
placeOrder(customer, items) {
if (!customer) throw new Error('Customer is required');
if (!items || items.length === 0) throw new Error('At least one item required');
// Validate stock
for (const item of items) {
if (item.product.stock < item.quantity) {
throw new Error(`Insufficient stock for ${item.product.sku}`);
}
}
// Deduct stock
for (const item of items) {
item.product.deductStock(item.quantity);
}
const subtotal = items.reduce((sum, item) => sum + item.subtotal, 0);
const total = this.#discountStrategy.applyDiscount(subtotal);
const orderId = `ORD-${++this.#counter}`;
const order = new Order(orderId, customer, items, total);
this.#orders.set(orderId, order);
this.#notifyObservers(order, null, OrderStatus.PLACED);
return order;
}
cancelOrder(orderId) {
const order = this.#getOrderOrThrow(orderId);
const oldStatus = order.status;
if (!this.#validator.canTransition(oldStatus, OrderStatus.CANCELLED)) {
throw new Error(`Cannot cancel order in state: ${oldStatus}. ` +
`Cancellation only allowed from PLACED or CONFIRMED.`);
}
for (const item of order.items) {
item.product.restoreStock(item.quantity);
}
order.setStatus(OrderStatus.CANCELLED);
this.#notifyObservers(order, oldStatus, OrderStatus.CANCELLED);
}
updateStatus(orderId, newStatus) {
const order = this.#getOrderOrThrow(orderId);
const oldStatus = order.status;
if (!this.#validator.canTransition(oldStatus, newStatus)) {
const valid = [...this.#validator.getValidTransitions(oldStatus)];
throw new Error(`Invalid transition: ${oldStatus} -> ${newStatus}. Valid: ${valid}`);
}
order.setStatus(newStatus);
this.#notifyObservers(order, oldStatus, newStatus);
}
getOrder(orderId) { return this.#getOrderOrThrow(orderId); }
getOrdersByCustomer(customerId) {
return [...this.#orders.values()].filter(o => o.customer.id === customerId);
}
#getOrderOrThrow(orderId) {
const order = this.#orders.get(orderId);
if (!order) throw new Error(`Order not found: ${orderId}`);
return order;
}
#notifyObservers(order, oldStatus, newStatus) {
for (const obs of this.#observers) {
obs.onStatusChange(order, oldStatus, newStatus);
}
}
}
Main / Demo
Puts it all together: creates products, wires up the service with a discount strategy and observers, then exercises placement, transitions, cancellation, error handling, and strategy switching.
public class OrderManagementDemo {
public static void main(String[] args) {
// Setup products
Product mouse = new Product("SKU-001", "Wireless Mouse", 29.99, 50);
Product keyboard = new Product("SKU-002", "Mechanical Keyboard", 79.99, 20);
// Setup service with 10% discount
OrderService service = new OrderService(new PercentageDiscount(10));
service.addObserver(new EmailNotifier());
service.addObserver(new SMSNotifier());
// Setup customer
Customer alice = new Customer("C-1", "Alice", "alice@example.com");
// Place order
System.out.println("=== Place Order ===");
List<OrderItem> items = List.of(
new OrderItem(mouse, 2),
new OrderItem(keyboard, 1)
);
Order order1 = service.placeOrder(alice, items);
System.out.println("Created: " + order1);
System.out.println("Mouse stock after order: " + mouse.getStock());
// Update status through lifecycle
System.out.println("\n=== Status Transitions ===");
service.updateStatus(order1.getId(), OrderStatus.CONFIRMED);
service.updateStatus(order1.getId(), OrderStatus.SHIPPED);
service.updateStatus(order1.getId(), OrderStatus.DELIVERED);
System.out.println("Final status: " + order1.getStatus());
// Place another order and cancel it
System.out.println("\n=== Cancel Order ===");
Order order2 = service.placeOrder(alice, List.of(new OrderItem(mouse, 5)));
System.out.println("Mouse stock after order2: " + mouse.getStock());
service.cancelOrder(order2.getId());
System.out.println("Mouse stock after cancel: " + mouse.getStock());
System.out.println("Order2 status: " + order2.getStatus());
// Invalid transition
System.out.println("\n=== Invalid Transition ===");
try {
service.updateStatus(order1.getId(), OrderStatus.PLACED);
} catch (IllegalStateException e) {
System.out.println("Caught: " + e.getMessage());
}
// Cannot cancel delivered order
System.out.println("\n=== Cannot Cancel Delivered ===");
try {
service.cancelOrder(order1.getId());
} catch (IllegalStateException e) {
System.out.println("Caught: " + e.getMessage());
}
// Query orders
System.out.println("\n=== Orders by Customer ===");
List<Order> aliceOrders = service.getOrdersByCustomer("C-1");
for (Order o : aliceOrders) {
System.out.println(" " + o);
}
// Switch discount strategy
System.out.println("\n=== Switch to No Discount ===");
service.setDiscountStrategy(new NoDiscount());
Order order3 = service.placeOrder(alice, List.of(new OrderItem(keyboard, 1)));
System.out.println("Order3 (no discount): " + order3);
}
}
def main():
mouse = Product("SKU-001", "Wireless Mouse", 29.99, 50)
keyboard = Product("SKU-002", "Mechanical Keyboard", 79.99, 20)
service = OrderService(PercentageDiscount(10))
service.add_observer(EmailNotifier())
service.add_observer(SMSNotifier())
alice = Customer("C-1", "Alice", "alice@example.com")
# Place order
print("=== Place Order ===")
items = [OrderItem(mouse, 2), OrderItem(keyboard, 1)]
order1 = service.place_order(alice, items)
print(f"Created: {order1}")
print(f"Mouse stock after order: {mouse.stock}")
# Transitions
print("\n=== Status Transitions ===")
service.update_status(order1.id, OrderStatus.CONFIRMED)
service.update_status(order1.id, OrderStatus.SHIPPED)
service.update_status(order1.id, OrderStatus.DELIVERED)
print(f"Final status: {order1.status.value}")
# Cancel
print("\n=== Cancel Order ===")
order2 = service.place_order(alice, [OrderItem(mouse, 5)])
print(f"Mouse stock after order2: {mouse.stock}")
service.cancel_order(order2.id)
print(f"Mouse stock after cancel: {mouse.stock}")
print(f"Order2 status: {order2.status.value}")
# Invalid transition
print("\n=== Invalid Transition ===")
try:
service.update_status(order1.id, OrderStatus.PLACED)
except ValueError as e:
print(f"Caught: {e}")
# Cannot cancel delivered
print("\n=== Cannot Cancel Delivered ===")
try:
service.cancel_order(order1.id)
except ValueError as e:
print(f"Caught: {e}")
# Query
print("\n=== Orders by Customer ===")
for o in service.get_orders_by_customer("C-1"):
print(f" {o}")
# Switch strategy
print("\n=== Switch to No Discount ===")
service.set_discount_strategy(NoDiscount())
order3 = service.place_order(alice, [OrderItem(keyboard, 1)])
print(f"Order3 (no discount): {order3}")
if __name__ == "__main__":
main()
int main() {
Product mouse("SKU-001", "Wireless Mouse", 29.99, 50);
Product keyboard("SKU-002", "Mechanical Keyboard", 79.99, 20);
PercentageDiscount discount(10);
OrderService service(&discount);
EmailNotifier emailNotifier;
SMSNotifier smsNotifier;
service.addObserver(&emailNotifier);
service.addObserver(&smsNotifier);
Customer alice{"C-1", "Alice", "alice@example.com"};
std::cout << "=== Place Order ===" << std::endl;
std::vector<OrderItem> items = { OrderItem(&mouse, 2), OrderItem(&keyboard, 1) };
Order* order1 = service.placeOrder(alice, items);
std::cout << "Created: " << order1->toString() << std::endl;
std::cout << "Mouse stock: " << mouse.getStock() << std::endl;
std::cout << "\n=== Status Transitions ===" << std::endl;
service.updateStatus(order1->id, OrderStatus::CONFIRMED);
service.updateStatus(order1->id, OrderStatus::SHIPPED);
service.updateStatus(order1->id, OrderStatus::DELIVERED);
std::cout << "Final: " << statusToString(order1->status) << std::endl;
std::cout << "\n=== Cancel Order ===" << std::endl;
Order* order2 = service.placeOrder(alice, {OrderItem(&mouse, 5)});
std::cout << "Mouse stock after order2: " << mouse.getStock() << std::endl;
service.cancelOrder(order2->id);
std::cout << "Mouse stock after cancel: " << mouse.getStock() << std::endl;
std::cout << "\n=== Invalid Transition ===" << std::endl;
try {
service.updateStatus(order1->id, OrderStatus::PLACED);
} catch (const std::runtime_error& e) {
std::cout << "Caught: " << e.what() << std::endl;
}
std::cout << "\n=== Cannot Cancel Delivered ===" << std::endl;
try {
service.cancelOrder(order1->id);
} catch (const std::runtime_error& e) {
std::cout << "Caught: " << e.what() << std::endl;
}
// Switch strategy
std::cout << "\n=== No Discount ===" << std::endl;
NoDiscount noDiscount;
service.setDiscountStrategy(&noDiscount);
Order* order3 = service.placeOrder(alice, {OrderItem(&keyboard, 1)});
std::cout << "Order3: " << order3->toString() << std::endl;
return 0;
}
function main() {
const mouse = new Product('SKU-001', 'Wireless Mouse', 29.99, 50);
const keyboard = new Product('SKU-002', 'Mechanical Keyboard', 79.99, 20);
const service = new OrderService(new PercentageDiscount(10));
service.addObserver(new EmailNotifier());
service.addObserver(new SMSNotifier());
const alice = new Customer('C-1', 'Alice', 'alice@example.com');
console.log('=== Place Order ===');
const items = [new OrderItem(mouse, 2), new OrderItem(keyboard, 1)];
const order1 = service.placeOrder(alice, items);
console.log(`Created: ${order1.toString()}`);
console.log(`Mouse stock after order: ${mouse.stock}`);
console.log('\n=== Status Transitions ===');
service.updateStatus(order1.id, OrderStatus.CONFIRMED);
service.updateStatus(order1.id, OrderStatus.SHIPPED);
service.updateStatus(order1.id, OrderStatus.DELIVERED);
console.log(`Final status: ${order1.status}`);
console.log('\n=== Cancel Order ===');
const order2 = service.placeOrder(alice, [new OrderItem(mouse, 5)]);
console.log(`Mouse stock after order2: ${mouse.stock}`);
service.cancelOrder(order2.id);
console.log(`Mouse stock after cancel: ${mouse.stock}`);
console.log(`Order2 status: ${order2.status}`);
console.log('\n=== Invalid Transition ===');
try {
service.updateStatus(order1.id, OrderStatus.PLACED);
} catch (e) {
console.log(`Caught: ${e.message}`);
}
console.log('\n=== Cannot Cancel Delivered ===');
try {
service.cancelOrder(order1.id);
} catch (e) {
console.log(`Caught: ${e.message}`);
}
console.log('\n=== Orders by Customer ===');
for (const o of service.getOrdersByCustomer('C-1')) {
console.log(` ${o.toString()}`);
}
console.log('\n=== Switch to No Discount ===');
service.setDiscountStrategy(new NoDiscount());
const order3 = service.placeOrder(alice, [new OrderItem(keyboard, 1)]);
console.log(`Order3 (no discount): ${order3.toString()}`);
}
main();
Follow-up Questions
-
How would you integrate payment processing? Add a PaymentService with a PaymentStrategy (CreditCard, UPI, Wallet). Order transitions to CONFIRMED only after payment succeeds. Handle payment timeouts with a retry mechanism or mark order as PAYMENT_PENDING.
-
How would you implement the Saga pattern for distributed transactions? When order placement spans multiple services (inventory, payment, shipping), use a Saga orchestrator that executes compensating transactions on failure. If payment fails after stock deduction, the saga triggers stock restoration.
-
How would you handle order retry on payment failure? Implement an idempotency key per order attempt. Store payment intent with retry count and exponential backoff. After max retries, transition to PAYMENT_FAILED state and restore stock.
-
How would you add support for partial cancellation (cancel individual items)? Add an OrderItem-level status. On partial cancel, restore stock for cancelled items, recalculate total. Order remains active if any items are still valid.
-
How would you implement real-time order tracking? Observer pattern with WebSocket push. Add location events (picked up, in transit, out for delivery) as sub-states of SHIPPED. A TrackingObserver pushes updates to connected clients.
-
How would you handle flash sale concurrency (1000 orders for 10 items)? Optimistic locking with version numbers on stock, or a queue-based approach where orders are serialized. Use a distributed lock (Redis) for cross-instance coordination. Return βsold outβ immediately once stock hits zero.
-
How would you add order history and audit logging? Event sourcing - store every state change as an immutable event (OrderPlaced, OrderConfirmed, etc.). Replay events to rebuild current state. Use a separate read model for query performance.
Related Concepts
Scale this design past a single process and these are the concepts it runs into:
- Event Sourcing & CQRS β β the pageβs own follow-up answer: store every status change as an immutable event and replay to rebuild state
- Distributed Locking β β the Redis lock the flash-sale follow-up calls for, coordinating stock across instances
- Transactions & Isolation Levels β β optimistic locking with a version column on stock is an isolation-level decision
- Saga Pattern β β once order, payment and shipping own separate data, compensations replace the single rollback
- Idempotency β β a retried place-order must deduct stock once, however many times the client sends it
Discussion
Newest first