Introduction
Most of us know what a vending machine is, but for a refresher let’s define it. A vending machine is an automated machine which dispenses shelf items like snacks, chocolates etc when an equal amount inserted via coins, cash or credit card. Design a vending machine is very popular interview question in may product based companies. Lets see in details how to design one?
Functional requirements
- Users should be able to select a desired item from the shelf.
- Vending machines have different locations which have unique id. These locations can keep multiple items of the same type, price.
- Users should be able to use coins and cash to pay for the desired item from the shelf.
- Vending machine should check the paid amount and then decide whether tp allow or reject the dispatch of the item.
- A vending machine should report if a restock is needed for items.
- Vending machine should return money after dispatch of items if any.
- Vending machine owners should be able to configure, arrange the items, and restock.
Design Considerations
Vending Machine States
Looking at the functional requirement, lets first come up with the set of states and then move on to the class diagram.
- ItemSelector – Selects the item via input from the dial pad.
- Payment – Provides payment mode options and does basic validation.
- CoinScanners – Algorithm to calculate the total amount added via coins.
- NoteScanner – Algorithm to calculate the total amount added via Notes.
- Validation – Validates the paid amount against item price, return any excess money.
- Dispatcher – Dispatch the selected item after getting paid.
- StockUpdates – Admins are only allowed to make stock updates.
Note: Scope of the CoinScanner and NoteScanner to identify INR.
Class Diagram
In addition to states defined above we also need to address issue of to how to describe a location with in Vending machine. To address the same LocationConfiguration class contains all the details of any specific location.
Each location is of some type {SMALL, MEDIUM, LARGE} and each location can have multiple items of same type like {BARS, SNACKS, COLD DRINKS}.
States that are defined as class fairly straight forward where each state has to implement execute() method for VendingMachineStates Interface. Also Adding new states later is also very easy with minimal impact to other states.
VendingMachine class take care of keeping the context and client can use the API exposed.
Implementation in Java
This above class dirgam is implemented in details below. Please copy the code in your editor and see for yourself.
Main.java
import working.example.tech.interfaces.TestCaseRunner;
import java.util.*;
import workingexample.tech.LLD.VendingMachine.VendingMachine;
public class Main {
public static void main(String[] args) {
VendingMachine vm = new VendingMachine();
vm.RunTest();
}
}
ItemSelectorState.java
package workingexample.tech.LLD.VendingMachine;
import java.util.HashMap;
import java.util.Scanner;
public class ItemSelectorState implements VendingMachineStates {
VendingMachine mVm;
ItemSelectorState(VendingMachine vm) {
mVm = vm;
}
@Override
public void execute() {
// TODO Auto-generated method stub
System.out.println("****************************");
System.out.println("***** SELECT ITEM STATE ****");
System.out.println("****************************");
HashMap<Integer, LocationConfiguration> locations = mVm.getmLocations();
System.out.print("Enter the location ID: ");
Integer id = (Integer)(mVm.getScanner().nextInt());
if (locations.containsKey(id) && locations.get(id).getItems().size() > 0) {
mVm.setState(new PaymentState(mVm, locations.get(id).getItems().getFirst(), id));
mVm.payUsing();
} else {
mVm.setState(new ErrorState("Incorrect Location selected Or nothing "
+ "present at the selected location", mVm));
mVm.errorState();
}
}
}
PaymentState.java
package workingexample.tech.LLD.VendingMachine;
import java.util.HashMap;
import java.util.Scanner;
public class PaymentState implements VendingMachineStates{
VendingMachine mVm;
LocationConfiguration.Item selectedItem;
int location;
enum PaymentOptions {
COINS_PAYMENT_STATERGY(1),
NOTES_PAYMENT_STATERGY(2),
MAX_PAYMENT_METHODS(3);
int value;
PaymentOptions(int val) {
value = val;
}
int getVal() {return value;}
}
PaymentState(VendingMachine vm, LocationConfiguration.Item selectedItem, int Id) {
mVm = vm;
this.selectedItem = selectedItem;
this.location = Id;
}
public interface PaymentScannerStratergy {
int GetAmount();
}
class CoinsScannerStratergy implements PaymentScannerStratergy {
@Override
public int GetAmount() {
// TODO Auto-generated method stub
System.out.println("Please enter the coins in 1 rupee denominations: ");
int totalCoins = mVm.getScanner().nextInt();
return totalCoins;
}
}
class NoteScannerStratergy implements PaymentScannerStratergy {
@Override
public int GetAmount() {
// TODO Auto-generated method stub
System.out.println("Please enter the Notes in 10 Rupee denomiations: ");
int totalNotesinVal = (mVm.getScanner().nextInt() * 10);
return totalNotesinVal;
}
}
@Override
public void execute() {
// TODO Auto-generated method stub
System.out.println("****************************");
System.out.println("****** PAYMENT STATE *******");
System.out.println("****************************");
System.out.println("Please Insert amount:" + selectedItem.price + " with option \n 1) CoinsInRupee \n 2) NotesInRuppe");
int option = (int)mVm.getScanner().nextInt();
System.out.println(option);
if (option < PaymentOptions.MAX_PAYMENT_METHODS.getVal()) {
if (option == PaymentOptions.COINS_PAYMENT_STATERGY.getVal()) {
CoinsScannerStratergy cs = new CoinsScannerStratergy();
int paid = cs.GetAmount();
mVm.setState(new Validate(mVm, selectedItem, paid, location));
} else if (option == PaymentOptions.NOTES_PAYMENT_STATERGY.getVal()) {
NoteScannerStratergy ns = new NoteScannerStratergy();
int paid = ns.GetAmount();
mVm.setState(new Validate(mVm, selectedItem, paid, location));
}
mVm.validate();
} else {
mVm.setState(new ErrorState("Incorret Payment Method ", mVm));
mVm.errorState();
}
}
}
ItemDispatcher.java
package workingexample.tech.LLD.VendingMachine;
import java.util.HashMap;
public class ItemDispatcher implements VendingMachineStates{
VendingMachine mVm;
LocationConfiguration.Item selectedItem;
int location;
int returnChange;
ItemDispatcher(VendingMachine vm, LocationConfiguration.Item itm, int id, int change) {
mVm = vm;
selectedItem = itm;
location = id;
returnChange = change;
}
@Override
public void execute() {
System.out.println("****************************");
System.out.println("****** DISPATCH STATE ******");
System.out.println("****************************");
// TODO Auto-generated method stub
System.out.println("Dispatching item: " + selectedItem.itemName);
try {
System.out.println("Please wait...");
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("Have nice meal :)");
mVm.getmLocations().get(location).removeItem();
if (returnChange > 0) {
System.out.println("Don't forget to take your change: " + returnChange);
}
mVm.setState(new ItemSelectorState(mVm));
mVm.SelectItems();
}
}
Validate.java
package workingexample.tech.LLD.VendingMachine;
import java.util.Random;
public class Validate implements VendingMachineStates{
VendingMachine mVm;
LocationConfiguration.Item selectedItem;
int paid;
int location;
Validate(VendingMachine vm, LocationConfiguration.Item selectedItem, int paidInRupee, int Id) {
mVm = vm;
this.selectedItem = selectedItem;
this.paid = paidInRupee;
this.location = Id;
}
private boolean checkChangeReturnable(int change) {
Random rnd = new Random();
int number = rnd.nextInt(0, 1000);
if ((number % 2) == 0) {
return true;
} else {
return false;
}
}
@Override
public void execute() {
// TODO Auto-generated method stub
System.out.println("****************************");
System.out.println("****** VALIDATE STATE ******");
System.out.println("****************************");
if (selectedItem.price > paid) {
System.out.println("Payment Validation Failed");
mVm.setState(new ErrorState("Paid less than the selected item", mVm));
mVm.errorState();
} else {
if (selectedItem.price == paid) {
mVm.setState(new ItemDispatcher(mVm, selectedItem, location,0));
System.out.println("Payment Validation success");
mVm.itemDispatch();
} else {
int retAmount = paid = selectedItem.price;
if (checkChangeReturnable(retAmount)) {
mVm.setState(new ItemDispatcher(mVm, selectedItem, location, retAmount));
System.out.println("Payment Validation success");
mVm.itemDispatch();
} else {
System.out.println("Payment Validation Failed");
mVm.setState(new ErrorState("Returnable change in not available in denomiations", mVm));
mVm.errorState();
}
}
}
}
}
RestockState.java
package workingexample.tech.LLD.VendingMachine;
import java.util.HashMap;
import java.util.Scanner;
public class ReStockState implements VendingMachineStates {
VendingMachine mVm;
ReStockState(VendingMachine vm) {
mVm = vm;
}
@Override
public void execute() {
// TODO Auto-generated method stub
HashMap<Integer, LocationConfiguration> locations = mVm.getmLocations();
String in;
System.out.println("****************************");
System.out.println("****** RESTOCK STATE ******");
System.out.println("****************************");
do {
System.out.println("Please enter the location ID: ");
int id = mVm.getScanner().nextInt();
System.out.println("Please enter the name: ");
String name = mVm.getScanner().next();
System.out.println("Please enter the price: ");
int price = mVm.getScanner().nextInt();
LocationConfiguration conf = locations.get(id);
conf.addItem(name, price);
locations.put(id, conf);
System.out.println("Restock done? yes / no");
in = mVm.getScanner().next();
} while(in.compareToIgnoreCase("yes") != 0);
mVm.setmLocations(locations);
mVm.setState(new ItemSelectorState(mVm));
mVm.SelectItems();
}
}
ErrorState.java
package workingexample.tech.LLD.VendingMachine;
public class ErrorState implements VendingMachineStates {
VendingMachine mVm;
String mMsg;
ErrorState(String msg, VendingMachine vm) {
mMsg = msg;
mVm = vm;
}
@Override
public void execute() {
System.out.println("****************************");
System.out.println("******* ERROR STATE *******");
System.out.println("****************************");
System.out.println("Cannot proceed further due to: " + mMsg);
mVm.setState(new ItemSelectorState(mVm));
mVm.SelectItems();
}
}
LocationConfiguration.java
package workingexample.tech.LLD.VendingMachine;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
public class LocationConfiguration {
enum LocationType {
SMALL,
MEDIUM,
LARGE
}
enum ItemType {
SOFTDRINKS,
CHOCOCLATE_BARS,
SNACKS
}
class Item {
String itemName;
int price;
Item(String itemName, int price) {
this.itemName = itemName;
this.price = price;
}
}
private int mId;
private int mMaximumItemsAllowed;
private LocationType mLocationType;
private ItemType mItemType;
private Queue<Item> items;
LocationConfiguration(int id, int maxItems, LocationType in, ItemType type) {
mId = id;
mMaximumItemsAllowed = maxItems;
mLocationType = in;
mItemType = type;
items = new LinkedList<>();
}
public int getmId() {
return mId;
}
public int getmMaximumItemsAllowed() {
return mMaximumItemsAllowed;
}
public LocationType getmLocationType() {
return mLocationType;
}
public ItemType getmItemType() {
return mItemType;
}
public LinkedList<Item> getItems() {return (LinkedList<Item>) items;}
public void addItem(String itemName, int price) {
if (items.size() < mMaximumItemsAllowed) {
items.add(new Item(itemName, price));
} else {
System.out.println("Not allowed!");
}
}
public void removeItem() {
if (items.size() >= 1) {
items.remove();
} else {
System.out.println("Not allowed!");
}
}
}
VendingMachine.java
package workingexample.tech.LLD.VendingMachine;
import java.util.HashMap;
import java.util.Scanner;
import working.example.tech.interfaces.TestCaseRunner;
public class VendingMachine implements TestCaseRunner {
private HashMap<Integer, LocationConfiguration> mLocations;
private VendingMachineStates mCurrentState;
final private Scanner obj = new Scanner(System.in);
void setState(VendingMachineStates st) {
mCurrentState = st;
}
Scanner getScanner() {
return obj;
}
public void setmLocations(HashMap<Integer, LocationConfiguration> mLocations) {
this.mLocations = mLocations;
}
public HashMap<Integer, LocationConfiguration> getmLocations() {
return mLocations;
}
void init() {
// SMALL
mLocations = new HashMap<Integer, LocationConfiguration>();
mLocations.put(100, new LocationConfiguration(100, 4,LocationConfiguration.LocationType.SMALL,
LocationConfiguration.ItemType.CHOCOCLATE_BARS));
mLocations.put(101, new LocationConfiguration(100, 4,LocationConfiguration.LocationType.SMALL,
LocationConfiguration.ItemType.CHOCOCLATE_BARS));
mLocations.put(102, new LocationConfiguration(100, 4,LocationConfiguration.LocationType.SMALL,
LocationConfiguration.ItemType.CHOCOCLATE_BARS));
mLocations.put(103, new LocationConfiguration(100, 4,LocationConfiguration.LocationType.SMALL,
LocationConfiguration.ItemType.CHOCOCLATE_BARS));
// MEDIUM
mLocations.put(200, new LocationConfiguration(200, 4,LocationConfiguration.LocationType.MEDIUM,
LocationConfiguration.ItemType.SNACKS));
mLocations.put(201, new LocationConfiguration(201, 4,LocationConfiguration.LocationType.MEDIUM,
LocationConfiguration.ItemType.SNACKS));
mLocations.put(202, new LocationConfiguration(202, 4,LocationConfiguration.LocationType.MEDIUM,
LocationConfiguration.ItemType.SNACKS));
// LARGE
mLocations.put(300, new LocationConfiguration(300, 6,LocationConfiguration.LocationType.LARGE,
LocationConfiguration.ItemType.SOFTDRINKS));
mLocations.put(301, new LocationConfiguration(301, 6,LocationConfiguration.LocationType.LARGE,
LocationConfiguration.ItemType.SOFTDRINKS));
}
void SelectItems() {mCurrentState.execute();}
void Restock() {mCurrentState.execute();}
void itemDispatch() {mCurrentState.execute();}
void validate() {mCurrentState.execute();}
void payUsing() {mCurrentState.execute();}
void errorState() {mCurrentState.execute();}
void shutdown() {obj.close();}
@Override
public void RunTest() {
// TODO Auto-generated method stub
this.init();
System.out.println("**************************************");
System.out.println("***WORKING EXAMPLE VENDING MACHINE ***");
System.out.println("**************************************");
System.out.println("Please enter 1. Restock(only Admin) 2.Existing Items");
int val = this.getScanner().nextInt();
if (1 == val) {
this.setState(new ReStockState(this));
this.Restock();
} else if (2 == val) {
this.setState(new ItemSelectorState(this));
this.SelectItems();
}
}
@Override
public void showOut() {
// TODO Auto-generated method stub
}
}
VendingMachineStates.java
package workingexample.tech.LLD.VendingMachine;
public interface VendingMachineStates {
void execute();
}
Output
**************************************
***WORKING EXAMPLE VENDING MACHINE ***
**************************************
Please enter 1. Restock(only Admin) 2.Existing Items
1
****************************
****** RESTOCK STATE ******
****************************
Please enter the location ID:
200
Please enter the name:
Lays-salted
Please enter the price:
20
Restock done? yes / no
yes
****************************
***** SELECT ITEM STATE ****
****************************
Enter the location ID: 200
****************************
****** PAYMENT STATE *******
****************************
Please Insert amount:20 with option
1) CoinsInRupee
2) NotesInRuppe
1
1
Please enter the coins in 1 rupee denominations:
25
****************************
****** VALIDATE STATE ******
****************************
Payment Validation success
****************************
****** DISPATCH STATE ******
****************************
Dispatching item: Lays-salted
Please wait...
Have nice meal :)
Don't forget to take your change: 5
****************************
***** SELECT ITEM STATE ****
****************************
Enter the location ID:
Conclusion
This blog we have discussed Vending Machine low level design. Which mostly used the state and strategy design pattern. The are many things that can be improved in this design like for instance restock state can update the LocationConfiguration info in one shot by reading file, instead of asking to add user to add one by one. Restock state should also take care of authenticating an admin user from normal user, and many more.
Related blogs
https://www.workingexample.tech/state-design-pattern/
https://www.workingexample.tech/how-to-write-a-webcrawler-for-large-language-models-at-scale-part-3/
https://www.workingexample.tech/low-level-design-of-youtubes-last-watched-video-feature-part-2/