Design the pricing engine for a chain of pizza stores. Customers build pizzas from a base and toppings, add drinks, and the store totals the order — applying whatever promotional deals are running. The problem grows in three parts, each raising the bar on your object-oriented design — by Part 3 you'll be expected to apply SOLID and the Strategy and Factory patterns so new deals slot in without editing the store.
Everything lives under src/main/java/io/coderpass/pizza/. All monetary amounts
are integer cents (int). Prices are store-specific — the same base or
topping can cost a different amount at a different store — so every price is
looked up against a Store catalog, never baked into the item.
- A base is the foundation (
THIN,REGULAR,THICK,STUFFED). - A topping is an add-on (
CHEESE,PEPPERONI,MUSHROOM, …). A pizza may repeat a topping (double cheese) and each repeat is charged again. - A drink (
WATER,COLA, …) is another kind of order item. - A
Storeholds its own catalog: a price for each base, topping and drink it stocks. Asking for something it doesn't stock throwsUnknownItemError.
import io.coderpass.pizza.*;
import java.util.Map;
Store store = new Store(
"Mario's",
Map.of(Base.THIN, 800, Base.REGULAR, 1000),
Map.of(Topping.CHEESE, 150, Topping.PEPPERONI, 250),
Map.of(DrinkType.COLA, 200));Price a single pizza against a store's catalog.
public class Store {
public int basePrice(Base base) { ... }
public int toppingPrice(Topping topping) { ... }
}
public class Pizza implements OrderItem {
public int price(Store store) { ... }
}basePrice/toppingPricereturn the catalog price, or throwUnknownItemErrorif the store doesn't stock that item.new Pizza(base, toppings).price(store)is the base price plus the price of every topping (repeats included).
new Pizza(Base.THIN, List.of(Topping.CHEESE, Topping.PEPPERONI)).price(store); // 800+150+250 = 1200An order holds any mix of pizzas, drinks and other items — each an OrderItem
that prices itself against the store.
public class Drink implements OrderItem { // wraps a DrinkType
public int price(Store store) { ... }
}
public class Order {
public void add(OrderItem item) { ... }
public int subtotal(Store store) { ... }
}new Drink(DrinkType.COLA).price(store)looks the drink up in the catalog (UnknownItemErrorif unstocked).Order.subtotal(store)sums every item's price. An empty order is0.
Order order = new Order();
order.add(new Pizza(Base.THIN, List.of(Topping.PEPPERONI))); // 1050
order.add(new Drink(DrinkType.COLA)); // 200
order.subtotal(store); // 1250Make deals pluggable without touching Store. A Promotion is a Strategy
that reports how many cents to knock off an order.
public interface Promotion {
int discount(Order order, Store store);
}
public class Store {
public int priceOrder(Order order, List<Promotion> promotions) { ... }
}Implement three deals:
BogoPizzaPromotion— buy-one-get-one-free on pizzas: pair them most-expensive-first, the cheaper of each pair is free. An odd pizza out is not free.FreeDrinkWithPizzaPromotion— one free drink per pizza; when drinks outnumber pizzas the cheapest drinks are the free ones.MostExpensiveToppingFreePromotion— each pizza's single dearest topping is free.
And a PromotionFactory.create(PromotionType) that builds a deal from its
enum.
priceOrder returns the subtotal minus every promotion's discount. Promotions
stack additively and the total never drops below zero.
store.priceOrder(order, List.of(PromotionFactory.create(PromotionType.BOGO_PIZZA)));With Docker (matches CI):
docker compose -f docker-compose.yaml run --rm testOr locally:
mvn -q -B test- Object-oriented modelling of a small domain (stores, catalogs, pizzas, orders)
- SOLID: single-responsibility split of catalog / item / order / promotion, and Open/Closed + Dependency-Inversion via injected promotion strategies
- Design patterns: Strategy (promotions) and Factory (promotion creation)
- Careful edge-case handling: store-specific prices, repeated toppings, deal pairing rules, additive stacking and the zero floor