Home/Learn/Low Level Design/Design an Online Bookstore

Design an Online Bookstore

Intermediate
LLD Interview Problems
Java Source Code

Model an e-commerce bookstore with a cart, pluggable discount strategies, stackable promotions via Decorator, and inventory-alert observers.

Overview

An online bookstore allows users to browse a catalog, add books to a Cart, and place an Order. Pricing is governed by a DiscountStrategy interface enabling flat, percentage, and coupon discounts. Decorator pattern stacks multiple promotions (e.g., a member discount on top of a seasonal sale). ShippingStrategy selects Standard, Express, or Free-over-threshold shipping. An Observer watches Inventory and fires low-stock alerts. On checkout, Cart validates inventory, applies discounts, chooses shipping, creates an Order, and decrements inventory. This problem exercises three GoF patterns in one cohesive design.

Requirements Analysis

Functional: browse catalog, add/remove items in cart, apply discount codes, select shipping method, place order, update inventory, alert on low stock. Non-functional: discount rules composable at runtime without modifying Order, inventory updates atomic to prevent overselling, observer-based stock alerts decoupled from order flow.

Requirements
// Entities : BookStore, Book, Cart, CartItem, Order, OrderItem, Inventory, Discount, ShippingStrategy
// Patterns : Strategy (discount + shipping), Decorator (stackable discounts), Observer (inventory alerts)

Core Classes & Relationships

DiscountStrategy interface has applyDiscount(double price). Concrete strategies: NoDiscount, PercentageDiscount, FlatDiscount. DiscountDecorator is an abstract Decorator wrapping another DiscountStrategy, enabling stacking. ShippingStrategy interface has calculateShipping(double orderTotal). Inventory emits low-stock events to StockObserver listeners.

Java — enums & interfaces
public interface DiscountStrategy {
    double applyDiscount(double totalPrice);
}

// Base strategies
public class NoDiscount       implements DiscountStrategy { public double applyDiscount(double p){ return p; } }
public class PercentageDiscount implements DiscountStrategy {
    private final double pct;
    public PercentageDiscount(double pct) { this.pct = pct; }
    public double applyDiscount(double p) { return p * (1 - pct / 100); }
}

// Decorator base
public abstract class DiscountDecorator implements DiscountStrategy {
    protected final DiscountStrategy wrapped;
    public DiscountDecorator(DiscountStrategy d) { this.wrapped = d; }
}

public class FlatDiscountDecorator extends DiscountDecorator {
    private final double amount;
    public FlatDiscountDecorator(DiscountStrategy d, double amount) { super(d); this.amount = amount; }
    public double applyDiscount(double p) { return Math.max(0, wrapped.applyDiscount(p) - amount); }
}

public interface ShippingStrategy { double calculateShipping(double orderTotal); }
public interface StockObserver    { void onLowStock(String isbn, int remaining); }

Java Implementation

Cart accumulates CartItems and computes a subtotal. checkout() validates stock, applies the composed DiscountStrategy, adds shipping via ShippingStrategy, creates an Order, and decrements Inventory. Inventory.decrement() checks the threshold and fires observers when stock falls below the alert level.

Java — core classes
public record CartItem(Book book, int quantity) {
    public double subtotal() { return book.price() * quantity; }
}

public class Cart {
    private final List<CartItem> items = new ArrayList<>();
    private DiscountStrategy discount = new NoDiscount();
    private ShippingStrategy shipping = total -> total > 500 ? 0 : 49.0;

    public void addItem(Book book, int qty) {
        items.stream().filter(i -> i.book().equals(book)).findFirst()
            .ifPresentOrElse(
                i -> items.set(items.indexOf(i), new CartItem(book, i.quantity() + qty)),
                ()  -> items.add(new CartItem(book, qty))
            );
    }

    public void setDiscount(DiscountStrategy d) { this.discount = d; }
    public void setShipping(ShippingStrategy s) { this.shipping = s; }

    public double subtotal()    { return items.stream().mapToDouble(CartItem::subtotal).sum(); }
    public double discounted()  { return discount.applyDiscount(subtotal()); }
    public double total()       { return discounted() + shipping.calculateShipping(discounted()); }

    public Order checkout(Inventory inventory) {
        items.forEach(i -> {
            if (inventory.available(i.book().isbn()) < i.quantity())
                throw new IllegalStateException("Insufficient stock for: " + i.book().title());
        });
        items.forEach(i -> inventory.decrement(i.book().isbn(), i.quantity()));
        Order order = new Order(List.copyOf(items), discounted(), shipping.calculateShipping(discounted()));
        items.clear();
        return order;
    }
}

public class Inventory {
    private final Map<String, Integer> stock = new HashMap<>();
    private final List<StockObserver> observers = new ArrayList<>();
    private static final int LOW_STOCK_THRESHOLD = 5;

    public void addObserver(StockObserver o) { observers.add(o); }
    public int available(String isbn) { return stock.getOrDefault(isbn, 0); }

    public void decrement(String isbn, int qty) {
        int remaining = stock.merge(isbn, -qty, Integer::sum);
        if (remaining < LOW_STOCK_THRESHOLD)
            observers.forEach(o -> o.onLowStock(isbn, remaining));
    }
}

public class Order {
    private final String orderId = UUID.randomUUID().toString();
    private final List<CartItem> items;
    private final double discountedSubtotal;
    private final double shippingCost;
    private OrderStatus status = OrderStatus.PLACED;

    public Order(List<CartItem> items, double discountedSubtotal, double shippingCost) {
        this.items = items; this.discountedSubtotal = discountedSubtotal; this.shippingCost = shippingCost;
    }
    public double grandTotal() { return discountedSubtotal + shippingCost; }
    public String getOrderId() { return orderId; }
}

enum OrderStatus { PLACED, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }

Key Points to Remember

  • 1Decorator stacks discounts at runtime: new FlatDiscountDecorator(new PercentageDiscount(10), 50) chains two promotions without modifying either class.
  • 2Strategy for shipping means swapping Free-over-threshold for Prime-style free shipping is a one-liner constructor change.
  • 3Observer on Inventory keeps the order flow clean — restocking alerts are entirely separate from checkout logic.
  • 4Using record for CartItem gives free equals/hashCode/toString, simplifying duplicate-item detection in the cart.

Interview Questions

Sign in to ask Aria
1

How would you prevent two users from buying the last copy of a book simultaneously?

HardAmazon
2

How does the Decorator pattern differ from simply subclassing PercentageDiscount for each combination?

MediumGoogle
3

How would you design the Order state machine to handle cancellation after shipment?

MediumUber

Ask Aria about Design an Online Bookstore

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…