Skip to main content
Contents4 / 15 ▾
04
Easy · 4 of 5

ATM Machine: State Machine Meets Chain of Responsibility

An ATM session walks a strict lifecycle — Idle → CardInserted → PinAuthenticated → SelectOperation → back to Idle — while every withdrawal is settled by a second, independent pattern: a chain of denomination handlers that dispense the largest notes first and pass whatever is left down the line.

4.1Problem Statement

Design the control flow of an ATM — from the moment a card slides into the slot to the moment cash lands in the tray. It reads like a UI-flow exercise, but the interesting part never touches a screen: it is the machine's internal life cycle, and the arithmetic of turning "please give me ₹2600" into an actual stack of notes.

An ATM session is a five-state lifecycle (Idle → CardInserted → PinAuthenticated → SelectOperation → TransactionComplete → Idle) driving one shared context, and every withdrawal is settled by a chain of denomination handlers that each dispense what they can and forward the remainder down the line.

Functional requirements:

  • Session states: Idle → CardInserted → PinAuthenticated → SelectOperation → TransactionComplete → Idle.
  • Three operations: check balance, withdraw cash, deposit. The user can eject the card / cancel at (almost) any point in the flow.
  • A withdrawal must validate against both the account's logical balance and the machine's physical available cash.
  • Dispense the requested amount using the available denominations, preferring fewer notes — e.g. hand out ₹2000 notes before ₹500 before ₹100.

Non-functional constraints, which are the actual point of the exercise:

  • The session flow should be a State pattern, not nested ifs — a switch on session status is a fail.
  • The dispenser should be a Chain of Responsibility: a ₹2000 handler → a ₹500 handler → a ₹100 handler, each dispensing what it can of the remaining amount and forwarding whatever is left to the next link.

The expected public interface, given up front so the design effort goes into behavior, not signatures:

enum class OpType { CHECK_BALANCE, WITHDRAW, DEPOSIT };

class ATM {
public:
    void insertCard(const Card& c);
    void enterPin(const std::string& pin);
    void selectOperation(OpType op);
    void withdraw(int amount);
    void ejectCard();
};

class CashDispenseHandler {       // Chain of Responsibility node
public:
    void setNext(CashDispenseHandler* next);
    virtual void dispense(int amount) = 0;   // Note2000Handler, Note500Handler, Note100Handler
    virtual ~CashDispenseHandler() = default;
protected:
    CashDispenseHandler* next_ = nullptr;
};

What the interviewer is really testing:

  • A clean State pattern for the session — one context, one state interface, concrete states that each answer every operation by either doing the work and transitioning, or rejecting cleanly.
  • The Chain of Responsibility dispenser. This is the detail that separates a strong answer from a basic one; folding dispensing into a loop inside the ATM class misses the ask.
  • Correct handling of the case where the account balance is fine but the machine physically cannot make the amount from the notes it has left.

Follow-up questions to expect once the base design stands:

  • The amount cannot be made from the available denominations — reject the whole transaction, with no partial dispense.
  • Concurrent access to a shared cash store (multiple ATMs against one account, or a thread-safe dispenser).
  • Transaction logging and rollback if a dispense fails partway through.

4.2How to Approach

Finding the Nouns

Mine the problem statement for its hard constraints before drawing a single class. Two sentences do all the work: "the session flow should be a State pattern" turns the card→pin→operation→done lifecycle into one Context plus a State interface plus concrete states — the same shape as a vending machine. "The dispenser should be a Chain of Responsibility" names the second pattern outright, and tells you not to fold dispensing into the ATM with a loop.

Circle the remaining nouns — card, account, PIN, bank, cash, denomination, note, handler — and they split cleanly along the seam the two named patterns already drew: session control (Card, Account, Bank, the ATMState family) versus cash mechanics (CashInventory, the CashDispenseHandler family). Each family only meets inside the ATM context.

When a prompt names two patterns, it's telling you the system splits along a seam. Find the seam — here, "session control" vs. "cash mechanics" — and let each pattern own one side.

Pinning the Interface

The public ATM methods are given, and the realization worth having early is that none of them should contain session logic — each one forwards to whatever state is current:

void enterPin(const std::string& pin) { state_->enterPin(*this, pin); }

The dispenser gets its own tiny contract: a chain node that only needs to know its successor and how to dispense. Two contracts, two responsibilities — the ATM's methods delegate session flow, CashDispenseHandler::dispense is recursive by composition, and neither should leak into the other.

Modeling the Leaf Classes

Build bottom-up: the dependency-free types first. Card is just a card number plus an account id; Account is an id, a PIN, and a balance. Bank hides the account map behind questions — authenticate, balance, debit, credit — so the ATM never touches an account directly, the same encapsulation discipline as an inventory class in a vending machine.

CashInventory tracks note counts per denomination:

class CashInventory {
public:
    void load(int denom, int count) { notes_[denom] += count; }
    int  count(int denom) const { /* 0 if absent */ }
    void remove(int denom, int count) { notes_.at(denom) -= count; }
private:
    std::unordered_map<int, int> notes_;   // denomination -> note count
};

Keeping CashInventory (physical notes) separate from Account::balance (logical funds) is what makes "balance OK but no notes" expressible at all. Conflate the two and that whole failure mode becomes impossible to represent, let alone handle correctly.

The Key Insight: Chain of Responsibility for Denomination Dispensing

This is the differentiator, and it deserves real thought. The naive answer loops over denominations largest-first, subtracting notes until the amount hits zero — it works, but it bakes the denomination list and the greedy logic into one function. The problem explicitly asks for a chain, and the chain buys open/closed extensibility: adding a ₹200 note later means inserting one new link, and touching nothing else.

Each handler owns exactly one denomination. On dispense(amount) it computes how many of its own notes it can use (min(amount / myDenom, notesAvailable)), hands those out, subtracts them from the amount, and passes whatever remains to next_ — if there is a next, and if anything is actually left:

void dispense(int amount) override {
    int give = std::min(amount / denom, inv_.count(denom));
    if (give > 0) { inv_.remove(denom, give); amount -= give * denom; }
    if (amount > 0 && next_) next_->dispense(amount);   // pass the remainder down
}

The chain order is the greedy "fewest notes" rule: the largest denomination gets first pick of the amount, consuming as much as it can before the leftover is even offered to the next link. Wiring the chain is nothing more than a run of setNext calls, largest denomination first.

Chain of Responsibility = each link does its bit and forwards the rest. Reach for it when a request is satisfied by a sequence of partial handlers — dispensing, logging filters, approval tiers.

Feasibility Before Mutation

Dispense is destructive: a chain that hands out ₹2000 notes and only then discovers it cannot make the last ₹100 has already emptied real cash it cannot put back. So the chain never runs until feasibility has been proven on a snapshot. Simulate the same greedy pass over copies of the note counts; if the simulation cannot reach exactly zero, reject the whole withdrawal before anything moves:

bool canDispense(const CashInventory& inv, int amount) {
    for (int d : {2000, 500, 100}) {
        int give = std::min(amount / d, inv.count(d));
        amount -= give * d;
    }
    return amount == 0;
}

A withdrawal then runs three guards in order, and only after all three pass does anything mutate: the amount must be positive and a multiple of the smallest note, the account balance must cover it, and canDispense must say yes. Check, then mutate — never the other way around, for any operation that touches real money.

Wiring the ATM Context

The ATM context owns both class families through unique_ptr members — four state singletons and three chain handlers — and exposes only non-owning raw pointers for "who is current" and "where does dispensing start." Both families get built and wired in the constructor, so the chain head is the only entry point anyone outside ever needs:

ATM::ATM(Bank& bank, CashInventory& cash) : bank_(bank), cash_(cash) {
    idle_ = std::make_unique<IdleState>();
    // ... other states ...
    state_ = idle_.get();                        // start Idle

    h2000_ = std::make_unique<Note2000Handler>(cash_);
    h500_  = std::make_unique<Note500Handler>(cash_);
    h100_  = std::make_unique<Note100Handler>(cash_);
    h2000_->setNext(h500_.get());
    h500_->setNext(h100_.get());
    dispenserHead_ = h2000_.get();                // largest-first = fewest notes
}
Own long-lived objects once via unique_ptr members; pass raw, non-owning pointers for "who's current" / "chain head." Build and wire the chain in the constructor so the head is the only entry point.

Follow-Ups: Concurrency and Rollback

The base design already answers follow-up one — an unmakeable amount is rejected outright, because canDispense runs on a snapshot before any debit or remove. The other two follow-ups are where senior candidates separate themselves.

Concurrent access. Bank and CashInventory are held by reference inside ATM precisely because they are shared — which means two machines can race for the last ₹2000 note if check-then-dispense isn't atomic. The fix is a critical section, not a redesign: wrap canDispense plus the chain run (and the balance debit) under one mutex owned by the shared resource, or use a compare-and-swap per denomination. The State and Chain families stay untouched; a lock goes around the existing mutation.

Logging and rollback. Feasibility being checked first means a correct run can't fail mid-chain — but hardware jams happen anyway. Make the operation transactional: journal the debit and the intended notes before dispensing starts, and if the chain throws partway through, roll back by crediting the account back and restoring counts from the journal, then surface the error. Because both the debit and the note removals are reversible, rollback is just a replay in reverse.

Notice the shape of both answers: a lock around a resource, a journal beside a mutation. Every follow-up maps onto a seam that already exists in the design — never a rewrite. That is exactly what an interviewer is listening for.

4.3Whole Solution

The reference solution below is scoped to the cash-dispensing chain — the piece of this problem most likely to separate a strong answer from a basic one — implemented as a standalone, self-contained program with its own main(). It uses ₹1000 / ₹500 / ₹200 / ₹100 handlers rather than the ₹2000 / ₹500 / ₹100 example used to explain the pattern above; same chain, different note values, same greedy largest-first rule.

#include <bits/stdc++.h>
using namespace std;

class MoneyHandler{
    protected:
        MoneyHandler* nextHandler;
    public: 
        MoneyHandler(){
            this->nextHandler = nullptr;
        }

        void setNextHandler(MoneyHandler* next){
            this->nextHandler = next;
        }

        virtual void dispense(int amount) = 0;
};


class ThousandHandler : public MoneyHandler{
    private:
        int numNotes;

    public:
        ThousandHandler(int numNotes){
            this->numNotes = numNotes;
        }

        void dispense(int amount){
            int notesNeeded= amount/1000;

            if(notesNeeded > numNotes){
                notesNeeded = numNotes;
                numNotes =0; 
            }else{
                numNotes-=notesNeeded;
            }

            if(notesNeeded>0){
                cout << "Despensing " << notesNeeded << " x 1000 Notes" << endl;
            }
            int remainingAmt =  amount - (notesNeeded*1000);

            if(remainingAmt>0){
                if(nextHandler!= nullptr){
                    nextHandler->dispense(remainingAmt);
                }else{
                    cout << "Remaining amount " << remainingAmt << " can't be fullfilled" << endl;
                }
            }
        }
};

class FiveHundresHandler : public MoneyHandler{
    private:
        int numNotes;

    public:
        FiveHundresHandler(int numNotes){
            this->numNotes = numNotes;
        }

        void dispense(int amount){
            int notesNeeded= amount/500;

            if(notesNeeded > numNotes){
                notesNeeded = numNotes;
                numNotes =0; 
            }else{
                numNotes-=notesNeeded;
            }

            if(notesNeeded>0){
                cout << "Despensing " << notesNeeded << " x 500 Notes" << endl;
            }
            int remainingAmt =  amount - (notesNeeded*500);

            if(remainingAmt>0){
                if(nextHandler!= nullptr){
                    nextHandler->dispense(remainingAmt);
                }else{
                    cout << "Remaining amount " << remainingAmt << " can't be fullfilled" << endl;
                }
            }
        }
};



class TwoHundresHandler : public MoneyHandler{
    private:
        int numNotes;

    public:
        TwoHundresHandler(int numNotes){
            this->numNotes = numNotes;
        }

        void dispense(int amount){
            int notesNeeded= amount/200;

            if(notesNeeded > numNotes){
                notesNeeded = numNotes;
                numNotes =0; 
            }else{
                numNotes-=notesNeeded;
            }

            if(notesNeeded>0){
                cout << "Despensing " << notesNeeded << " x 200 Notes" << endl;
            }
            int remainingAmt =  amount - (notesNeeded*200);

            if(remainingAmt>0){
                if(nextHandler!= nullptr){
                    nextHandler->dispense(remainingAmt);
                }else{
                    cout << "Remaining amount " << remainingAmt << " can't be fullfilled" << endl;
                }
            }
        }
};


class HundresHandler : public MoneyHandler{
    private:
        int numNotes;

    public:
        HundresHandler(int numNotes){
            this->numNotes = numNotes;
        }

        void dispense(int amount){
            int notesNeeded= amount/100;

            if(notesNeeded > numNotes){
                notesNeeded = numNotes;
                numNotes =0; 
            }else{
                numNotes-=notesNeeded;
            }

            if(notesNeeded>0){
                cout << "Despensing " << notesNeeded << " x 100 Notes" << endl;
            }
            int remainingAmt =  amount - (notesNeeded*100);

            if(remainingAmt>0){
                if(nextHandler!= nullptr){
                    nextHandler->dispense(remainingAmt);
                }else{
                    cout << "Remaining amount " << remainingAmt << " can't be fullfilled" << endl;
                }
            }
        }
};

int main(){

    MoneyHandler* thousandHandler = new ThousandHandler(3);
    MoneyHandler* fiveHundredHandler = new FiveHundresHandler(5);
    MoneyHandler* twoHundredHandler = new TwoHundresHandler(10);
    MoneyHandler* hundredHandler = new HundresHandler(20);

    thousandHandler->setNextHandler(fiveHundredHandler);
    fiveHundredHandler->setNextHandler(twoHundredHandler);
    twoHundredHandler->setNextHandler(hundredHandler);

    int amtToWithdraw =2800;

    cout << "Despensing Amount: " <<amtToWithdraw << endl;
    thousandHandler->dispense(amtToWithdraw);
    return 0;

}
Why build the dispenser as a Chain of Responsibility instead of one function that loops over denominations?

A loop works for today's three or four denominations, but it bakes the note list and the greedy order into a single function. Chain of Responsibility gives the same behavior with open/closed extensibility: adding a new note value means writing one new handler and inserting it into the chain, not touching existing logic. It also matches the general shape of "a request satisfied by a sequence of partial handlers," which shows up again in logging filters and approval pipelines.

The account has enough balance, but the ATM can't make the exact amount from its notes. What happens?

The transaction is rejected in full, before any mutation. A feasibility check runs the same greedy algorithm on a snapshot of the note counts; if it can't reach exactly zero remaining, the withdrawal fails with no debit and no partial dispense. Only after that check (and the balance check) pass does the real chain run against the real inventory.

How would you make withdrawals safe across multiple ATMs sharing one account and one cash store?

Wrap the whole check-then-dispense sequence — the feasibility check, the balance debit, and the chain's dispense call — in one critical section guarded by a mutex owned by the shared Bank and CashInventory. The State and Chain class families don't change at all; the fix is a lock placed around the existing mutation path, which is the general pattern for hardening any correctly-designed system against concurrency once it's proven correct single-threaded.