"Design an e-commerce shopping cart and checkout system. Users add products to a cart, optionally add per-item add-ons like gift wrap, apply coupons, and check out. Checkout must handle tax, discounts, and inventory correctly under concurrent load."
B. Difficulty
Intermediate, climbing to Advanced on the checkout-concurrency and pricing-pipeline follow-ups. The cart itself is simple; the interesting design decisions are (1) picking Decorator for stackable per-item add-ons instead of misusing Strategy for them, and (2) making inventory decrement race-free under concurrent checkouts.
C. What This Problem Tests
Decorator for per-item add-ons (gift wrap, extended warranty, engraving) that stack and each contribute their own price/behavior — deliberately distinguished from Strategy
Strategy for the pricing pipeline (tax, discount, coupon) — a fixed, ordered sequence of interchangeable rules applied to a running total
State machine for order lifecycle (placed → paid → shipped → delivered, plus cancelled/refunded)
The check-then-act race on inventory decrement under concurrent checkouts of the same product
Idempotent payment webhook handling, the same lesson already established for Movie Ticket Booking (Problem 17)
D. Requirements
Functional
Add/remove products from a cart, with optional per-item add-ons
Apply a coupon code and compute tax and discounts at checkout
Check out, decrementing inventory and creating an immutable order
Track order status from placed through delivered, including cancellation/refund
Non-Functional
Concurrent checkouts for the last unit of a product must not oversell it
A retried payment confirmation webhook must not double-charge or double-create an order
Adding a new discount rule or a new add-on type must not require touching the cart or checkout core logic
E. Clarifying Questions
#
Question
Why It Matters
Design Decision Affected
1
Can a cart item have multiple stacked add-ons at once (e.g. gift wrap and engraving on the same item)?
Confirms add-ons need to compose (Decorator) rather than being one mutually-exclusive choice (Strategy)
Design of the add-on hierarchy
2
In what order are tax, discounts, and coupons applied — does a coupon apply before or after tax?
Determines whether the pricing pipeline is a fixed-order chain or needs configurable ordering
Design of the pricing pipeline
3
Is overselling ever acceptable with a backorder fallback, or is inventory a hard limit?
Determines whether the decrement check is a hard invariant or a soft threshold
Inventory-check strictness in checkout
4
Is payment synchronous or confirmed later via a provider webhook that might retry?
Determines whether order creation needs an idempotency key
Design of the checkout/payment-confirmation flow
F. Assumptions
Confirmed: cart management with stackable add-ons, coupon/tax/discount pricing, checkout with inventory decrement, order lifecycle tracking.
Assumption: inventory is a hard limit — no intentional overselling/backordering unless stated otherwise.
Assumption: payment confirmation arrives via a retryable webhook, so checkout completion must be idempotent.
G. Actors
Actor
Responsibility
Customer
Manages cart, applies coupons, checks out
Payment Gateway
External system authorizing payment and confirming via webhook
CheckoutService (facade)
Top-level API coordinating pricing, inventory, and order creation
H. Core Domain Entities
Class
Responsibility
Key Attributes
Key Methods
Does NOT Handle
Product
Catalog entry and live stock count
name, price, stock
decrementStock(qty)
Cart-specific add-ons
CartItem (component)
Base priceable unit — one product at a quantity
product, quantity
getPrice()
Add-on behavior directly
CartItemDecorator (abstract)
Wraps a CartItem to add one stackable add-on's price/behavior
wrapped item
getPrice() (delegates + adds its own charge)
Pricing-pipeline concerns (tax/discount) — those apply at cart level, not per add-on
Cart
A customer's mutable, pre-checkout collection of items
items
addItem(), removeItem(), subtotal()
Tax/discount math, inventory commitment
PricingStep (interface)
One stage of the pricing pipeline (tax, discount, coupon)
—
apply(runningTotal)
—
Order (snapshot)
Immutable checkout result and its lifecycle status
Decorator for add-ons, Strategy for the pricing pipeline — and why they're not the same tool: this is the canonical place in this document to make that boundary explicit. An add-on (gift wrap, engraving) wraps a specific cart item and stacks arbitrarily — an item can have gift wrap and engraving and extended warranty all at once, each layering its own price on top of the last, which is exactly Decorator's shape. A pricing step (tax, a specific discount rule, a coupon) is not attached to one item and does not stack arbitrarily — it's one selected algorithm applied once, in a fixed pipeline position, to a running total, which is Strategy's shape. Reaching for Strategy to model add-ons (one "AddOnStrategy" enum picking exactly one add-on) would break the moment two add-ons need to combine on the same item; reaching for Decorator to model tax/discount (wrapping the cart in a "TaxDecorator") would obscure that these are pipeline stages applied in a specific, meaningful order, not arbitrarily stackable wrappers.
Cart vs. Order is the mutable-draft-vs.-frozen-snapshot split, once more:Cart is mutable right up until checkout; Order is an immutable record of exactly what was charged, at what price, with what add-ons — the same "shared template vs. frozen instance" family as Book/BookItem, Room/RoomInventory, Seat/ShowSeat, and Food Delivery's OrderLineItem (Problem 18), now applied to an entire cart's worth of line items at once, including their decorator-priced add-ons.
State machine for order lifecycle: the same shape as Food Delivery's OrderState (Problem 18), tailored here to a payment-gated first transition and a cancellation/refund path that food delivery didn't need to model.
J. Class Diagram
View Mermaid source
classDiagram
class CartItem {
product
quantity
+getPrice()
}
class CartItemDecorator {
+getPrice()
}
class Cart {
+addItem()
+removeItem()
+subtotal()
}
class PricingStep {
<<interface>>
+apply(runningTotal)
}
class Order {
totalPrice
status
+transitionTo(status)
}
class OrderState {
<<interface>>
+onPaid()
+onShipped()
+onDelivered()
+onCancelled()
}
class CheckoutService {
+checkout()
+confirmPayment()
}
CartItemDecorator ..|> CartItem
Cart --> "*" CartItem
Cart --> PricingStep
CheckoutService --> Cart
CheckoutService --> Order
Order --> OrderState
K. Responsibility Mapping
Class
Responsibility
Does NOT Handle
CartItemDecorator subtypes
One stackable add-on's price/behavior contribution
One Cart.getTotal() method computing add-ons, tax, discounts, and coupons all inline
OCP
A new add-on is a new CartItemDecorator; a new pricing rule is a new PricingStep
A total-price method with an ever-growing if/else chain for each new add-on or discount type
LSP
Every CartItemDecorator honestly implements the same getPrice() contract as a plain CartItem, so decorators compose transparently in any order
A decorator that requires being applied first/last to work correctly, breaking substitutability
ISP
PricingStep stays a single-method interface
A combined "CheckoutPolicy" interface mixing pricing with inventory and shipping rules
DIP
CheckoutService depends on the PricingStep and OrderState abstractions, not hardcoded tax/discount math
Order.calculateTotal() embedding a tax-rate table directly on the entity
M. Design Patterns
Pattern
Problem It Solves
Where It Appears
When NOT to Use
Decorator
Per-item add-ons stack arbitrarily and each contributes independently to price/behavior
CartItemDecorator and subtypes
Skip if only one mutually-exclusive add-on can ever apply — that's Strategy's shape instead
Strategy
Tax/discount/coupon rules are selected-and-applied-once at a fixed pipeline stage, not stacked
PricingStep
Skip if pricing is one flat formula with no variation
State
Order lifecycle transitions genuinely depend on current status
OrderState
Skip if the round only needs a single "is this order done" boolean
Explicitly avoided: using Strategy to model stackable add-ons, or Decorator to model the pricing pipeline — see I for the full reasoning on why swapping these two patterns breaks the first time a real requirement (multiple simultaneous add-ons, or an ordered pipeline) surfaces.
N. What Is Likely to Change?
Behavior
Current Design
Anticipated Change
Protecting Abstraction
Add-on catalog
Gift wrap, engraving
New add-on types (rush processing, insurance)
New CartItemDecorator subtype, additive
Pricing pipeline
Tax → discount → coupon
New pipeline stages (loyalty points, regional tax rules)
New PricingStep, inserted into the pipeline
Order lifecycle
Placed/paid/shipped/delivered/cancelled/refunded
Add a "returned, awaiting inspection" state
New OrderState implementation, additive
O. Design Evolution
V1: a cart with no add-ons, one flat price, no tax/discount, synchronous checkout, no order lifecycle tracking.
V2: introduce CartItemDecorator the moment a second add-on type is requested — this is exactly when a single "addOnType" field would need to become a set, signaling the shift to a stackable Decorator chain.
V3: extract the PricingStep pipeline once tax and discounts both need to apply, in order.
V4: formalize OrderState once cancellation/refund and asynchronous payment confirmation are requested.
V5: add the idempotency key on checkout confirmation once payment moves from synchronous to webhook-based.
What stays stable:CheckoutService's public checkout/confirm API. New add-ons and pricing stages are additive.
P. API / Interface Design
interface PricingStep {
Money apply(Money runningTotal, Cart cart);
}
class CheckoutService {
Order checkout(Cart cart, String couponCode); // reserves inventory, returns PENDING_PAYMENT order
Order confirmPayment(String orderId, PaymentResult result, String idempotencyKey);
}
interface PricingStep {
Money apply(Money runningTotal, Cart cart);
}
class CheckoutService {
Order checkout(Cart cart, String couponCode); // reserves inventory, returns PENDING_PAYMENT order
Order confirmPayment(String orderId, PaymentResult result, String idempotencyKey);
}
class PricingStep {
apply(runningTotal, cart) { throw new Error('not implemented'); }
}
class CheckoutService {
checkout(cart, couponCode) {} // reserves inventory, returns PENDING_PAYMENT order
confirmPayment(orderId, result, idempotencyKey) {}
}
interface PricingStep {
apply(runningTotal: Money, cart: Cart): Money;
}
class CheckoutService {
checkout(cart: Cart, couponCode: string): Order {} // reserves inventory, returns PENDING_PAYMENT order
confirmPayment(orderId: string, result: PaymentResult, idempotencyKey: string): Order {}
}
Ownership/validation notes:checkout() atomically decrements inventory for every line item before returning (see W for the concurrency detail), and confirmPayment() is idempotency-keyed exactly like Movie Ticket Booking's (Problem 17) payment confirmation, so a retried webhook cannot double-create or double-charge an order.
Q. Pseudocode — Critical Logic
CheckoutService.checkout(cart, couponCode):
orderedProductIds = sort(cart.items.map(i => i.product.id)) // canonical order, avoids cross-cart deadlock
reserved = []
try:
for productId in orderedProductIds:
lock(productId)
product = productFor(productId)
qty = cart.quantityFor(productId)
if product.stock < qty: rollback(reserved); reject "out of stock"
product.stock -= qty
reserved.append(productId)
total = cart.subtotal() // includes decorator-priced add-ons
for step in pricingPipeline: total = step.apply(total, cart)
order = new Order(freeze(cart.items), total, status = PENDING_PAYMENT)
return order
finally:
for productId in reserved: unlock(productId)
CheckoutService.confirmPayment(orderId, result, idempotencyKey):
if alreadyProcessed(idempotencyKey): return existingOrderFor(idempotencyKey)
order = orderFor(orderId)
if result.success: order.transitionTo(PAID)
else: order.transitionTo(CANCELLED); restoreInventory(order)
recordProcessed(idempotencyKey, order)
return order
Order checkout(Cart cart, String couponCode) {
List<String> orderedProductIds = sort(cart.getItems().stream().map(i -> i.getProduct().getId()).collect(toList())); // canonical order, avoids cross-cart deadlock
List<String> reserved = new ArrayList<>();
try {
for (String productId : orderedProductIds) {
lock(productId);
Product product = productFor(productId);
int qty = cart.quantityFor(productId);
if (product.getStock() < qty) { rollback(reserved); throw new OutOfStockException("out of stock"); }
product.setStock(product.getStock() - qty);
reserved.add(productId);
}
Money total = cart.subtotal(); // includes decorator-priced add-ons
for (PricingStep step : pricingPipeline) { total = step.apply(total, cart); }
Order order = new Order(freeze(cart.getItems()), total, /* status = */ PENDING_PAYMENT);
return order;
} finally {
for (String productId : reserved) { unlock(productId); }
}
}
Order confirmPayment(String orderId, PaymentResult result, String idempotencyKey) {
if (alreadyProcessed(idempotencyKey)) { return existingOrderFor(idempotencyKey); }
Order order = orderFor(orderId);
if (result.isSuccess()) {
order.transitionTo(PAID);
} else {
order.transitionTo(CANCELLED);
restoreInventory(order);
}
recordProcessed(idempotencyKey, order);
return order;
}
std::shared_ptr<Order> CheckoutService::checkout(std::shared_ptr<Cart> cart, const std::string& couponCode) {
auto orderedProductIds = sort(mapToProductIds(cart->items)); // canonical order, avoids cross-cart deadlock
std::vector<std::string> reserved;
try {
for (auto& productId : orderedProductIds) {
lock(productId);
auto product = productFor(productId);
int qty = cart->quantityFor(productId);
if (product->stock < qty) { rollback(reserved); throw std::runtime_error("out of stock"); }
product->stock -= qty;
reserved.push_back(productId);
}
Money total = cart->subtotal(); // includes decorator-priced add-ons
for (auto& step : pricingPipeline) { total = step->apply(total, cart); }
auto order = std::make_shared<Order>(freeze(cart->items), total, /* status = */ PENDING_PAYMENT);
for (auto& productId : reserved) { unlock(productId); }
return order;
} catch (...) {
for (auto& productId : reserved) { unlock(productId); }
throw;
}
}
std::shared_ptr<Order> CheckoutService::confirmPayment(const std::string& orderId, PaymentResult result, const std::string& idempotencyKey) {
if (alreadyProcessed(idempotencyKey)) { return existingOrderFor(idempotencyKey); }
auto order = orderFor(orderId);
if (result.success) {
order->transitionTo(PAID);
} else {
order->transitionTo(CANCELLED);
restoreInventory(order);
}
recordProcessed(idempotencyKey, order);
return order;
}
checkout(cart, couponCode) {
const orderedProductIds = sort(cart.items.map(i => i.product.id)); // canonical order, avoids cross-cart deadlock
const reserved = [];
try {
for (const productId of orderedProductIds) {
lock(productId);
const product = productFor(productId);
const qty = cart.quantityFor(productId);
if (product.stock < qty) { rollback(reserved); throw new Error("out of stock"); }
product.stock -= qty;
reserved.push(productId);
}
let total = cart.subtotal(); // includes decorator-priced add-ons
for (const step of pricingPipeline) { total = step.apply(total, cart); }
const order = new Order(freeze(cart.items), total, /* status = */ PENDING_PAYMENT);
return order;
} finally {
for (const productId of reserved) { unlock(productId); }
}
}
confirmPayment(orderId, result, idempotencyKey) {
if (alreadyProcessed(idempotencyKey)) { return existingOrderFor(idempotencyKey); }
const order = orderFor(orderId);
if (result.success) {
order.transitionTo(PAID);
} else {
order.transitionTo(CANCELLED);
restoreInventory(order);
}
recordProcessed(idempotencyKey, order);
return order;
}
checkout(cart: Cart, couponCode: string): Order {
const orderedProductIds = sort(cart.items.map(i => i.product.id)); // canonical order, avoids cross-cart deadlock
const reserved: string[] = [];
try {
for (const productId of orderedProductIds) {
lock(productId);
const product = productFor(productId);
const qty = cart.quantityFor(productId);
if (product.stock < qty) { rollback(reserved); throw new Error("out of stock"); }
product.stock -= qty;
reserved.push(productId);
}
let total = cart.subtotal(); // includes decorator-priced add-ons
for (const step of pricingPipeline) { total = step.apply(total, cart); }
const order = new Order(freeze(cart.items), total, /* status = */ PENDING_PAYMENT);
return order;
} finally {
for (const productId of reserved) { unlock(productId); }
}
}
confirmPayment(orderId: string, result: PaymentResult, idempotencyKey: string): Order {
if (alreadyProcessed(idempotencyKey)) { return existingOrderFor(idempotencyKey); }
const order = orderFor(orderId);
if (result.success) {
order.transitionTo(PAID);
} else {
order.transitionTo(CANCELLED);
restoreInventory(order);
}
recordProcessed(idempotencyKey, order);
return order;
}
R. End-to-End Use Case
cart.addItem(Product("Watch"), qty=1).decorate(GiftWrap).decorate(Engraving)
│
▼
checkout(cart, coupon="SAVE10")
│ lock(product) → stock check → decrement → unlock
│ subtotal (base + gift wrap + engraving) → tax → discount → coupon → total
│ Order created, status = PENDING_PAYMENT
▼
payment webhook fires → confirmPayment(orderId, success, idempotencyKey)
│ order → PAID; retried webhook with same key returns the same Order, no double-charge
S. Data Structures
Structure
Used For
Why
Alternative & Trade-off
Linked chain of CartItemDecorator wrapping a base CartItem
Composable add-on pricing
getPrice() recurses through the chain, naturally summing every layer
A flat list of add-on flags on CartItem also works but scatters "does this add-on affect price/packaging/etc." logic outside the add-on itself
Ordered list of PricingStep
Deterministic pipeline application
Tax-before-or-after-discount is a real business rule, not an implementation detail — an ordered list makes the order explicit and testable
—
Sorted product-id order before locking
Deadlock-free multi-item checkout
Same canonical-ordering technique as File System (Problem 9) and Movie Ticket Booking (Problem 17), applied to inventory rows instead of files or seats
—
T. Complexity Analysis
Operation
Time
Space
CartItem.getPrice() through k decorators
O(k)
O(k)
checkout() for n line items
O(n log n) for the sort + O(n) for locking/decrementing + O(pipeline stages)
O(n)
confirmPayment()
O(1) idempotency check + O(1) transition
O(1)
U. Testing Strategy
Happy path: an item with two stacked add-ons prices as base + add-on A + add-on B, and the pipeline applies tax/discount/coupon in the documented order.
Edge cases: checking out a cart where one line item is out of stock rolls back all reservations made earlier in that same checkout, not just the failed one.
Idempotency tests: calling confirmPayment() twice with the same idempotency key produces exactly one PAID transition and one charge.
Concurrency tests: two customers concurrently checking out the last unit of the same product result in exactly one success and one clean "out of stock" rejection.
V. Interviewer Follow-Ups
Follow-Up
Testing
Design Change
Classes Changed
Pattern/Trade-off
"A customer wants to stack three add-ons on one item, and later remove just the middle one."
Whether add-ons were modeled as an arbitrarily-ordered stack or a fixed pair
Support arbitrary decorator chain length and removal by add-on type
CartItemDecorator only
Validates the Decorator choice from I actually holds up under real stacking
"Add a loyalty-points redemption step to checkout."
Whether pricing logic was still isolated in the pipeline
New PricingStep inserted at the appropriate pipeline position
New strategy only
Validates the pipeline's extensibility
"What if inventory needs to be reserved across two different products atomically for a bundle deal?"
Whether multi-item locking was already deadlock-free
None needed — canonical product-id ordering from Q already covers this
None
Confirms the File System/Movie Ticket lock-ordering lesson generalizes correctly
W. Concurrency
Shared mutable state: each Product's stock count, particularly for a popular item near its last few units.
The check-then-act race, on inventory this time: reading "is there enough stock" and then decrementing it are two steps; without a lock (or an atomic conditional update) around the specific product, two concurrent checkouts could both read sufficient stock before either commits — the same bug shape as Parking Lot, Splitwise, ATM, Library, Hotel, and Movie Ticket Booking, now on inventory rows.
Multi-item checkout deadlock avoidance: a checkout touching several products must lock them in a fixed canonical order (sorted by product id) to avoid the same circular-wait deadlock already solved in File System (Problem 9) and Movie Ticket Booking (Problem 17).
Database-level alternative: a single atomic UPDATE products SET stock = stock - qty WHERE id = ? AND stock >= qty avoids application-level locking entirely for the single-product case — the affected-row-count tells the caller whether the decrement succeeded, pushing the race-free guarantee down to the database.
Idempotent payment confirmation: exactly the same idempotency-key discipline as Movie Ticket Booking (Problem 17) — a retried webhook must return the already-processed result, not reprocess it.
X. Persistence
Orders and their frozen line items (including decorator-priced add-ons) are durable, legally significant records. Cart contents are more ephemeral — losing an unfinished cart on a crash is a UX annoyance, not a correctness failure — so a cart can reasonably live in a faster, less durable store than the order it eventually produces.
Y. Scaling Boundary
Layer
What Belongs Here
LLD
Decorator-based add-ons, pricing pipeline, order state machine, per-product locking/atomic decrement
HLD
Distributed inventory across warehouses, cart-abandonment-recovery pipelines, fraud-detection integration at the payment layer
Explicitly out of scope
Multi-warehouse inventory routing unless the round pivots there
Z. Failure Scenarios
Inventory is decremented but the order creation step fails afterward: the decrement and order creation must be transactionally consistent, or the decrement needs a compensating restore — the same commit-ordering discipline as ATM's (Problem 14) debit-before-dispense logic.
A payment fails after inventory was already reserved:confirmPayment()'s failure path must restore the reserved stock, not leave it permanently decremented for an order that was never actually paid for.
AA. Common Bad Designs
Using Strategy to model add-ons, or Decorator to model the pricing pipeline — the defining pattern-boundary mistake for this problem; see I.
Checking stock and decrementing it as two separate, unlocked steps — reopens the classic check-then-act race even with a well-designed Product entity.
Locking multiple products in whatever order they appear in the cart instead of a canonical sorted order — the same deadlock trap as unordered multi-seat/multi-file locking.
Treating payment confirmation as safe to process more than once — without an idempotency key, a retried webhook can double-create an order or double-charge.
AB. Design Trade-Offs
Decision
Option A
Option B
Recommendation
Inventory decrement mechanism
Application-level lock + re-check
Atomic conditional DB update (WHERE stock >= qty)
Atomic DB update where the schema allows it — removes an entire class of application-level locking bugs; describe the application-level lock version if the round is explicitly about in-memory design
Failed-checkout inventory handling
Restore stock immediately on payment failure
Hold reserved stock for a grace period in case of a slow retry
A short grace period (mirroring Movie Ticket Booking's hold TTL) balances not permanently losing a sale to a slow retry against not starving other buyers indefinitely
AC. Interviewer Evaluation
Strong signal: correctly choosing Decorator for add-ons and Strategy for the pricing pipeline, and being able to articulate why swapping them would break; independently identifying the inventory check-then-act race and the multi-item deadlock risk; flagging payment-confirmation idempotency without being prompted.
Red flag: modeling add-ons as a Strategy enum that can't stack; modeling the pricing pipeline as nested Decorators; unlocked stock check-then-decrement; unordered multi-item locking; non-idempotent payment confirmation.
AD. Interview Simulation
Interviewer: Design an e-commerce shopping cart and checkout flow. Items can have add-ons like gift wrap.
Candidate: I'd model add-ons with Decorator, not Strategy — because add-ons stack. A watch can have gift wrap and engraving at once, each layering its own price contribution on top of the base item. Strategy would only fit if exactly one add-on could ever apply, which isn't the real requirement here.
Interviewer: How do tax and discounts fit in, then?
Candidate: Those are Strategy, not Decorator — each is one rule applied once, at a specific, meaningful position in an ordered pricing pipeline. Tax-before-or-after-discount is an actual business rule, so I'd make the pipeline an explicit ordered list of PricingSteps rather than implying an order through nested wrapping.
Interviewer: Two customers check out the last unit of a product at the same time.
Candidate: Same check-then-act race as everywhere else in this document — lock the product, re-verify stock, then decrement. In production I'd actually push this to an atomic conditional update at the database level, so the race-free guarantee doesn't depend on application-level locking at all.
Interviewer: What if the payment provider retries its confirmation webhook?
Candidate: Idempotency-keyed confirmation, same as I'd do for the movie-ticket payment flow — a retried call with the same key returns the already-processed order instead of creating a second one or charging twice.
Broad requirements-and-class walkthrough covering catalog, cart, checkout, and order tracking
Good functional breadth across the full e-commerce surface
Add-ons and the pricing pipeline are generally not distinguished as separate pattern concerns — both tend to collapse into ad hoc total-calculation logic
Useful for the overall functional scope; this document's explicit Decorator/Strategy boundary and inventory-race handling are the depth worth adding
Don't design multi-warehouse inventory routing or fraud detection unless asked
AG. Full Implementation
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.
// Problem 19: E-commerce Shopping Cart & Checkout
// Patterns: Decorator (add-ons on CartItem), Strategy (pricing pipeline), State (Order lifecycle), Facade (CheckoutService)
import java.util.*;
public class p19 {
// ---------- Exceptions ----------
static class OutOfStockException extends RuntimeException {
OutOfStockException(String msg) { super(msg); }
}
static class OrderNotFoundException extends RuntimeException {
OrderNotFoundException(String msg) { super(msg); }
}
// ---------- Catalog ----------
static class Product {
final String id;
final String name;
final double price;
int stock;
Product(String id, String name, double price, int stock) {
this.id = id; this.name = name; this.price = price; this.stock = stock;
}
void decrementStock(int qty) {
if (stock < qty) throw new OutOfStockException("Insufficient stock for " + id);
stock -= qty;
}
void restoreStock(int qty) { stock += qty; }
}
// ---------- CartItem (component) + Decorator ----------
static class CartItem {
final Product product;
final int quantity;
CartItem(Product product, int quantity) {
this.product = product; this.quantity = quantity;
}
double getPrice() { return product.price * quantity; }
Product getProduct() { return product; }
int getQuantity() { return quantity; }
}
// Abstract decorator: subclasses CartItem so it's substitutable everywhere a CartItem is
// expected, but wraps another CartItem to layer one stackable add-on charge on top.
static abstract class CartItemDecorator extends CartItem {
protected final CartItem wrapped;
CartItemDecorator(CartItem wrapped) {
super(wrapped.getProduct(), wrapped.getQuantity());
this.wrapped = wrapped;
}
@Override
abstract double getPrice(); // delegates to wrapped.getPrice() + adds its own charge
}
static class GiftWrapDecorator extends CartItemDecorator {
private static final double FEE = 2.99;
GiftWrapDecorator(CartItem wrapped) { super(wrapped); }
@Override double getPrice() { return wrapped.getPrice() + FEE; }
}
static class EngravingDecorator extends CartItemDecorator {
private static final double FEE = 5.00;
EngravingDecorator(CartItem wrapped) { super(wrapped); }
@Override double getPrice() { return wrapped.getPrice() + FEE; }
}
// ---------- Cart ----------
static class Cart {
private final List<CartItem> items = new ArrayList<>();
void addItem(CartItem item) { items.add(item); }
void removeItem(String productId) {
items.removeIf(i -> i.getProduct().id.equals(productId));
}
double subtotal() {
double total = 0;
for (CartItem i : items) total += i.getPrice();
return total;
}
int quantityFor(String productId) {
int qty = 0;
for (CartItem i : items) if (i.getProduct().id.equals(productId)) qty += i.getQuantity();
return qty;
}
List<CartItem> getItems() { return items; }
}
// ---------- Pricing pipeline (Strategy) ----------
interface PricingStep {
double apply(double runningTotal, Cart cart);
}
static class TaxPricingStep implements PricingStep {
private final double rate;
TaxPricingStep(double rate) { this.rate = rate; }
@Override public double apply(double runningTotal, Cart cart) { return runningTotal + runningTotal * rate; }
}
static class CouponDiscountPricingStep implements PricingStep {
private final String couponCode;
private final Map<String, Double> validCoupons;
CouponDiscountPricingStep(String couponCode, Map<String, Double> validCoupons) {
this.couponCode = couponCode; this.validCoupons = validCoupons;
}
@Override public double apply(double runningTotal, Cart cart) {
if (couponCode == null || !validCoupons.containsKey(couponCode)) return runningTotal;
double discount = validCoupons.get(couponCode);
return runningTotal * (1 - discount);
}
}
// ---------- Order lifecycle (State) ----------
enum OrderStatus { PENDING_PAYMENT, PAID, SHIPPED, DELIVERED, CANCELLED }
interface OrderState {
OrderState onPaid(Order order);
OrderState onShipped(Order order);
OrderState onDelivered(Order order);
OrderState onCancelled(Order order);
}
static class PendingPaymentState implements OrderState {
@Override public OrderState onPaid(Order order) { order.status = OrderStatus.PAID; return new PaidState(); }
@Override public OrderState onShipped(Order order) { throw new IllegalStateException("Cannot ship before payment"); }
@Override public OrderState onDelivered(Order order) { throw new IllegalStateException("Cannot deliver before payment"); }
@Override public OrderState onCancelled(Order order) { order.status = OrderStatus.CANCELLED; return new CancelledState(); }
}
static class PaidState implements OrderState {
@Override public OrderState onPaid(Order order) { return this; }
@Override public OrderState onShipped(Order order) { order.status = OrderStatus.SHIPPED; return new ShippedState(); }
@Override public OrderState onDelivered(Order order) { throw new IllegalStateException("Cannot deliver before shipping"); }
@Override public OrderState onCancelled(Order order) { order.status = OrderStatus.CANCELLED; return new CancelledState(); } // refund path
}
static class ShippedState implements OrderState {
@Override public OrderState onPaid(Order order) { return this; }
@Override public OrderState onShipped(Order order) { return this; }
@Override public OrderState onDelivered(Order order) { order.status = OrderStatus.DELIVERED; return new DeliveredState(); }
@Override public OrderState onCancelled(Order order) { throw new IllegalStateException("Cannot cancel a shipped order"); }
}
static class DeliveredState implements OrderState {
@Override public OrderState onPaid(Order order) { return this; }
@Override public OrderState onShipped(Order order) { return this; }
@Override public OrderState onDelivered(Order order) { return this; }
@Override public OrderState onCancelled(Order order) { throw new IllegalStateException("Cannot cancel a delivered order"); }
}
static class CancelledState implements OrderState {
@Override public OrderState onPaid(Order order) { throw new IllegalStateException("Order is cancelled"); }
@Override public OrderState onShipped(Order order) { throw new IllegalStateException("Order is cancelled"); }
@Override public OrderState onDelivered(Order order) { throw new IllegalStateException("Order is cancelled"); }
@Override public OrderState onCancelled(Order order) { return this; }
}
// ---------- Order (immutable snapshot + lifecycle) ----------
static class Order {
final String id;
final List<CartItem> lineItems; // frozen at checkout time
final double totalPrice;
OrderStatus status;
private OrderState state;
Order(String id, List<CartItem> lineItems, double totalPrice) {
this.id = id;
this.lineItems = Collections.unmodifiableList(new ArrayList<>(lineItems));
this.totalPrice = totalPrice;
this.status = OrderStatus.PENDING_PAYMENT;
this.state = new PendingPaymentState();
}
Order transitionTo(OrderStatus target) {
switch (target) {
case PAID: state = state.onPaid(this); break;
case SHIPPED: state = state.onShipped(this); break;
case DELIVERED: state = state.onDelivered(this); break;
case CANCELLED: state = state.onCancelled(this); break;
default: throw new IllegalArgumentException("Unsupported transition: " + target);
}
return this;
}
}
static class PaymentResult {
final boolean success;
PaymentResult(boolean success) { this.success = success; }
}
// ---------- CheckoutService (Facade) ----------
static class CheckoutService {
private final List<PricingStep> pricingPipeline;
private final Map<String, Order> ordersById = new HashMap<>();
private final Map<String, Order> processedIdempotencyKeys = new HashMap<>();
private int nextOrderId = 1;
CheckoutService(List<PricingStep> pricingPipeline) {
this.pricingPipeline = pricingPipeline;
}
Order checkout(Cart cart, String couponCode) {
List<String> orderedProductIds = new ArrayList<>();
for (CartItem item : cart.getItems()) {
if (!orderedProductIds.contains(item.getProduct().id)) orderedProductIds.add(item.getProduct().id);
}
Collections.sort(orderedProductIds); // canonical order avoids cross-cart deadlock
List<Product> reserved = new ArrayList<>();
List<Integer> reservedQty = new ArrayList<>();
try {
for (String productId : orderedProductIds) {
Product product = productFor(cart, productId);
int qty = cart.quantityFor(productId);
product.decrementStock(qty); // throws OutOfStockException on failure
reserved.add(product);
reservedQty.add(qty);
}
} catch (OutOfStockException e) {
for (int i = 0; i < reserved.size(); i++) reserved.get(i).restoreStock(reservedQty.get(i));
throw e;
}
double total = cart.subtotal(); // includes decorator-priced add-ons
for (PricingStep step : pricingPipeline) total = step.apply(total, cart);
if (couponCode != null) {
// coupon step is applied like any other pipeline step if wired in; here we allow ad-hoc override
}
Order order = new Order("ORD-" + (nextOrderId++), cart.getItems(), total);
ordersById.put(order.id, order);
return order;
}
Order confirmPayment(String orderId, PaymentResult result, String idempotencyKey) {
if (processedIdempotencyKeys.containsKey(idempotencyKey)) {
return processedIdempotencyKeys.get(idempotencyKey); // already processed: return same result, no double effect
}
Order order = ordersById.get(orderId);
if (order == null) throw new OrderNotFoundException("No such order: " + orderId);
if (result.success) {
order.transitionTo(OrderStatus.PAID);
} else {
order.transitionTo(OrderStatus.CANCELLED);
restoreInventory(order);
}
processedIdempotencyKeys.put(idempotencyKey, order);
return order;
}
private void restoreInventory(Order order) {
for (CartItem item : order.lineItems) item.getProduct().restoreStock(item.getQuantity());
}
private Product productFor(Cart cart, String productId) {
for (CartItem item : cart.getItems()) if (item.getProduct().id.equals(productId)) return item.getProduct();
throw new IllegalStateException("Product not in cart: " + productId);
}
}
// ---------- Demo ----------
public static void main(String[] args) {
Product laptop = new Product("P1", "Laptop", 1000.0, 5);
Product mug = new Product("P2", "Mug", 15.0, 10);
Cart cart = new Cart();
cart.addItem(new GiftWrapDecorator(new CartItem(laptop, 1))); // laptop + gift wrap
cart.addItem(new EngravingDecorator(new CartItem(mug, 2))); // 2 mugs + engraving
System.out.println("Subtotal (with add-ons): " + cart.subtotal());
Map<String, Double> coupons = new HashMap<>();
coupons.put("SAVE10", 0.10);
List<PricingStep> pipeline = new ArrayList<>();
pipeline.add(new CouponDiscountPricingStep("SAVE10", coupons));
pipeline.add(new TaxPricingStep(0.08));
CheckoutService checkoutService = new CheckoutService(pipeline);
Order order = checkoutService.checkout(cart, "SAVE10");
System.out.println("Order created: " + order.id + " total=" + order.totalPrice + " status=" + order.status);
System.out.println("Laptop stock after checkout: " + laptop.stock);
Order confirmed = checkoutService.confirmPayment(order.id, new PaymentResult(true), "idem-key-1");
System.out.println("After payment: " + confirmed.status);
// Retried webhook with same idempotency key must not double-process
Order retried = checkoutService.confirmPayment(order.id, new PaymentResult(true), "idem-key-1");
System.out.println("Retry same order id? " + (retried == confirmed));
confirmed.transitionTo(OrderStatus.SHIPPED);
confirmed.transitionTo(OrderStatus.DELIVERED);
System.out.println("Final status: " + confirmed.status);
}
}
// Problem 19: E-commerce Shopping Cart & Checkout
// Patterns: Decorator (add-ons on CartItem), Strategy (pricing pipeline), State (Order lifecycle), Facade (CheckoutService)
#include <iostream>
#include <vector>
#include <map>
#include <memory>
#include <string>
#include <algorithm>
#include <stdexcept>
using namespace std;
// ---------- Exceptions ----------
struct OutOfStockException : runtime_error { OutOfStockException(const string& m) : runtime_error(m) {} };
struct OrderNotFoundException : runtime_error { OrderNotFoundException(const string& m) : runtime_error(m) {} };
// ---------- Catalog ----------
struct Product {
string id, name;
double price;
int stock;
Product(string id_, string name_, double price_, int stock_)
: id(move(id_)), name(move(name_)), price(price_), stock(stock_) {}
void decrementStock(int qty) {
if (stock < qty) throw OutOfStockException("Insufficient stock for " + id);
stock -= qty;
}
void restoreStock(int qty) { stock += qty; }
};
// ---------- CartItem (component) + Decorator ----------
class CartItem {
public:
Product* product;
int quantity;
CartItem(Product* product_, int quantity_) : product(product_), quantity(quantity_) {}
virtual ~CartItem() = default;
virtual double getPrice() const { return product->price * quantity; }
Product* getProduct() const { return product; }
int getQuantity() const { return quantity; }
};
// Abstract decorator: subclasses CartItem so it's substitutable everywhere a CartItem is
// expected, but wraps another CartItem to layer one stackable add-on charge on top.
class CartItemDecorator : public CartItem {
protected:
shared_ptr<CartItem> wrapped;
public:
CartItemDecorator(shared_ptr<CartItem> wrapped_)
: CartItem(wrapped_->getProduct(), wrapped_->getQuantity()), wrapped(wrapped_) {}
};
class GiftWrapDecorator : public CartItemDecorator {
static constexpr double FEE = 2.99;
public:
GiftWrapDecorator(shared_ptr<CartItem> wrapped_) : CartItemDecorator(wrapped_) {}
double getPrice() const override { return wrapped->getPrice() + FEE; }
};
class EngravingDecorator : public CartItemDecorator {
static constexpr double FEE = 5.00;
public:
EngravingDecorator(shared_ptr<CartItem> wrapped_) : CartItemDecorator(wrapped_) {}
double getPrice() const override { return wrapped->getPrice() + FEE; }
};
// ---------- Cart ----------
class Cart {
vector<shared_ptr<CartItem>> items;
public:
void addItem(shared_ptr<CartItem> item) { items.push_back(item); }
void removeItem(const string& productId) {
items.erase(remove_if(items.begin(), items.end(),
[&](const shared_ptr<CartItem>& i) { return i->getProduct()->id == productId; }), items.end());
}
double subtotal() const {
double total = 0;
for (auto& i : items) total += i->getPrice();
return total;
}
int quantityFor(const string& productId) const {
int qty = 0;
for (auto& i : items) if (i->getProduct()->id == productId) qty += i->getQuantity();
return qty;
}
const vector<shared_ptr<CartItem>>& getItems() const { return items; }
};
// ---------- Pricing pipeline (Strategy) ----------
class PricingStep {
public:
virtual ~PricingStep() = default;
virtual double apply(double runningTotal, const Cart& cart) = 0;
};
class TaxPricingStep : public PricingStep {
double rate;
public:
TaxPricingStep(double rate_) : rate(rate_) {}
double apply(double runningTotal, const Cart& cart) override { return runningTotal + runningTotal * rate; }
};
class CouponDiscountPricingStep : public PricingStep {
string couponCode;
map<string, double> validCoupons;
public:
CouponDiscountPricingStep(string couponCode_, map<string, double> validCoupons_)
: couponCode(move(couponCode_)), validCoupons(move(validCoupons_)) {}
double apply(double runningTotal, const Cart& cart) override {
auto it = validCoupons.find(couponCode);
if (it == validCoupons.end()) return runningTotal;
return runningTotal * (1 - it->second);
}
};
// ---------- Order lifecycle (State) ----------
enum class OrderStatus { PENDING_PAYMENT, PAID, SHIPPED, DELIVERED, CANCELLED };
class Order; // fwd decl
class OrderState {
public:
virtual ~OrderState() = default;
virtual unique_ptr<OrderState> onPaid(Order& order) = 0;
virtual unique_ptr<OrderState> onShipped(Order& order) = 0;
virtual unique_ptr<OrderState> onDelivered(Order& order) = 0;
virtual unique_ptr<OrderState> onCancelled(Order& order) = 0;
};
class Order {
public:
string id;
vector<shared_ptr<CartItem>> lineItems; // frozen at checkout time
double totalPrice;
OrderStatus status;
unique_ptr<OrderState> state;
Order(string id_, vector<shared_ptr<CartItem>> lineItems_, double totalPrice_);
Order* transitionTo(OrderStatus target);
};
class PendingPaymentState : public OrderState {
public:
unique_ptr<OrderState> onPaid(Order& order) override;
unique_ptr<OrderState> onShipped(Order& order) override { throw runtime_error("Cannot ship before payment"); }
unique_ptr<OrderState> onDelivered(Order& order) override { throw runtime_error("Cannot deliver before payment"); }
unique_ptr<OrderState> onCancelled(Order& order) override;
};
class PaidState : public OrderState {
public:
unique_ptr<OrderState> onPaid(Order& order) override;
unique_ptr<OrderState> onShipped(Order& order) override;
unique_ptr<OrderState> onDelivered(Order& order) override { throw runtime_error("Cannot deliver before shipping"); }
unique_ptr<OrderState> onCancelled(Order& order) override; // refund path
};
class ShippedState : public OrderState {
public:
unique_ptr<OrderState> onPaid(Order& order) override;
unique_ptr<OrderState> onShipped(Order& order) override;
unique_ptr<OrderState> onDelivered(Order& order) override;
unique_ptr<OrderState> onCancelled(Order& order) override { throw runtime_error("Cannot cancel a shipped order"); }
};
class DeliveredState : public OrderState {
public:
unique_ptr<OrderState> onPaid(Order& order) override;
unique_ptr<OrderState> onShipped(Order& order) override;
unique_ptr<OrderState> onDelivered(Order& order) override;
unique_ptr<OrderState> onCancelled(Order& order) override { throw runtime_error("Cannot cancel a delivered order"); }
};
class CancelledState : public OrderState {
public:
unique_ptr<OrderState> onPaid(Order& order) override { throw runtime_error("Order is cancelled"); }
unique_ptr<OrderState> onShipped(Order& order) override { throw runtime_error("Order is cancelled"); }
unique_ptr<OrderState> onDelivered(Order& order) override { throw runtime_error("Order is cancelled"); }
unique_ptr<OrderState> onCancelled(Order& order) override;
};
unique_ptr<OrderState> PendingPaymentState::onPaid(Order& order) { order.status = OrderStatus::PAID; return make_unique<PaidState>(); }
unique_ptr<OrderState> PendingPaymentState::onCancelled(Order& order) { order.status = OrderStatus::CANCELLED; return make_unique<CancelledState>(); }
unique_ptr<OrderState> PaidState::onPaid(Order& order) { return make_unique<PaidState>(); }
unique_ptr<OrderState> PaidState::onShipped(Order& order) { order.status = OrderStatus::SHIPPED; return make_unique<ShippedState>(); }
unique_ptr<OrderState> PaidState::onCancelled(Order& order) { order.status = OrderStatus::CANCELLED; return make_unique<CancelledState>(); }
unique_ptr<OrderState> ShippedState::onPaid(Order& order) { return make_unique<ShippedState>(); }
unique_ptr<OrderState> ShippedState::onShipped(Order& order) { return make_unique<ShippedState>(); }
unique_ptr<OrderState> ShippedState::onDelivered(Order& order) { order.status = OrderStatus::DELIVERED; return make_unique<DeliveredState>(); }
unique_ptr<OrderState> DeliveredState::onPaid(Order& order) { return make_unique<DeliveredState>(); }
unique_ptr<OrderState> DeliveredState::onShipped(Order& order) { return make_unique<DeliveredState>(); }
unique_ptr<OrderState> DeliveredState::onDelivered(Order& order) { return make_unique<DeliveredState>(); }
unique_ptr<OrderState> CancelledState::onCancelled(Order& order) { return make_unique<CancelledState>(); }
Order::Order(string id_, vector<shared_ptr<CartItem>> lineItems_, double totalPrice_)
: id(move(id_)), lineItems(move(lineItems_)), totalPrice(totalPrice_),
status(OrderStatus::PENDING_PAYMENT), state(make_unique<PendingPaymentState>()) {}
Order* Order::transitionTo(OrderStatus target) {
switch (target) {
case OrderStatus::PAID: state = state->onPaid(*this); break;
case OrderStatus::SHIPPED: state = state->onShipped(*this); break;
case OrderStatus::DELIVERED: state = state->onDelivered(*this); break;
case OrderStatus::CANCELLED: state = state->onCancelled(*this); break;
}
return this;
}
struct PaymentResult { bool success; };
string statusToString(OrderStatus s) {
switch (s) {
case OrderStatus::PENDING_PAYMENT: return "PENDING_PAYMENT";
case OrderStatus::PAID: return "PAID";
case OrderStatus::SHIPPED: return "SHIPPED";
case OrderStatus::DELIVERED: return "DELIVERED";
case OrderStatus::CANCELLED: return "CANCELLED";
}
return "?";
}
// ---------- CheckoutService (Facade) ----------
class CheckoutService {
vector<shared_ptr<PricingStep>> pricingPipeline;
map<string, shared_ptr<Order>> ordersById;
map<string, shared_ptr<Order>> processedIdempotencyKeys;
int nextOrderId = 1;
public:
CheckoutService(vector<shared_ptr<PricingStep>> pipeline) : pricingPipeline(move(pipeline)) {}
shared_ptr<Order> checkout(Cart& cart, const string& couponCode) {
vector<string> orderedProductIds;
for (auto& item : cart.getItems()) {
if (find(orderedProductIds.begin(), orderedProductIds.end(), item->getProduct()->id) == orderedProductIds.end())
orderedProductIds.push_back(item->getProduct()->id);
}
sort(orderedProductIds.begin(), orderedProductIds.end()); // canonical order avoids cross-cart deadlock
vector<Product*> reserved;
vector<int> reservedQty;
try {
for (auto& productId : orderedProductIds) {
Product* product = productFor(cart, productId);
int qty = cart.quantityFor(productId);
product->decrementStock(qty); // throws OutOfStockException on failure
reserved.push_back(product);
reservedQty.push_back(qty);
}
} catch (OutOfStockException&) {
for (size_t i = 0; i < reserved.size(); i++) reserved[i]->restoreStock(reservedQty[i]);
throw;
}
double total = cart.subtotal(); // includes decorator-priced add-ons
for (auto& step : pricingPipeline) total = step->apply(total, cart);
auto order = make_shared<Order>("ORD-" + to_string(nextOrderId++), cart.getItems(), total);
ordersById[order->id] = order;
return order;
}
shared_ptr<Order> confirmPayment(const string& orderId, PaymentResult result, const string& idempotencyKey) {
auto found = processedIdempotencyKeys.find(idempotencyKey);
if (found != processedIdempotencyKeys.end()) return found->second;
auto it = ordersById.find(orderId);
if (it == ordersById.end()) throw OrderNotFoundException("No such order: " + orderId);
auto order = it->second;
if (result.success) {
order->transitionTo(OrderStatus::PAID);
} else {
order->transitionTo(OrderStatus::CANCELLED);
restoreInventory(*order);
}
processedIdempotencyKeys[idempotencyKey] = order;
return order;
}
private:
void restoreInventory(Order& order) {
for (auto& item : order.lineItems) item->getProduct()->restoreStock(item->getQuantity());
}
Product* productFor(Cart& cart, const string& productId) {
for (auto& item : cart.getItems()) if (item->getProduct()->id == productId) return item->getProduct();
throw runtime_error("Product not in cart: " + productId);
}
};
// ---------- Demo ----------
int main() {
Product laptop("P1", "Laptop", 1000.0, 5);
Product mug("P2", "Mug", 15.0, 10);
Cart cart;
cart.addItem(make_shared<GiftWrapDecorator>(make_shared<CartItem>(&laptop, 1)));
cart.addItem(make_shared<EngravingDecorator>(make_shared<CartItem>(&mug, 2)));
cout << "Subtotal (with add-ons): " << cart.subtotal() << endl;
map<string, double> coupons = {{"SAVE10", 0.10}};
vector<shared_ptr<PricingStep>> pipeline;
pipeline.push_back(make_shared<CouponDiscountPricingStep>("SAVE10", coupons));
pipeline.push_back(make_shared<TaxPricingStep>(0.08));
CheckoutService checkoutService(pipeline);
auto order = checkoutService.checkout(cart, "SAVE10");
cout << "Order created: " << order->id << " total=" << order->totalPrice << " status=" << statusToString(order->status) << endl;
cout << "Laptop stock after checkout: " << laptop.stock << endl;
auto confirmed = checkoutService.confirmPayment(order->id, PaymentResult{true}, "idem-key-1");
cout << "After payment: " << statusToString(confirmed->status) << endl;
auto retried = checkoutService.confirmPayment(order->id, PaymentResult{true}, "idem-key-1");
cout << "Retry same order id? " << (retried == confirmed) << endl;
confirmed->transitionTo(OrderStatus::SHIPPED);
confirmed->transitionTo(OrderStatus::DELIVERED);
cout << "Final status: " << statusToString(confirmed->status) << endl;
return 0;
}
// Problem 19: E-commerce Shopping Cart & Checkout
// Patterns: Decorator (add-ons on CartItem), Strategy (pricing pipeline), State (Order lifecycle), Facade (CheckoutService)
class OutOfStockException extends Error {}
class OrderNotFoundException extends Error {}
// ---------- Catalog ----------
class Product {
constructor(id, name, price, stock) {
this.id = id; this.name = name; this.price = price; this.stock = stock;
}
decrementStock(qty) {
if (this.stock < qty) throw new OutOfStockException(`Insufficient stock for ${this.id}`);
this.stock -= qty;
}
restoreStock(qty) { this.stock += qty; }
}
// ---------- CartItem (component) + Decorator ----------
class CartItem {
constructor(product, quantity) {
this.product = product; this.quantity = quantity;
}
getPrice() { return this.product.price * this.quantity; }
getProduct() { return this.product; }
getQuantity() { return this.quantity; }
}
// Abstract decorator: subclasses CartItem so it's substitutable everywhere a CartItem is
// expected, but wraps another CartItem to layer one stackable add-on charge on top.
class CartItemDecorator extends CartItem {
constructor(wrapped) {
super(wrapped.getProduct(), wrapped.getQuantity());
this.wrapped = wrapped;
}
getPrice() { throw new Error('abstract method'); }
}
class GiftWrapDecorator extends CartItemDecorator {
getPrice() { return this.wrapped.getPrice() + 2.99; }
}
class EngravingDecorator extends CartItemDecorator {
getPrice() { return this.wrapped.getPrice() + 5.00; }
}
// ---------- Cart ----------
class Cart {
constructor() { this.items = []; }
addItem(item) { this.items.push(item); }
removeItem(productId) { this.items = this.items.filter(i => i.getProduct().id !== productId); }
subtotal() { return this.items.reduce((sum, i) => sum + i.getPrice(), 0); }
quantityFor(productId) {
return this.items.filter(i => i.getProduct().id === productId).reduce((sum, i) => sum + i.getQuantity(), 0);
}
getItems() { return this.items; }
}
// ---------- Pricing pipeline (Strategy) ----------
class TaxPricingStep {
constructor(rate) { this.rate = rate; }
apply(runningTotal, cart) { return runningTotal + runningTotal * this.rate; }
}
class CouponDiscountPricingStep {
constructor(couponCode, validCoupons) { this.couponCode = couponCode; this.validCoupons = validCoupons; }
apply(runningTotal, cart) {
if (!this.validCoupons.has(this.couponCode)) return runningTotal;
return runningTotal * (1 - this.validCoupons.get(this.couponCode));
}
}
// ---------- Order lifecycle (State) ----------
const OrderStatus = Object.freeze({
PENDING_PAYMENT: 'PENDING_PAYMENT', PAID: 'PAID', SHIPPED: 'SHIPPED',
DELIVERED: 'DELIVERED', CANCELLED: 'CANCELLED',
});
class PendingPaymentState {
onPaid(order) { order.status = OrderStatus.PAID; return new PaidState(); }
onShipped() { throw new Error('Cannot ship before payment'); }
onDelivered() { throw new Error('Cannot deliver before payment'); }
onCancelled(order) { order.status = OrderStatus.CANCELLED; return new CancelledState(); }
}
class PaidState {
onPaid() { return this; }
onShipped(order) { order.status = OrderStatus.SHIPPED; return new ShippedState(); }
onDelivered() { throw new Error('Cannot deliver before shipping'); }
onCancelled(order) { order.status = OrderStatus.CANCELLED; return new CancelledState(); } // refund path
}
class ShippedState {
onPaid() { return this; }
onShipped() { return this; }
onDelivered(order) { order.status = OrderStatus.DELIVERED; return new DeliveredState(); }
onCancelled() { throw new Error('Cannot cancel a shipped order'); }
}
class DeliveredState {
onPaid() { return this; }
onShipped() { return this; }
onDelivered() { return this; }
onCancelled() { throw new Error('Cannot cancel a delivered order'); }
}
class CancelledState {
onPaid() { throw new Error('Order is cancelled'); }
onShipped() { throw new Error('Order is cancelled'); }
onDelivered() { throw new Error('Order is cancelled'); }
onCancelled() { return this; }
}
// ---------- Order (immutable snapshot + lifecycle) ----------
class Order {
constructor(id, lineItems, totalPrice) {
this.id = id;
this.lineItems = Object.freeze([...lineItems]);
this.totalPrice = totalPrice;
this.status = OrderStatus.PENDING_PAYMENT;
this.state = new PendingPaymentState();
}
transitionTo(target) {
switch (target) {
case OrderStatus.PAID: this.state = this.state.onPaid(this); break;
case OrderStatus.SHIPPED: this.state = this.state.onShipped(this); break;
case OrderStatus.DELIVERED: this.state = this.state.onDelivered(this); break;
case OrderStatus.CANCELLED: this.state = this.state.onCancelled(this); break;
default: throw new Error(`Unsupported transition: ${target}`);
}
return this;
}
}
// ---------- CheckoutService (Facade) ----------
class CheckoutService {
constructor(pricingPipeline) {
this.pricingPipeline = pricingPipeline;
this.ordersById = new Map();
this.processedIdempotencyKeys = new Map();
this.nextOrderId = 1;
}
checkout(cart, couponCode) {
const orderedProductIds = [...new Set(cart.getItems().map(i => i.getProduct().id))].sort(); // canonical order
const reserved = [];
try {
for (const productId of orderedProductIds) {
const product = this._productFor(cart, productId);
const qty = cart.quantityFor(productId);
product.decrementStock(qty); // throws OutOfStockException on failure
reserved.push({ product, qty });
}
} catch (e) {
for (const r of reserved) r.product.restoreStock(r.qty);
throw e;
}
let total = cart.subtotal(); // includes decorator-priced add-ons
for (const step of this.pricingPipeline) total = step.apply(total, cart);
const order = new Order(`ORD-${this.nextOrderId++}`, cart.getItems(), total);
this.ordersById.set(order.id, order);
return order;
}
confirmPayment(orderId, result, idempotencyKey) {
if (this.processedIdempotencyKeys.has(idempotencyKey)) {
return this.processedIdempotencyKeys.get(idempotencyKey); // already processed: no double effect
}
const order = this.ordersById.get(orderId);
if (!order) throw new OrderNotFoundException(`No such order: ${orderId}`);
if (result.success) {
order.transitionTo(OrderStatus.PAID);
} else {
order.transitionTo(OrderStatus.CANCELLED);
this._restoreInventory(order);
}
this.processedIdempotencyKeys.set(idempotencyKey, order);
return order;
}
_restoreInventory(order) {
for (const item of order.lineItems) item.getProduct().restoreStock(item.getQuantity());
}
_productFor(cart, productId) {
const item = cart.getItems().find(i => i.getProduct().id === productId);
if (!item) throw new Error(`Product not in cart: ${productId}`);
return item.getProduct();
}
}
// ---------- Demo ----------
function main() {
const laptop = new Product('P1', 'Laptop', 1000.0, 5);
const mug = new Product('P2', 'Mug', 15.0, 10);
const cart = new Cart();
cart.addItem(new GiftWrapDecorator(new CartItem(laptop, 1)));
cart.addItem(new EngravingDecorator(new CartItem(mug, 2)));
console.log('Subtotal (with add-ons):', cart.subtotal());
const coupons = new Map([['SAVE10', 0.10]]);
const pipeline = [new CouponDiscountPricingStep('SAVE10', coupons), new TaxPricingStep(0.08)];
const checkoutService = new CheckoutService(pipeline);
const order = checkoutService.checkout(cart, 'SAVE10');
console.log(`Order created: ${order.id} total=${order.totalPrice} status=${order.status}`);
console.log('Laptop stock after checkout:', laptop.stock);
const confirmed = checkoutService.confirmPayment(order.id, { success: true }, 'idem-key-1');
console.log('After payment:', confirmed.status);
const retried = checkoutService.confirmPayment(order.id, { success: true }, 'idem-key-1');
console.log('Retry same order id?', retried === confirmed);
confirmed.transitionTo(OrderStatus.SHIPPED);
confirmed.transitionTo(OrderStatus.DELIVERED);
console.log('Final status:', confirmed.status);
}
if (require.main === module) main();
module.exports = {
Product, CartItem, CartItemDecorator, GiftWrapDecorator, EngravingDecorator,
Cart, TaxPricingStep, CouponDiscountPricingStep, Order, OrderStatus, CheckoutService,
};
// Problem 19: E-commerce Shopping Cart & Checkout
// Patterns: Decorator (add-ons on CartItem), Strategy (pricing pipeline), State (Order lifecycle), Facade (CheckoutService)
class OutOfStockException extends Error {}
class OrderNotFoundException extends Error {}
// ---------- Catalog ----------
class Product {
constructor(public id: string, public name: string, public price: number, public stock: number) {}
decrementStock(qty: number): void {
if (this.stock < qty) throw new OutOfStockException(`Insufficient stock for ${this.id}`);
this.stock -= qty;
}
restoreStock(qty: number): void { this.stock += qty; }
}
// ---------- CartItem (component) + Decorator ----------
class CartItem {
constructor(protected product: Product, protected quantity: number) {}
getPrice(): number { return this.product.price * this.quantity; }
getProduct(): Product { return this.product; }
getQuantity(): number { return this.quantity; }
}
// Abstract decorator: subclasses CartItem so it's substitutable everywhere a CartItem is
// expected, but wraps another CartItem to layer one stackable add-on charge on top.
abstract class CartItemDecorator extends CartItem {
constructor(protected wrapped: CartItem) {
super(wrapped.getProduct(), wrapped.getQuantity());
}
abstract getPrice(): number;
}
class GiftWrapDecorator extends CartItemDecorator {
private static readonly FEE = 2.99;
getPrice(): number { return this.wrapped.getPrice() + GiftWrapDecorator.FEE; }
}
class EngravingDecorator extends CartItemDecorator {
private static readonly FEE = 5.00;
getPrice(): number { return this.wrapped.getPrice() + EngravingDecorator.FEE; }
}
// ---------- Cart ----------
class Cart {
private items: CartItem[] = [];
addItem(item: CartItem): void { this.items.push(item); }
removeItem(productId: string): void { this.items = this.items.filter(i => i.getProduct().id !== productId); }
subtotal(): number { return this.items.reduce((sum, i) => sum + i.getPrice(), 0); }
quantityFor(productId: string): number {
return this.items.filter(i => i.getProduct().id === productId).reduce((sum, i) => sum + i.getQuantity(), 0);
}
getItems(): CartItem[] { return this.items; }
}
// ---------- Pricing pipeline (Strategy) ----------
interface PricingStep {
apply(runningTotal: number, cart: Cart): number;
}
class TaxPricingStep implements PricingStep {
constructor(private rate: number) {}
apply(runningTotal: number, _cart: Cart): number { return runningTotal + runningTotal * this.rate; }
}
class CouponDiscountPricingStep implements PricingStep {
constructor(private couponCode: string, private validCoupons: Map<string, number>) {}
apply(runningTotal: number, _cart: Cart): number {
if (!this.validCoupons.has(this.couponCode)) return runningTotal;
return runningTotal * (1 - this.validCoupons.get(this.couponCode)!);
}
}
// ---------- Order lifecycle (State) ----------
enum OrderStatus { PENDING_PAYMENT = 'PENDING_PAYMENT', PAID = 'PAID', SHIPPED = 'SHIPPED', DELIVERED = 'DELIVERED', CANCELLED = 'CANCELLED' }
interface OrderState {
onPaid(order: Order): OrderState;
onShipped(order: Order): OrderState;
onDelivered(order: Order): OrderState;
onCancelled(order: Order): OrderState;
}
class PendingPaymentState implements OrderState {
onPaid(order: Order): OrderState { order.status = OrderStatus.PAID; return new PaidState(); }
onShipped(): OrderState { throw new Error('Cannot ship before payment'); }
onDelivered(): OrderState { throw new Error('Cannot deliver before payment'); }
onCancelled(order: Order): OrderState { order.status = OrderStatus.CANCELLED; return new CancelledState(); }
}
class PaidState implements OrderState {
onPaid(): OrderState { return this; }
onShipped(order: Order): OrderState { order.status = OrderStatus.SHIPPED; return new ShippedState(); }
onDelivered(): OrderState { throw new Error('Cannot deliver before shipping'); }
onCancelled(order: Order): OrderState { order.status = OrderStatus.CANCELLED; return new CancelledState(); } // refund path
}
class ShippedState implements OrderState {
onPaid(): OrderState { return this; }
onShipped(): OrderState { return this; }
onDelivered(order: Order): OrderState { order.status = OrderStatus.DELIVERED; return new DeliveredState(); }
onCancelled(): OrderState { throw new Error('Cannot cancel a shipped order'); }
}
class DeliveredState implements OrderState {
onPaid(): OrderState { return this; }
onShipped(): OrderState { return this; }
onDelivered(): OrderState { return this; }
onCancelled(): OrderState { throw new Error('Cannot cancel a delivered order'); }
}
class CancelledState implements OrderState {
onPaid(): OrderState { throw new Error('Order is cancelled'); }
onShipped(): OrderState { throw new Error('Order is cancelled'); }
onDelivered(): OrderState { throw new Error('Order is cancelled'); }
onCancelled(): OrderState { return this; }
}
// ---------- Order (immutable snapshot + lifecycle) ----------
class Order {
readonly id: string;
readonly lineItems: ReadonlyArray<CartItem>;
readonly totalPrice: number;
status: OrderStatus;
private state: OrderState;
constructor(id: string, lineItems: CartItem[], totalPrice: number) {
this.id = id;
this.lineItems = Object.freeze([...lineItems]);
this.totalPrice = totalPrice;
this.status = OrderStatus.PENDING_PAYMENT;
this.state = new PendingPaymentState();
}
transitionTo(target: OrderStatus): Order {
switch (target) {
case OrderStatus.PAID: this.state = this.state.onPaid(this); break;
case OrderStatus.SHIPPED: this.state = this.state.onShipped(this); break;
case OrderStatus.DELIVERED: this.state = this.state.onDelivered(this); break;
case OrderStatus.CANCELLED: this.state = this.state.onCancelled(this); break;
default: throw new Error(`Unsupported transition: ${target}`);
}
return this;
}
}
interface PaymentResult { success: boolean; }
// ---------- CheckoutService (Facade) ----------
class CheckoutService {
private ordersById = new Map<string, Order>();
private processedIdempotencyKeys = new Map<string, Order>();
private nextOrderId = 1;
constructor(private pricingPipeline: PricingStep[]) {}
checkout(cart: Cart, _couponCode: string | null): Order {
const orderedProductIds = [...new Set(cart.getItems().map(i => i.getProduct().id))].sort(); // canonical order
const reserved: { product: Product; qty: number }[] = [];
try {
for (const productId of orderedProductIds) {
const product = this.productFor(cart, productId);
const qty = cart.quantityFor(productId);
product.decrementStock(qty); // throws OutOfStockException on failure
reserved.push({ product, qty });
}
} catch (e) {
for (const r of reserved) r.product.restoreStock(r.qty);
throw e;
}
let total = cart.subtotal(); // includes decorator-priced add-ons
for (const step of this.pricingPipeline) total = step.apply(total, cart);
const order = new Order(`ORD-${this.nextOrderId++}`, cart.getItems(), total);
this.ordersById.set(order.id, order);
return order;
}
confirmPayment(orderId: string, result: PaymentResult, idempotencyKey: string): Order {
if (this.processedIdempotencyKeys.has(idempotencyKey)) {
return this.processedIdempotencyKeys.get(idempotencyKey)!; // already processed: no double effect
}
const order = this.ordersById.get(orderId);
if (!order) throw new OrderNotFoundException(`No such order: ${orderId}`);
if (result.success) {
order.transitionTo(OrderStatus.PAID);
} else {
order.transitionTo(OrderStatus.CANCELLED);
this.restoreInventory(order);
}
this.processedIdempotencyKeys.set(idempotencyKey, order);
return order;
}
private restoreInventory(order: Order): void {
for (const item of order.lineItems) item.getProduct().restoreStock(item.getQuantity());
}
private productFor(cart: Cart, productId: string): Product {
const item = cart.getItems().find(i => i.getProduct().id === productId);
if (!item) throw new Error(`Product not in cart: ${productId}`);
return item.getProduct();
}
}
// ---------- Demo ----------
function main(): void {
const laptop = new Product('P1', 'Laptop', 1000.0, 5);
const mug = new Product('P2', 'Mug', 15.0, 10);
const cart = new Cart();
cart.addItem(new GiftWrapDecorator(new CartItem(laptop, 1)));
cart.addItem(new EngravingDecorator(new CartItem(mug, 2)));
console.log('Subtotal (with add-ons):', cart.subtotal());
const coupons = new Map<string, number>([['SAVE10', 0.10]]);
const pipeline: PricingStep[] = [new CouponDiscountPricingStep('SAVE10', coupons), new TaxPricingStep(0.08)];
const checkoutService = new CheckoutService(pipeline);
const order = checkoutService.checkout(cart, 'SAVE10');
console.log(`Order created: ${order.id} total=${order.totalPrice} status=${order.status}`);
console.log('Laptop stock after checkout:', laptop.stock);
const confirmed = checkoutService.confirmPayment(order.id, { success: true }, 'idem-key-1');
console.log('After payment:', confirmed.status);
const retried = checkoutService.confirmPayment(order.id, { success: true }, 'idem-key-1');
console.log('Retry same order id?', retried === confirmed);
confirmed.transitionTo(OrderStatus.SHIPPED);
confirmed.transitionTo(OrderStatus.DELIVERED);
console.log('Final status:', confirmed.status);
}
main();
export {
Product, CartItem, CartItemDecorator, GiftWrapDecorator, EngravingDecorator,
Cart, PricingStep, TaxPricingStep, CouponDiscountPricingStep, Order, OrderStatus, CheckoutService,
};