MindForge
MindForge
DashboardHelp & Support
Dashboard
MindForge

45-Day Interview Preparation Bootcamp

Course progress

0%
0 of 177 modules complete
Start Here0/1
  • How to Use This Course
Data Structures & Algorithms0/27
  • Arrays and Hashing
  • Two Pointers
  • Sliding Window
  • Sliding Window: Harder Problems
  • Binary Search
  • Stacks
  • Practice Test: Arrays, Pointers, Windows, Search and Stacks
  • Trees: Traversals and Core Patterns
  • Binary Search Trees and Rebuilding a Tree
  • Tries (Prefix Trees)
  • Heaps and Priority Queues
  • Graphs: BFS and DFS
  • Topological Sort and Cycle Detection
  • Union-Find (Disjoint Set Union)
  • Practice Test: Trees, Heaps and Graphs
  • Dynamic Programming Basics
  • Dynamic Programming: Two Dimensions
  • Dynamic Programming: Hard String Matching
  • Backtracking
  • Backtracking: Constraint Satisfaction
  • Greedy Algorithms
  • Interval Problems
  • Bit Manipulation
  • Math and Geometry
  • String Manipulation
  • Pattern Recall and Mock Coding Rounds
  • Practice Test: DP, Backtracking, Greedy and More

System Design

Foundations0/16
  • The System Design Interview Framework
  • Back-of-the-Envelope Estimation
  • Networking and Protocols for Design
  • API Design and Idempotency
  • Load Balancing, Proxies, and Scaling Out
  • Caching and CDNs
  • Choosing a Database: Storage, Indexes and Transactions
  • Replication, Partitioning, and Sharding
  • CAP, PACELC, and Levels of Consistency
  • Messaging, Queues, and Event-Driven Architecture
  • Distributed Transactions and Data Integrity
  • Coordination, Consensus, and Distributed Locks
  • Reliability, Resilience, and Rate Limiting
  • Observability, Security, and Cost
  • System Design Cheat Sheet
  • Practice Test: System Design Foundations
Case Studies0/23
  • Design a URL Shortener
  • Design a Rate Limiter
  • Design a Notification Service
  • Design a Job Queue System
  • Design a Chat App (WhatsApp)
  • Design an Analytics Pipeline
  • Design a Multi-tenant SaaS Architecture
  • Design a Social Media Feed (Twitter/X)
  • Design a Search Autocomplete System
  • Design a File Storage Service (Dropbox)
  • Design Google Docs
  • Design a Code Review System
  • Design Netflix
  • Design YouTube
  • Design Spotify
  • Design Uber/Lyft
  • Design Airbnb
  • Design Ticketmaster (Eventbrite)
  • Design an E-commerce Cart, Checkout, and Catalog
  • Design a Payment System
  • Design a Logging and Monitoring System
  • Mock Design Rounds
  • Practice Test: System Design Case Studies
Low-Level Design0/19
  • The LLD Interview Framework and UML
  • OOP Foundations for Design
  • SOLID Principles
  • Creational Patterns
  • Structural Patterns
  • Behavioral Patterns I — Strategy, Observer, Command, State, Template Method
  • Behavioral Patterns II and Anti-Patterns
  • Concurrency in Low-Level Design
  • Design a Parking Lot
  • Design an Elevator System
  • Design a Movie Ticket Booking System
  • Design an Expense-Sharing App (Splitwise)
  • Design a Vending Machine and an ATM
  • Design a Rate Limiter and a Logging Framework
  • Design a Notification Service and an In-Memory Cache
  • Design Games — Tic-Tac-Toe, Snake and Ladder, Chess
  • Database Design for LLD
  • LLD Cheat Sheet and Recall Drill
  • Practice Test: Low-Level Design

Backend

Python0/5
  • How Python Runs Your Code
  • Functions Deep Dive: Closures, Generators, Context Managers, Scope
  • Classes Deep Dive: Method Types, MRO, Metaclasses
  • Standard Library and Serialization
  • Practice Test: Python
Django0/6
  • Django Request Lifecycle
  • Lab: Django Middleware — Request ID & Response Time
  • Django ORM and Querysets
  • Django's Cache Framework
  • Practice Test: Django
  • Django in Production: Debugging Labs
FastAPI0/5
  • FastAPI, ASGI, and Async
  • FastAPI Dependency Injection
  • FastAPI Background Tasks
  • Lab: FastAPI Async — Concurrent Aggregation
  • Practice Test: FastAPI
Databases (PostgreSQL)0/6
  • Indexing
  • Query Optimization
  • Transactions and Isolation
  • Migrations and Schema Changes
  • Hierarchical Data and Safe Concurrent Updates
  • Practice Test: Databases
Caching, Queues & Distributed Systems0/8
  • Redis Caching
  • Redis Advanced Patterns
  • Celery and Background Workers
  • Kafka and Message Queues
  • Async Pipelines
  • Distributed Systems Basics
  • Distributed Locks
  • Practice Test: Caching, Queues and Distributed Systems
APIs, Security & Deployment0/9
  • REST API Design
  • API Versioning and Error Handling
  • API Security and Authentication
  • WebSockets and Real-Time Systems
  • Docker and Containers
  • File Storage with S3
  • Testing Strategies
  • Mock Backend Round
  • Practice Test: APIs, Security and Deployment
Frontend0/38
  • HTML Document Structure and Semantics
  • CSS Box Model, Flexbox and Grid
  • CSS Architecture and Theming
  • CSS and Rendering Performance
  • Variables, Scope, Hoisting, and Truthiness
  • Functions, this, and Binding
  • Objects and Prototypal Inheritance
  • JavaScript Utility Patterns: Cloning, Caching, and Memory
  • Promises From Scratch
  • The Event Loop and Browser Rendering Pipeline
  • The DOM: Nodes, Events, and Traversal
  • Practice Test: HTML, CSS and JavaScript
  • React Rendering Fundamentals
  • React Hooks Internals
  • React Performance
  • Lab: React — Stateful Counter Component
  • State Management
  • State Machines in React
  • React Custom Hooks and Composition Patterns
  • TypeScript for React
  • React Testing
  • Practice Test: React
  • HTTP and Caching
  • Network Performance and Web Vitals
  • Code Splitting and Bundles
  • Virtualization
  • GraphQL and Data Fetching
  • Web Security
  • WebSockets and Real-time
  • SSR and Next.js
  • Micro-frontends
  • Build Tools and Bundling
  • Accessibility
  • Error Handling
  • Internationalization
  • Animation
  • Rapid Recall and Mock Frontend Rounds
  • Practice Test: Performance, Architecture and Production
Behavioral & Interview Day0/14
  • Resume and Project Walkthroughs
  • The STAR Method and Refining Your Stories
  • Conflict and Difficult Coworkers
  • Failures, Incidents, and Obstacles
  • Leadership, Mentorship, and Ownership
  • Teamwork, Communication, and Feedback
  • Growth and Handling Ambiguity
  • Career Goals, Why This Company, and Negotiation
  • Situational Questions
  • Company Research
  • Mock Behavioral Rounds
  • Interview Day
  • After the Interview
  • Practice Test: Behavioral and Interview Day
Design a Vending Machine and an ATM
Course contents

45-Day Interview Preparation Bootcamp

Course progress

0%
0 of 177 modules complete
Start Here0/1
  • How to Use This Course
Data Structures & Algorithms0/27
  • Arrays and Hashing
  • Two Pointers
  • Sliding Window
  • Sliding Window: Harder Problems
  • Binary Search
  • Stacks
  • Practice Test: Arrays, Pointers, Windows, Search and Stacks
  • Trees: Traversals and Core Patterns
  • Binary Search Trees and Rebuilding a Tree
  • Tries (Prefix Trees)
  • Heaps and Priority Queues
  • Graphs: BFS and DFS
  • Topological Sort and Cycle Detection
  • Union-Find (Disjoint Set Union)
  • Practice Test: Trees, Heaps and Graphs
  • Dynamic Programming Basics
  • Dynamic Programming: Two Dimensions
  • Dynamic Programming: Hard String Matching
  • Backtracking
  • Backtracking: Constraint Satisfaction
  • Greedy Algorithms
  • Interval Problems
  • Bit Manipulation
  • Math and Geometry
  • String Manipulation
  • Pattern Recall and Mock Coding Rounds
  • Practice Test: DP, Backtracking, Greedy and More

System Design

Foundations0/16
  • The System Design Interview Framework
  • Back-of-the-Envelope Estimation
  • Networking and Protocols for Design
  • API Design and Idempotency
  • Load Balancing, Proxies, and Scaling Out
  • Caching and CDNs
  • Choosing a Database: Storage, Indexes and Transactions
  • Replication, Partitioning, and Sharding
  • CAP, PACELC, and Levels of Consistency
  • Messaging, Queues, and Event-Driven Architecture
  • Distributed Transactions and Data Integrity
  • Coordination, Consensus, and Distributed Locks
  • Reliability, Resilience, and Rate Limiting
  • Observability, Security, and Cost
  • System Design Cheat Sheet
  • Practice Test: System Design Foundations
Case Studies0/23
  • Design a URL Shortener
  • Design a Rate Limiter
  • Design a Notification Service
  • Design a Job Queue System
  • Design a Chat App (WhatsApp)
  • Design an Analytics Pipeline
  • Design a Multi-tenant SaaS Architecture
  • Design a Social Media Feed (Twitter/X)
  • Design a Search Autocomplete System
  • Design a File Storage Service (Dropbox)
  • Design Google Docs
  • Design a Code Review System
  • Design Netflix
  • Design YouTube
  • Design Spotify
  • Design Uber/Lyft
  • Design Airbnb
  • Design Ticketmaster (Eventbrite)
  • Design an E-commerce Cart, Checkout, and Catalog
  • Design a Payment System
  • Design a Logging and Monitoring System
  • Mock Design Rounds
  • Practice Test: System Design Case Studies
Low-Level Design0/19
  • The LLD Interview Framework and UML
  • OOP Foundations for Design
  • SOLID Principles
  • Creational Patterns
  • Structural Patterns
  • Behavioral Patterns I — Strategy, Observer, Command, State, Template Method
  • Behavioral Patterns II and Anti-Patterns
  • Concurrency in Low-Level Design
  • Design a Parking Lot
  • Design an Elevator System
  • Design a Movie Ticket Booking System
  • Design an Expense-Sharing App (Splitwise)
  • Design a Vending Machine and an ATM
  • Design a Rate Limiter and a Logging Framework
  • Design a Notification Service and an In-Memory Cache
  • Design Games — Tic-Tac-Toe, Snake and Ladder, Chess
  • Database Design for LLD
  • LLD Cheat Sheet and Recall Drill
  • Practice Test: Low-Level Design

Backend

Python0/5
  • How Python Runs Your Code
  • Functions Deep Dive: Closures, Generators, Context Managers, Scope
  • Classes Deep Dive: Method Types, MRO, Metaclasses
  • Standard Library and Serialization
  • Practice Test: Python
Django0/6
  • Django Request Lifecycle
  • Lab: Django Middleware — Request ID & Response Time
  • Django ORM and Querysets
  • Django's Cache Framework
  • Practice Test: Django
  • Django in Production: Debugging Labs
FastAPI0/5
  • FastAPI, ASGI, and Async
  • FastAPI Dependency Injection
  • FastAPI Background Tasks
  • Lab: FastAPI Async — Concurrent Aggregation
  • Practice Test: FastAPI
Databases (PostgreSQL)0/6
  • Indexing
  • Query Optimization
  • Transactions and Isolation
  • Migrations and Schema Changes
  • Hierarchical Data and Safe Concurrent Updates
  • Practice Test: Databases
Caching, Queues & Distributed Systems0/8
  • Redis Caching
  • Redis Advanced Patterns
  • Celery and Background Workers
  • Kafka and Message Queues
  • Async Pipelines
  • Distributed Systems Basics
  • Distributed Locks
  • Practice Test: Caching, Queues and Distributed Systems
APIs, Security & Deployment0/9
  • REST API Design
  • API Versioning and Error Handling
  • API Security and Authentication
  • WebSockets and Real-Time Systems
  • Docker and Containers
  • File Storage with S3
  • Testing Strategies
  • Mock Backend Round
  • Practice Test: APIs, Security and Deployment
Frontend0/38
  • HTML Document Structure and Semantics
  • CSS Box Model, Flexbox and Grid
  • CSS Architecture and Theming
  • CSS and Rendering Performance
  • Variables, Scope, Hoisting, and Truthiness
  • Functions, this, and Binding
  • Objects and Prototypal Inheritance
  • JavaScript Utility Patterns: Cloning, Caching, and Memory
  • Promises From Scratch
  • The Event Loop and Browser Rendering Pipeline
  • The DOM: Nodes, Events, and Traversal
  • Practice Test: HTML, CSS and JavaScript
  • React Rendering Fundamentals
  • React Hooks Internals
  • React Performance
  • Lab: React — Stateful Counter Component
  • State Management
  • State Machines in React
  • React Custom Hooks and Composition Patterns
  • TypeScript for React
  • React Testing
  • Practice Test: React
  • HTTP and Caching
  • Network Performance and Web Vitals
  • Code Splitting and Bundles
  • Virtualization
  • GraphQL and Data Fetching
  • Web Security
  • WebSockets and Real-time
  • SSR and Next.js
  • Micro-frontends
  • Build Tools and Bundling
  • Accessibility
  • Error Handling
  • Internationalization
  • Animation
  • Rapid Recall and Mock Frontend Rounds
  • Practice Test: Performance, Architecture and Production
Behavioral & Interview Day0/14
  • Resume and Project Walkthroughs
  • The STAR Method and Refining Your Stories
  • Conflict and Difficult Coworkers
  • Failures, Incidents, and Obstacles
  • Leadership, Mentorship, and Ownership
  • Teamwork, Communication, and Feedback
  • Growth and Handling Ambiguity
  • Career Goals, Why This Company, and Negotiation
  • Situational Questions
  • Company Research
  • Mock Behavioral Rounds
  • Interview Day
  • After the Interview
  • Practice Test: Behavioral and Interview Day
Browsing without an account — progress is saved in this browser only.Sign in to save it

Design a Vending Machine and an ATM

Reading50 min

These two problems are really the same problem wearing different clothes: a physical machine with a strict lifecycle, where nearly every bug turns out to be an illegal transition — "dispensed without payment," "cash released without a matching debit." They're the textbook use case for the State pattern, and each one adds a genuinely interesting algorithm on top — coin change for the vending machine, cash denomination selection for the ATM.

Step 1 — Requirements and the state machine

Vending machine — what it needs to do:

  • Show the products on offer, with their prices and how many are left.
  • Accept coins or notes one at a time, and show the running total.
  • Let the customer pick a product: dispense it and hand back the right change, or refuse with a clear reason.
  • Cancel at any point before dispensing: refund everything that was put in.
  • Admin actions: restock products, refill change, collect the cash.

Here's the state machine, drawn before writing a single class:

        ┌──────── refund / cancel ─────────┐
        ▼                                   │
     IDLE ──insert coin──▶ HAS_MONEY ──select──▶ DISPENSING ──▶ IDLE
       ▲                       │  ▲                     │
       │                       └──┘ (more coins)        │
       └──────────── out of stock / insufficient ───────┘
                     (stay, with a message)

   Any state ──admin──▶ OUT_OF_SERVICE ──service done──▶ IDLE

Every rule the machine has to follow is either an edge on that diagram, or a deliberately missing one:

Attempted action Current state Correct behaviour
Select a product IDLE, with no money in Refuse: "insert money first"
Insert a coin DISPENSING Refuse: "please wait"
Cancel DISPENSING Refuse: it's already committed
Select an out-of-stock item HAS_MONEY Stay in HAS_MONEY, show a message, keep the money
Select with not enough credit HAS_MONEY Stay, show exactly how much more is needed
The machine can't make exact change HAS_MONEY Refuse the sale and refund — never quietly short-change the customer

That last row is the interesting one. "I can sell it to you, but I can't give you your change back" must never mean the machine quietly keeps the difference. The standard fix is to refuse the sale and refund everything, or offer the customer store credit instead. Raising this yourself, unprompted, shows you were thinking about the actual domain, not just the code.

Remember: if the machine can't make exact change, it refuses the sale and refunds — it never quietly keeps the difference, and it never dispenses without giving full change.

Sign in to try this knowledge check.

Step 2 — The State pattern implementation

Each state is its own class implementing the same interface. The machine simply hands off to whatever its current state is, and the states themselves decide what transitions are legal. This is exactly what makes illegal transitions impossible to write by accident, instead of merely checked everywhere.

Python
from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class Product:
    code: str
    name: str
    price_paise: int
    stock: int


class VendingState(ABC):
    """Every action has a default: a clear rejection. States only override what's actually legal."""
    def insert(self, machine: "VendingMachine", paise: int) -> str:
        return "cannot insert money right now"
    def select(self, machine: "VendingMachine", code: str) -> str:
        return "cannot select right now"
    def cancel(self, machine: "VendingMachine") -> str:
        return "nothing to cancel"
    @abstractmethod
    def name(self) -> str: ...


class IdleState(VendingState):
    def name(self) -> str: return "IDLE"
    def insert(self, machine, paise):
        machine.accept(paise)                 # held in escrow, not the vault, until the sale commits
        machine.state = HasMoneyState()
        return f"credit {machine.credit}"
    def select(self, machine, code):
        return "insert money first"


class HasMoneyState(VendingState):
    def name(self) -> str: return "HAS_MONEY"

    def insert(self, machine, paise):
        machine.accept(paise)
        return f"credit {machine.credit}"

    def select(self, machine, code):
        product = machine.products.get(code)
        if product is None:
            return "unknown product"
        if product.stock == 0:
            return "out of stock"                        # stays in HAS_MONEY, keeps the credit
        if machine.credit < product.price_paise:
            return f"need {product.price_paise - machine.credit} more"

        change_due = machine.credit - product.price_paise
        coins = machine.plan_change(change_due)
        if coins is None:                                 # exact change isn't possible
            refund = machine.refund_all()
            machine.state = IdleState()
            return f"cannot make change; refunded {refund}"

        machine.state = DispensingState()
        return machine.state.dispense(machine, product, coins, change_due)

    def cancel(self, machine):
        refund = machine.refund_all()
        machine.state = IdleState()
        return f"refunded {refund}"


class DispensingState(VendingState):
    def name(self) -> str: return "DISPENSING"

    def dispense(self, machine, product, coins, change_due) -> str:
        product.stock -= 1
        machine.commit_escrow()                           # the escrowed coins now join the vault
        for denom, count in coins.items():                # remove exactly the change handed out
            machine.coin_box[denom] -= count
        machine.credit = 0
        machine.state = IdleState()
        return f"dispensed {product.name}, change {change_due}"


class VendingMachine:
    def __init__(self, products: list[Product], coins: dict[int, int]):
        self.products = {p.code: p for p in products}
        self.coin_box = dict(coins)          # the vault: denomination (paise) -> count
        self.escrow: dict[int, int] = {}     # coins inserted but not yet committed to a sale
        self.credit = 0
        self.state: VendingState = IdleState()

    def accept(self, paise: int) -> None:
        self.escrow[paise] = self.escrow.get(paise, 0) + 1
        self.credit += paise

    def commit_escrow(self) -> None:
        for denom, count in self.escrow.items():
            self.coin_box[denom] = self.coin_box.get(denom, 0) + count
        self.escrow.clear()

    # --- delegation: the machine itself never makes a decision ---------------
    def insert(self, paise: int) -> str: return self.state.insert(self, paise)
    def select(self, code: str) -> str: return self.state.select(self, code)
    def cancel(self) -> str: return self.state.cancel(self)

    def plan_change(self, amount: int) -> dict[int, int] | None:
        """Greedy over the coins on hand. Returns None if exact change isn't possible."""
        remaining, plan = amount, {}
        for denom in sorted(self.coin_box, reverse=True):
            take = min(remaining // denom, self.coin_box[denom])
            if take:
                plan[denom] = take
                remaining -= denom * take
        return plan if remaining == 0 else None

    def refund_all(self) -> int:
        """Hand back exactly the coins put in — the escrow is why a refund is always possible."""
        refund, self.credit = self.credit, 0
        self.escrow.clear()
        return refund


machine = VendingMachine(
    products=[Product("A1", "chips", 3_000, stock=1), Product("A2", "cola", 5_000, stock=0)],
    coins={1_000: 5, 500: 2, 100: 10},
)

assert machine.select("A1") == "insert money first"        # illegal while in IDLE
assert machine.state.name() == "IDLE"

machine.insert(2_000)
assert machine.state.name() == "HAS_MONEY"
assert machine.select("A1") == "need 1000 more"            # stays in HAS_MONEY
assert machine.select("A2") == "out of stock"              # the credit is untouched

machine.insert(2_000)                                       # credit is now 4000
assert machine.select("A1") == "dispensed chips, change 1000"
assert machine.state.name() == "IDLE" and machine.credit == 0
assert machine.products["A1"].stock == 0

machine.insert(1_000)
assert machine.cancel() == "refunded 1000"                  # cancel refunds everything put in
assert machine.state.name() == "IDLE"
print("final state:", machine.state.name(), "| vault:", machine.coin_box, "| escrow:", machine.escrow)

Notice what the pattern buys you: there's no if state == anywhere inside VendingMachine. Adding a MAINTENANCE state means one new class, and not a single existing method has to change.

Remember: giving every action a safe default means a new state can never accidentally forget to handle one. Its overrides become a readable list of exactly what's legal.

Sign in to try this knowledge check.

Step 3 — The ATM: the same machine, harder money

What it needs to do: authenticate a card and PIN, check the balance, withdraw cash, deposit, print a receipt, eject the card. States: IDLE → CARD_INSERTED → AUTHENTICATED → TRANSACTION → DISPENSING → IDLE, plus OUT_OF_SERVICE.

Three things make the ATM more than just a vending machine wearing a different skin:

1. Authentication is a state with a retry counter built in. Three wrong PINs in a row has to capture or block the card. That counter lives on the session, not on the card itself, and being blocked is a real state transition — not just a boolean checked in five different places.

2. Dispensing cash is a denomination problem, not just subtraction. Withdrawing ₹3,700 from a machine holding ₹2,000, ₹500, and ₹100 notes needs an actual plan for which notes to use, and the machine has to refuse cleanly whenever it can't make up that amount from what it's holding.

Greedy, meaning always take the biggest note first, works fine for the standard Indian note set, and most real currencies in general, but it's not correct in general. For denominations {1, 3, 4} and an amount of 6, greedy gives you 4+1+1, three notes, while the actual best answer is 3+3, just two notes. The general answer is a coin-change dynamic program. Say both: greedy for a standard, well-behaved currency, and DP whenever the denomination set is arbitrary or you genuinely need the minimum note count. This single observation is one of the highest-value things you can say in this whole problem, because it ties LLD directly back to the coin-change pattern from the algorithms section.

3. The debit and the dispense can never be allowed to disagree. The failure that actually matters here: the account gets debited, and then the cash dispenser jams. The correct sequence is reserve, then dispense, then commit — with a compensating credit if the dispense fails. This is the saga pattern from the distributed-transactions HLD lesson, in miniature. Bringing this up yourself is the senior move in this whole problem.

Python
def plan_notes(amount: int, inventory: dict[int, int]) -> dict[int, int] | None:
    """Greedy note selection, limited by what the machine actually holds."""
    remaining, plan = amount, {}
    for note in sorted(inventory, reverse=True):
        take = min(remaining // note, inventory[note])
        if take:
            plan[note] = take
            remaining -= note * take
    return plan if remaining == 0 else None


def min_notes_dp(amount: int, denominations: list[int]) -> int | None:
    """Unbounded coin change: the provably minimal note count, for when greedy isn't safe."""
    INF = float("inf")
    best = [0] + [INF] * amount
    for value in range(1, amount + 1):
        for d in denominations:
            if d <= value and best[value - d] + 1 < best[value]:
                best[value] = best[value - d] + 1
    return None if best[amount] == INF else int(best[amount])


inventory = {2_000: 3, 500: 4, 100: 10}
assert plan_notes(3_700, inventory) == {2_000: 1, 500: 3, 100: 2}
assert plan_notes(50, inventory) is None                 # smaller than the smallest note
assert plan_notes(100_000, inventory) is None            # more than the machine even holds

# Where greedy gets it wrong, and DP gets it right:
assert min_notes_dp(6, [1, 3, 4]) == 2                   # 3 + 3
greedy_count = sum(plan_notes(6, {4: 5, 3: 5, 1: 5}).values())
assert greedy_count == 3                                 # 4 + 1 + 1
print("greedy notes:", greedy_count, "| optimal:", min_notes_dp(6, [1, 3, 4]))

Remember: greedy note selection only works because real currencies are built to make it work. For an arbitrary denomination set, only dynamic programming guarantees the minimum.

Sign in to try this knowledge check.

Steps 4 and 5 — Concurrency, edge cases, and extensions

Concurrency. A single physical vending machine only ever serves one customer at a time, so the interesting version of this question is really about the ATM network as a whole:

Concern How it's handled
Two ATMs withdrawing from the same account at once The account balance is the shared, changeable data, and it lives in the bank's own database: UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1 — zero rows affected means insufficient funds, checked and applied atomically
Cash inventory inside a single machine A lock (or a single-threaded controller) around planning and dispensing together — this is a check-then-act sequence
Card session state Scoped to that one session, with nothing shared
Debit succeeds but the dispense fails A compensating credit; the whole transaction is a saga with an explicit RESERVED state, and a reconciliation process catches anything the compensation missed

Edge cases worth raising yourself:

Case How it's handled
Power failure mid-dispense Write down the intent before acting on it. On restart, compare that journal against the physical cash count
Card left in the machine A timeout, then the card gets captured and the machine returns to IDLE
A note gets jammed Enter OUT_OF_SERVICE and raise an alert — never silently retry
Daily withdrawal limits A policy object, checked before the funds are even reserved
Coin box full, or running low on cash Admin-configured thresholds and an alert; refuse denominations that would overflow the box
A product's price changes mid-selection Read the price once, at the moment of selection, and use that same value for the whole transaction

Extensions, and how the design absorbs each one:

  • Card payments on the vending machine → a PaymentMethod interface (CoinPayment, CardPayment, UpiPayment); the state machine doesn't change, since every state only cares whether "credit is sufficient," never how that credit arrived.
  • Multiple currencies or regions → a denomination set passed in as configuration, not hardcoded — which is exactly why plan_change loops over self.coin_box instead of a fixed list.
  • Remote telemetry and restocking alerts → Observers on stock levels and cash levels changing.
  • A maintenance mode → one new state class.
  • Different dispensing hardware → an interface sitting behind DispensingState.

Remember: debit-then-dispense is a saga, not a single transaction. Reserve, act, commit — and have a compensating credit ready for when it fails.

Sign in to try this knowledge check.

Quick recap

  • Draw the state machine before you write a single class. Both of these problems are lifecycles at heart, and the diagram is the design — every rule is either an edge, or a deliberately missing one.
  • State pattern with a rejecting base class means every state handles every action, illegal transitions become impossible to write by accident, and a new state is one new class with zero edits anywhere else.
  • The change or note-selection problem is the algorithmic core: greedy, limited by what's actually on hand, refusing cleanly when exact change isn't possible — plus the honest caveat that greedy is only optimal for well-designed denomination sets.
  • Never silently keep the customer's money. Check whether change is available right alongside stock and credit, before anything actually commits.
  • The ATM's debit-then-dispense is a saga. Reserve, act, commit, compensate, and reconcile — the exact same reasoning as any cross-system workflow, which is why this problem shows up in backend interviews just as often as LLD ones.
PreviousDesign an Expense-Sharing App (Splitwise)NextDesign a Rate Limiter and a Logging Framework

Course progress

0%
0 of 177 modules complete

On this page

  • Step 1 — Requirements and the state machine
  • Step 2 — The State pattern implementation
  • Step 3 — The ATM: the same machine, harder money
  • Steps 4 and 5 — Concurrency, edge cases, and extensions
  • Quick recap