Order Lifecycle & Cancel Rules
Representing stages with an `OrderStatus` enum and giant `switch (status_)` statements scattered across every method (`accept`, `startCooking`, `pickUp`, `deliver`, `cancel`) allows illegal transitions (e.g. cancelling an order already out for delivery or delivering an unaccepted order). Adding a new status forces risky edits across half a dozen switch blocks.
Adding a single new state (QualityChecked) forces developers to edit 5 separate switch statements across NaiveOrder.cpp. Forgetting a single case leaves an order stuck in an invalid state.
Allow an object to alter its behavior when its internal state changes, appearing as if the object changed its class.
We replace the OrderStatus enum and switch statements with an OrderState polymorphic interface. Each lifecycle stage (PlacedState, AcceptedState, CookingState, PickedUpState, DeliveredState, CancelledState) is its own class encapsulating its valid transitions. Order delegates all action calls to state_->action(*this), letting states transition the context cleanly.
In Sprint 3 (A3), we decoupled order event broadcasts via the Observer pattern so the kitchen, riders, and customer apps receive real-time notifications.
We need to manage the order's entire lifecycle progression: Placed → Accepted → Cooking → PickedUp (Out for Delivery) → Delivered, while enforcing business rules such as allowing cancellation with refund only before cooking begins.
Representing stages with an OrderStatus enum and giant switch (status_) statements scattered across every method (accept, startCooking, pickUp, deliver, cancel) allows illegal transitions (e.g. cancelling an order already out for delivery or delivering an unaccepted order). Adding a new status forces risky edits across half a dozen switch blocks.
Encapsulating lifecycle rules inside concrete OrderState subclasses (or modern std::variant states) makes illegal transitions unrepresentable and isolates state-specific behavior. Next in Sprint 5 (A5 · Factory Method), once cooking completes and an order enters pickup, we must dispatch the right vehicle type (ExpressBikePartner, EcoBatchPartner, DronePartner) without scattering new across our codebase.
The product ticket
Orders progress through a strict multi-stage lifecycle: Placed → Accepted → Cooking → PickedUp → Delivered. Customer cancellation is permitted during early stages (Placed and Accepted), but must be strictly forbidden once food preparation or delivery has started.
- •
Enforce valid forward transitions: an order cannot be picked up before cooking starts, nor delivered before pickup.
- •
Allow customer cancellation in
PlacedandAcceptedstages with full refund. - •
Reject cancellation attempts once the order reaches
Cooking,PickedUp, orDeliveredstages.
Operations is adding new intermediate stages next quarter (QualityChecked and Delayed) with customized retry logic and notification timeouts. In the naive codebase, every new state requires hunting down and updating half a dozen switch (status_) blocks across the codebase.
Step 1 — The naive code & why it breaks
The order holds an OrderStatus enum, and every lifecycle action (accept(), startCooking(), pickUp(), deliver(), cancel()) executes a giant switch (status_) statement.
#include <print>
#include <string>
enum class OrderStatus {
Placed,
Accepted,
Cooking,
PickedUp,
Delivered,
Cancelled
};
// Naive Order: every lifecycle method contains a switch over OrderStatus.
// Adding a new state or transition rule requires editing every single method!
class NaiveOrder {
std::string orderId_;
OrderStatus status_{OrderStatus::Placed};
std::string riderId_;
public:
explicit NaiveOrder(std::string id) : orderId_(std::move(id)) {}
bool accept() {
switch (status_) {
case OrderStatus::Placed:
status_ = OrderStatus::Accepted;
std::println("[{}] Restaurant accepted the order.", orderId_);
return true;
default:
std::println("[{}] Cannot accept in current state.", orderId_);
return false;
}
}
bool startCooking() {
switch (status_) {
case OrderStatus::Accepted:
status_ = OrderStatus::Cooking;
std::println("[{}] Kitchen started cooking.", orderId_);
return true;
default:
std::println("[{}] Cannot start cooking unless accepted.", orderId_);
return false;
}
}
bool pickUp(const std::string& riderId) {
switch (status_) {
case OrderStatus::Cooking:
status_ = OrderStatus::PickedUp;
riderId_ = riderId;
std::println("[{}] Rider {} picked up the order.", orderId_, riderId_);
return true;
default:
std::println("[{}] Cannot pick up order in current state.", orderId_);
return false;
}
}
bool deliver() {
switch (status_) {
case OrderStatus::PickedUp:
status_ = OrderStatus::Delivered;
std::println("[{}] Delivered to customer successfully!", orderId_);
return true;
default:
std::println("[{}] Cannot deliver order unless picked up.", orderId_);
return false;
}
}
bool cancel() {
switch (status_) {
case OrderStatus::Placed:
case OrderStatus::Accepted:
status_ = OrderStatus::Cancelled;
std::println("[{}] Order cancelled. Full refund issued.", orderId_);
return true;
case OrderStatus::Cooking:
std::println("[{}] Cannot cancel: Kitchen is actively preparing food!", orderId_);
return false;
case OrderStatus::PickedUp:
std::println("[{}] Cannot cancel: Rider is already en route!", orderId_);
return false;
case OrderStatus::Delivered:
std::println("[{}] Cannot cancel: Already delivered.", orderId_);
return false;
case OrderStatus::Cancelled:
std::println("[{}] Already cancelled.", orderId_);
return false;
}
return false;
}
[[nodiscard]] OrderStatus status() const { return status_; }
};
int main() {
NaiveOrder order("ORD-401");
order.accept();
order.startCooking();
// Cancellation should fail now because it is already cooking!
order.cancel();
order.pickUp("RIDER-7");
order.deliver();
}Adding a single new state (QualityChecked) forces developers to edit 5 separate switch statements across NaiveOrder.cpp. Forgetting a single case leaves an order stuck in an invalid state.
To understand all the behaviors of CookingState, you cannot look at one place. You must read accept(), startCooking(), pickUp(), and cancel() to piece together what Cooking allows and forbids.
Fields like riderId_ (only needed after pickup) or cancellationReason_ are dumped into NaiveOrder as nullable members, bloating the class and weakening invariants.
Step 2 — The fix: State Pattern
Allow an object to alter its behavior when its internal state changes, appearing as if the object changed its class.
We replace the OrderStatus enum and switch statements with an OrderState polymorphic interface. Each lifecycle stage (PlacedState, AcceptedState, CookingState, PickedUpState, DeliveredState, CancelledState) is its own class encapsulating its valid transitions. Order delegates all action calls to state_->action(*this), letting states transition the context cleanly.
OrderState — declares methods for lifecycle transitions (accept, startCooking, pickUp, deliver, cancel) with safe default rejection handlers.
PlacedState, AcceptedState, CookingState, PickedUpState, DeliveredState, CancelledState — encapsulate the valid actions and next state transitions for each phase.
Order — owns std::unique_ptr<OrderState> state_ and delegates lifecycle actions to the current state.
Unlock Full UML Diagram & C++23 Solution
The interactive UML architecture diagrams, refactored C++23 code, real-world extensions, and interview Q&A are part of Brute2Best Pro. All 7 foundation chapters are 100% free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.