A hands-on guide to building a modular, extensible vending machine using object-oriented principles and design patterns in Java.
π§ Table of Contents
- Introduction
- System Requirements
- Design Goals
- Class Design Overview
- Design Patterns Used
- UML Diagram
- Code Implementation
- Sample Output
- Conclusion & Next Steps
π° Introduction
The Vending Machine system is a frequently asked LLD question for junior to mid-level software engineers in interviews. It checks your ability to:
- Design systems using encapsulation, state transition, and behavioral logic
- Apply object-oriented principles for clarity and reusability
- Leverage design patterns to enhance scalability
In this blog, weβll model and build a console-based vending machine that dispenses items, accepts coins, returns change, and handles errors gracefully.
β System Requirements
Functional Requirements:
- Display available products with prices
- Accept money (coins only)
- Dispense selected item if sufficient money is inserted
- Return change if applicable
- Cancel order and refund inserted coins
Non-Functional Requirements:
- Easy to extend for new item types or coin denominations
- Clean state management
- Configurable item inventory
π― Design Goals
- Adhere to SOLID principles
- Isolate business logic using State pattern
- Maintain clean separation of concerns
- Make the system testable and extensible
ποΈ Class Design Overview
Class/Interface | Responsibility |
---|---|
VendingMachine |
Core machine logic and state management |
Item |
Represents a product with price and name |
Coin (enum) |
Supported denominations |
Inventory |
Tracks available items and coins |
VendingMachineState |
Interface for machine state transitions |
IdleState , HasMoneyState , DispensingState
|
Concrete state implementations |
StateContext |
Maintains current state |
π§° Design Patterns Used
Pattern | Role |
---|---|
State | To manage machine states (Idle β HasMoney β Dispense) |
Factory (optional) | To generate items or states dynamically |
Strategy (optional) | For change calculation algorithm |
π UML Diagram (Simplified)
+------------------------+
| VendingMachine |
+------------------------+
| - inventory: Inventory |
| - currentState: VendingMachineState |
+------------------------+
| +insertCoin(Coin) |
| +selectItem(String) |
| +cancel() |
| +dispense() |
+------------------------+
|
βΌ
+-----------------------------+
| <<interface>> VendingMachineState |
+-----------------------------+
| +insertCoin(Coin) |
| +selectItem(String) |
| +cancel() |
| +dispense() |
+-----------------------------+
β² β² β²
| | |
+----------+ +----------------+ +-------------------+
| IdleState| | HasMoneyState | | DispensingState |
+----------+ +----------------+ +-------------------+
+------------------------+
| Coin (enum) |
| PENNY, NICKEL, DIME... |
+------------------------+
+------------------------+
| Item |
| - name: String |
| - price: int |
+------------------------+
+------------------------+
| Inventory |
| - items: Map<Item, Integer> |
+------------------------+
π§± Code Implementation
β
Coin.java
public enum Coin {
PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
private final int value;
Coin(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
β
Item.java
public class Item {
private final String name;
private final int price; // In cents
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public int getPrice() { return price; }
}
β
Inventory.java
public class Inventory {
private final Map<Item, Integer> items = new HashMap<>();
public void addItem(Item item, int quantity) {
items.put(item, items.getOrDefault(item, 0) + quantity);
}
public boolean hasItem(Item item) {
return items.getOrDefault(item, 0) > 0;
}
public void dispenseItem(Item item) {
items.put(item, items.get(item) - 1);
}
public Set<Item> getAvailableItems() {
return items.keySet();
}
}
β
VendingMachineState.java
public interface VendingMachineState {
void insertCoin(Coin coin);
void selectItem(String itemName);
void cancel();
void dispense();
}
β
IdleState.java
public class IdleState implements VendingMachineState {
private final VendingMachine machine;
public IdleState(VendingMachine machine) {
this.machine = machine;
}
public void insertCoin(Coin coin) {
machine.addBalance(coin.getValue());
machine.setState(new HasMoneyState(machine));
}
public void selectItem(String itemName) {
System.out.println("Insert coin first.");
}
public void cancel() {
System.out.println("Nothing to cancel.");
}
public void dispense() {
System.out.println("Insert coin first.");
}
}
β
HasMoneyState.java
public class HasMoneyState implements VendingMachineState {
private final VendingMachine machine;
public HasMoneyState(VendingMachine machine) {
this.machine = machine;
}
public void insertCoin(Coin coin) {
machine.addBalance(coin.getValue());
}
public void selectItem(String itemName) {
Item item = machine.getItemByName(itemName);
if (item == null || !machine.getInventory().hasItem(item)) {
System.out.println("Item not available.");
return;
}
if (machine.getBalance() >= item.getPrice()) {
machine.setSelectedItem(item);
machine.setState(new DispensingState(machine));
} else {
System.out.println("Insufficient balance.");
}
}
public void cancel() {
machine.returnChange();
machine.setState(new IdleState(machine));
}
public void dispense() {
System.out.println("Select item first.");
}
}
β
DispensingState.java
public class DispensingState implements VendingMachineState {
private final VendingMachine machine;
public DispensingState(VendingMachine machine) {
this.machine = machine;
}
public void insertCoin(Coin coin) {
System.out.println("Dispensing in progress...");
}
public void selectItem(String itemName) {
System.out.println("Already dispensing...");
}
public void cancel() {
System.out.println("Dispensing in progress...");
}
public void dispense() {
Item item = machine.getSelectedItem();
machine.getInventory().dispenseItem(item);
machine.deductBalance(item.getPrice());
machine.returnChange();
System.out.println("Dispensed: " + item.getName());
machine.setState(new IdleState(machine));
}
}
β
VendingMachine.java
public class VendingMachine {
private VendingMachineState currentState;
private final Inventory inventory = new Inventory();
private int balance = 0;
private Item selectedItem;
public VendingMachine() {
this.currentState = new IdleState(this);
}
public void insertCoin(Coin coin) { currentState.insertCoin(coin); }
public void selectItem(String name) { currentState.selectItem(name); }
public void cancel() { currentState.cancel(); }
public void dispense() { currentState.dispense(); }
// State and logic management
public void setState(VendingMachineState state) { this.currentState = state; }
public void addBalance(int value) { this.balance += value; }
public void deductBalance(int value) { this.balance -= value; }
public void returnChange() {
System.out.println("Returning change: " + balance + " cents");
balance = 0;
}
public void setSelectedItem(Item item) { this.selectedItem = item; }
public Item getSelectedItem() { return selectedItem; }
public Inventory getInventory() { return inventory; }
public int getBalance() { return balance; }
public Item getItemByName(String name) {
return inventory.getAvailableItems().stream()
.filter(i -> i.getName().equalsIgnoreCase(name))
.findFirst().orElse(null);
}
}
β
Main.java
public class Main {
public static void main(String[] args) {
VendingMachine machine = new VendingMachine();
Item coke = new Item("Coke", 25);
Item pepsi = new Item("Pepsi", 35);
machine.getInventory().addItem(coke, 5);
machine.getInventory().addItem(pepsi, 3);
machine.insertCoin(Coin.QUARTER);
machine.selectItem("Coke");
machine.dispense();
machine.insertCoin(Coin.DIME);
machine.insertCoin(Coin.QUARTER);
machine.selectItem("Pepsi");
machine.cancel();
}
}
π¨οΈ Sample Output
Returning change: 0 cents
Dispensed: Coke
Returning change: 10 cents
β Conclusion & Next Steps
Youβve now built a stateful, object-oriented vending machine using:
- State pattern for behavior transitions
- Enums for coins
- Encapsulation for inventory logic
Top comments (0)