Design a Coffee Vending Machine
OOP design for a coffee vending machine covering State pattern for machine lifecycle, beverage recipes with ingredient management, payment processing, and inventory tracking.
The Problem
Your company installs coffee machines in co-working spaces. Each machine offers espresso, latte, cappuccino, and americano, with customization options like extra sugar and decaf. The current firmware is a single procedure: 700 lines of nested if-else blocks checking a machineStatus integer, inline ingredient math, and hardcoded recipes. Adding oat milk last quarter required editing 14 different branches, and a typo in one caused machines to brew without deducting water.
A coffee vending machine combines two classic design challenges: a state machine (idle, selecting, payment, brewing, dispensing) and a recipe system that maps beverages to ingredient quantities. The interviewer wants to see clean state transitions, a decoupled recipe/inventory model, and a design that handles new beverages without touching existing code.
Design the core classes for a coffee vending machine that supports multiple beverage types with recipes, ingredient inventory tracking, customizations (extra sugar, skim milk, decaf), coin and card payment, and maintenance mode for restocking.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to pin down the scope. Cover four areas: core actions, error handling, boundaries, and extensibility.
You: "What beverages does the machine offer, and can we add new ones later?"
Interviewer: "Start with espresso, latte, cappuccino, and americano. The design should allow adding new beverages without code changes."
Four beverages to start, but the menu is not fixed. That tells you recipes should be data, not hardcoded logic.
You: "What ingredients does each beverage require?"
Interviewer: "Coffee beans, water, and milk. Each recipe specifies quantities. For example, a latte needs 7g coffee, 30ml water, and 150ml milk."
Recipes are essentially a map of ingredient to quantity. Now clarify customization.
You: "What customizations are available? Can users combine multiple?"
Interviewer: "Extra sugar, skim milk instead of regular, and decaf. Users can combine them freely."
Multiple stackable customizations on top of a base recipe. This points toward Decorator or Builder at the customization layer.
You: "What payment methods does the machine accept?"
Interviewer: "Coins (5, 10, 25 cent denominations) and card. Coins need change calculation. Card is always exact."
Two payment paths with different mechanics. Strategy pattern fits here.
You: "What happens if an ingredient runs out mid-menu? Can the user still see unavailable drinks?"
Interviewer: "Show all beverages but mark unavailable ones. If someone selects an unavailable drink, reject the selection."
Good. The machine checks inventory against the recipe before accepting a selection. This means Inventory needs a canFulfill(Recipe) method.
You: "Is there a maintenance mode for restocking ingredients?"
Interviewer: "Yes. An operator can enter maintenance mode, refill ingredients, and add new recipes. The machine must not accept user input during maintenance."
Maintenance is a distinct state in the state machine. It blocks all normal transitions.
You: "Does the machine handle one user at a time, or concurrent access?"
Interviewer: "One user at a time. It is a physical machine with a single interface."
You: "Should the user be able to cancel at any point and get a refund?"
Interviewer: "Yes, at any point before brewing starts. Once brewing begins, no cancellation."
That constrains the cancel transition: it is valid from SELECTING and PAYMENT states, but not from BREWING or DISPENSING.
Final Requirements
Functional Requirements:
- Offer multiple beverages (espresso, latte, cappuccino, americano) with recipes defining ingredient quantities
- Track ingredient inventory (coffee beans, water, milk, sugar) and prevent selection of beverages with insufficient stock
- Support customizations: extra sugar, skim milk, decaf (stackable)
- Accept coin payment (5, 10, 25 cents) with change calculation, and card payment (exact amount)
- Transition through well-defined states: IDLE, SELECTING, PAYMENT, BREWING, DISPENSING, MAINTENANCE
- Allow operators to enter maintenance mode for restocking and adding new recipes
- Support cancellation with full refund before brewing starts
Non-Functional Requirements:
- Extensibility: adding a new beverage requires only a new Recipe entry, not code changes
- State safety: invalid transitions are rejected cleanly
- Separation of concerns: recipes, inventory, payment, and state management live in separate classes
Out of Scope:
- UI rendering or hardware integration
- Persistence or database storage
- Multi-user concurrency
- Loyalty programs (discussed in Extensibility)
Interview tip
Number your requirements and read them back. The interviewer can then say "focus on requirements 1-3" which saves time. It also shows you think in structured specs.
Example Inputs and Outputs
Scenario 1: Successful latte purchase with coins
- Input: User selects "Latte" (price: $1.50). Inserts 6 quarters ($1.50 exact).
- Expected: Machine brews latte (deducts 7g coffee, 30ml water, 150ml milk), dispenses cup, returns no change. State returns to IDLE.
- Why: Validates the happy path (requirements 1, 4, 5).
Scenario 2: Insufficient ingredients
- Input: Machine has only 20ml milk remaining. User selects "Cappuccino" (requires 120ml milk).
- Expected: Machine rejects the selection with "Cappuccino unavailable: insufficient milk." User stays in SELECTING state and can pick another drink.
- Why: Validates inventory check before brewing (requirement 2).
Scenario 3: Customized order with card payment
- Input: User selects "Americano" with extra sugar and decaf. Pays by card ($1.25).
- Expected: Machine brews americano using decaf beans (8g) + extra sugar (10g) + 200ml water. Dispenses. Card is charged exact amount.
- Why: Validates customization stacking and card payment (requirements 3, 4).
Scenario 4: Cancel mid-payment
- Input: User selects "Espresso" (price: $1.00). Inserts 2 quarters ($0.50). Presses cancel.
- Expected: Machine returns $0.50 (2 quarters). State returns to IDLE. No ingredients consumed.
- Why: Validates cancellation and refund (requirement 7).
Try It Yourself
Try it yourself
Before reading the solution, spend 15 minutes sketching class diagrams. Focus on two things: how recipes map beverages to ingredients, and how the machine transitions between states. These are the two pillars of this design. Compare your approach with the walkthrough below.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this system? Look for nouns in the requirements: machine, beverage, recipe, ingredient, inventory, payment, state. Each noun hints at a class with one clear responsibility.
A common trap is stuffing recipe logic, inventory checks, and state transitions into one CoffeeMachine god class. That violates SRP and makes every change risky. I see candidates do this constantly, and the interviewer immediately asks "how would you add a new beverage?" and the answer involves editing a 200-line method.
| Entity | Responsibility | Key attributes |
|---|---|---|
| CoffeeMachine | The orchestrator. Coordinates state transitions, delegates to other components. | currentState, inventory, recipeStore, paymentProcessor |
| Beverage | Value object representing a drink type with its base price. | name, basePrice |
| Recipe | Maps a beverage to required ingredients and their quantities. | beverage, ingredientMap |
| Ingredient | Enum of raw materials the machine uses. | COFFEE_BEANS, WATER, MILK, SUGAR |
| Inventory | Tracks current levels of each ingredient. Checks and deducts stock. | stockLevels (map of Ingredient to quantity) |
| MachineState | Interface for the State pattern. Each state handles user actions differently. | (methods: selectBeverage, insertCoin, payByCard, cancel, brew) |
| PaymentProcessor | Handles coin accumulation, card charging, change calculation, and refunds. | currentBalance, selectedPrice |
| RecipeStore | Registry of all available recipes. Lookup by beverage name. | recipes (map of String to Recipe) |
| Customization | Modifier applied to a recipe (extra sugar, decaf, skim milk). | name, ingredientAdjustments |
Notice CoffeeMachine does not hold recipes directly. It delegates to RecipeStore, which is a separate registry. This means adding a new beverage in production is a RecipeStore configuration change, not a CoffeeMachine code change. Similarly, Inventory knows nothing about recipes: it just tracks quantities and answers "do you have X grams of Y?"
Step 2: Define Relationships and Class Design
CoffeeMachine (the orchestrator)
CoffeeMachine coordinates all actions but owns no business logic. It delegates state-specific behavior to the current MachineState, recipe lookup to RecipeStore, stock checks to Inventory, and money handling to PaymentProcessor.
Deriving state from requirements:
| Requirement | What CoffeeMachine must track |
|---|---|
| "Transition through well-defined states" | currentState (MachineState) |
| "Track ingredient inventory" | inventory (Inventory) |
| "Offer multiple beverages with recipes" | recipeStore (RecipeStore), selectedRecipe (Recipe) |
| "Accept coin and card payment" | payment (PaymentProcessor) |
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "User selects a beverage" | selectBeverage(String name) |
| "Stackable customizations" | addCustomization(Customization c) |
| "Insert coins for payment" | insertCoin(int cents) |
| "Pay by card" | payByCard() |
| "Cancel for refund" | cancel() |
| "Operator maintenance" | enterMaintenance(), exitMaintenance() |
Every public method delegates to currentState.doSomething(this, ...). The machine itself is a thin dispatcher.
Recipe and Customization
A Recipe holds a Beverage and a map of ingredients to quantities. Customizations modify the map: "extra sugar" adds 10g sugar, "decaf" swaps coffee beans for decaf beans (we model this as adjusting the COFFEE_BEANS quantity or adding a DECAF_BEANS ingredient). The withCustomization() method returns a new Recipe with adjusted quantities, keeping the original immutable.
Key relationship decisions
CoffeeMachine to MachineState is delegation. The machine holds one state at a time and swaps it on transitions. This is the core of the State pattern.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.