Close
Vending Machine

Design a Vending Machine LLD

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.

Vending Machine States

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.

Vending Machine LLD class digram

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/

Leave a Reply

Your email address will not be published. Required fields are marked *