"Design a coffee vending machine. It offers several beverages, each requiring specific quantities of ingredients (water, milk, coffee, sugar). A user selects a beverage, pays, and the machine dispenses it — or rejects the request if ingredients are insufficient."
B. Difficulty
Beginner → Intermediate. Deliberately the smallest, most contained problem in this set — often used as a warm-up or as the "textbook State pattern" round, precisely because the machine's behavior really does change shape by state in a way that's hard to fake with a simple boolean flag.
C. What This Problem Tests
Whether a candidate reaches for the State pattern when a switch-on-status-field would clearly rot
Judgment about when not to add a pattern — Template Method looks tempting here and is a considered, deliberate rejection
Atomic ingredient reservation, the same check-then-act race shape as every resource-allocation problem in this set, at a much smaller scale
Strategy for payment methods without over-building a payments system
D. Requirements
Functional
Offer several beverages, each with a fixed recipe (ingredient → required quantity)
Let a user select a beverage and pay via cash or card
Dispense the beverage if and only if all required ingredients are sufficiently stocked
Reject the selection (and refund, if paid) if ingredients are insufficient
Allow a technician to refill ingredients
Non-Functional
The machine must never dispense a beverage it can't fully complete (no half-made drinks)
Ingredient inventory must not go negative even if operations overlap (multi-slot machines)
Adding a new beverage must not require code changes beyond adding a recipe
E. Clarifying Questions
#
Question
Why It Matters
Design Decision Affected
1
Does the machine have one brewing unit, or multiple slots that can brew concurrently sharing one ingredient hopper?
Determines whether concurrency actually matters here or is a stretch extension
Scope of W (Concurrency)
2
What payment methods are required — cash, card, mobile wallet?
Determines the PaymentStrategy set
Payment interface scope
3
What happens on a mid-dispense failure (e.g. physically jams)?
Determines whether reserved ingredients need to be "refunded" back to inventory
State machine's failure transition
4
Should low-inventory trigger an alert to a technician?
Determines whether Observer is in scope
Presence of a restock-alert listener
5
Can a user cancel mid-selection before paying?
Adds a transition edge to the state machine
CoffeeMachineState transition table
F. Assumptions
Confirmed: fixed recipes; ingredient sufficiency check before dispensing; refill capability.
Assumption: a single shared ingredient hopper feeds potentially multiple brewing slots, making the ingredient-reservation race a real (if small-scale) concern rather than a purely academic one.
Assumption: payment is a simple PaymentStrategy (cash/card) that returns success/failure; actual card-processing internals are out of scope, matching Splitwise/Ride-Sharing's payment-gateway boundary.
Assumption: a failed mid-dispense attempt returns reserved ingredients to inventory and refunds payment — no half-charged, half-brewed outcomes.
G. Actors
Actor
Responsibility
User
Selects a beverage, pays, receives it or a refund
Technician
Refills ingredients, services the machine
CoffeeMachine
Enforces state transitions, checks stock, coordinates dispensing
H. Core Domain Entities
Class
Responsibility
Key Attributes
Key Methods
Does NOT Handle
Ingredient
One stocked ingredient
name, quantity, capacity
reserve(qty), release(qty), refill(qty)
Recipes or pricing
Recipe
Data: what a beverage needs
Map<ingredient, requiredQty>, price
—
Any computation — pure data, same role as Splitwise's Split
The actual ingredient math (delegates to the machine/context)
PaymentStrategy (interface)
One payment method
—
charge(amount), refund(amount)
Selection logic, dispensing
CoffeeMachine (context/facade)
Owns current state, inventory, recipes; drives the whole flow
currentState, ingredients, beverages
select(), pay(), dispense()
State-specific legality checks (delegated to the current state object)
I. Class Relationships
State:CoffeeMachine delegates every action to its current CoffeeMachineState object (Idle → Selecting → Dispensing → Idle, with OutOfStock and Cancelled as alternate branches) rather than an internal switch statement — the textbook GoF State example, and the cleanest fit for State in this entire document since the states genuinely change which actions are even legal, not just cosmetic labels.
Strategy:PaymentStrategy implementations (cash/card) are mutually exclusive alternatives, same shape as every other Strategy in this set.
Composition: a Beverage owns its Recipe; a Recipe has no meaning outside the beverage it describes.
Why Template Method was considered and rejected: every beverage's brewing steps are identical — check ingredients, reserve them, combine, dispense, commit inventory — and only the quantities differ, which is data (the Recipe), not behavior. Template Method exists to let subclasses override specific steps of an algorithm; here there is nothing for a subclass to override, so introducing a beverage-per-subclass hierarchy just to hang a shared method on would add a class hierarchy with zero actual variation in behavior. A single parameterized brew(recipe) method captures this correctly. This is the same class of judgment call as Coffee's sibling problems rejecting Bridge (Problem 7) or a second parallel Strategy (Problem 5's surge).
J. Class Diagram
View Mermaid source
classDiagram
class Ingredient {
name
quantity
capacity
+reserve(qty)
+release(qty)
+refill(qty)
}
class Recipe {
requirements
price
}
class Beverage {
name
recipe
}
class CoffeeMachineState {
<<interface>>
+selectBeverage()
+insertPayment()
+dispense()
+cancel()
}
class PaymentStrategy {
<<interface>>
+charge(amount)
+refund(amount)
}
class CoffeeMachine {
currentState
+select()
+pay()
+dispense()
}
CoffeeMachine --> CoffeeMachineState : delegates
CoffeeMachineState <|.. IdleState
CoffeeMachineState <|.. SelectingState
CoffeeMachineState <|.. DispensingState
CoffeeMachineState <|.. OutOfStockState
Beverage --> Recipe
Recipe --> Ingredient
CoffeeMachine --> PaymentStrategy
PaymentStrategy <|.. CashPayment
PaymentStrategy <|.. CardPayment
K. Responsibility Mapping
Class
Responsibility
Does NOT Handle
CoffeeMachineState
What's legal right now
Ingredient math, payment mechanics
Ingredient
Its own quantity, atomically
Recipes, pricing
Recipe
Pure data: what's needed
Any computation
PaymentStrategy
Charge/refund mechanics for one method
Deciding when to charge (that's the state machine's job)
CoffeeMachine
Orchestration and state delegation
Every specific check (delegated to state/ingredient/payment objects)
L. SOLID Analysis
Principle
How It's Applied
Naive Violation
SRP
Ingredient only tracks quantity; PaymentStrategy only handles money; state objects only enforce legality
One CoffeeMachine class with a status string and giant if/else blocks checking it everywhere
OCP
New beverage = new Recipe data, zero code changes; new payment method = new PaymentStrategy
if (beverage == "LATTE") ... else if (beverage == "MOCHA") ... inline brewing logic per drink
LSP
Every CoffeeMachineState implements all four actions, even if most just throw/reject ("can't dispense while Idle") — a defined, expected response, not an inconsistent crash
A state that silently no-ops on an illegal action instead of rejecting it clearly, masking bugs in the calling code
ISP
Small, focused PaymentStrategy and CoffeeMachineState interfaces
A fat interface also requiring printReceipt(), sendEmail(), logAnalytics()
DIP
CoffeeMachine depends on CoffeeMachineState and PaymentStrategy as interfaces
CoffeeMachine directly checking if (cash) ... else ... inline instead of delegating to a payment object
M. Design Patterns
Pattern
Problem It Solves
Where It Appears
When NOT to Use
State
Legal actions genuinely differ by machine state, not just cosmetically
CoffeeMachineState hierarchy, delegated to by CoffeeMachine
Skip only if the machine truly has one state ever (unrealistic for this prompt)
Strategy
Payment method is a mutually exclusive alternative
PaymentStrategy
Skip if only one payment method will ever exist
Observer (optional)
Low-inventory alerts to a technician shouldn't couple the machine to a specific alerting mechanism
An onLowInventory hook fired when an ingredient crosses a threshold — could plug directly into Problem 7's NotificationService
Skip if refill alerts are out of scope for the round
Explicitly avoided: Template Method for per-beverage brewing steps — see I for why the variation here is data (a Recipe), not behavior, making a beverage-subclass hierarchy pure overhead.
N. What Is Likely to Change?
Behavior
Current Design
Anticipated Change
Protecting Abstraction
Beverage menu
Fixed set of recipes
Seasonal/customizable recipes (extra shot, less sugar)
Recipe as data the machine reads, not hardcoded per-beverage logic
Payment
Cash, card
Mobile wallet, loyalty points
PaymentStrategy interface
Machine topology
Single brewing unit
Multiple slots sharing one hopper
Ingredient.reserve() designed atomic from day one (see W)
Alerting
None
Low-inventory notifications to a technician or ops dashboard
Observer hook, not a hardcoded call
O. Design Evolution
V1: one beverage, no payment, dispense always succeeds if ingredients exist (simple boolean check).
V2: multiple beverages via Recipe data; still a boolean "status" field.
V3: replace the boolean/status field with a proper CoffeeMachineState hierarchy once illegal-action bugs start appearing (e.g. dispensing twice for one payment).
What stays stable:CoffeeMachine.select()/pay()/dispense() signatures. New beverages are pure data additions; new payment methods and alerting are additive via existing interfaces.
P. API / Interface Design
interface CoffeeMachineState {
void selectBeverage(CoffeeMachine ctx, Beverage b);
void insertPayment(CoffeeMachine ctx, PaymentStrategy method, Money amount);
void dispense(CoffeeMachine ctx);
void cancel(CoffeeMachine ctx);
}
class CoffeeMachine {
void select(Beverage b); // Idle -> Selecting, or rejects if not Idle
void pay(PaymentStrategy method, Money amount); // Selecting -> Dispensing
void dispenseNow(); // Dispensing -> Idle (or -> OutOfStock)
void cancel(); // Selecting -> Idle, refunds if already paid
void refill(Ingredient ing, int qty); // technician action, any state
}
interface CoffeeMachineState {
void selectBeverage(CoffeeMachine ctx, Beverage b);
void insertPayment(CoffeeMachine ctx, PaymentStrategy method, Money amount);
void dispense(CoffeeMachine ctx);
void cancel(CoffeeMachine ctx);
}
class CoffeeMachine {
void select(Beverage b); // Idle -> Selecting, or rejects if not Idle
void pay(PaymentStrategy method, Money amount); // Selecting -> Dispensing
void dispenseNow(); // Dispensing -> Idle (or -> OutOfStock)
void cancel(); // Selecting -> Idle, refunds if already paid
void refill(Ingredient ing, int qty); // technician action, any state
}
class CoffeeMachineState {
public:
virtual void selectBeverage(CoffeeMachine& ctx, const Beverage& b) = 0;
virtual void insertPayment(CoffeeMachine& ctx, std::shared_ptr<PaymentStrategy> method, Money amount) = 0;
virtual void dispense(CoffeeMachine& ctx) = 0;
virtual void cancel(CoffeeMachine& ctx) = 0;
};
class CoffeeMachine {
public:
void select(const Beverage& b); // Idle -> Selecting, or rejects if not Idle
void pay(std::shared_ptr<PaymentStrategy> method, Money amount); // Selecting -> Dispensing
void dispenseNow(); // Dispensing -> Idle (or -> OutOfStock)
void cancel(); // Selecting -> Idle, refunds if already paid
void refill(std::shared_ptr<Ingredient> ing, int qty); // technician action, any state
};
class CoffeeMachineState {
selectBeverage(ctx, b) { throw new Error('not implemented'); }
insertPayment(ctx, method, amount) { throw new Error('not implemented'); }
dispense(ctx) { throw new Error('not implemented'); }
cancel(ctx) { throw new Error('not implemented'); }
}
class CoffeeMachine {
select(b) {} // Idle -> Selecting, or rejects if not Idle
pay(method, amount) {} // Selecting -> Dispensing
dispenseNow() {} // Dispensing -> Idle (or -> OutOfStock)
cancel() {} // Selecting -> Idle, refunds if already paid
refill(ing, qty) {} // technician action, any state
}
Ownership/validation notes:dispenseNow() reserves all required ingredients atomically before physically dispensing anything — if reservation fails partway (one ingredient insufficient), nothing is reserved and the machine transitions to OutOfStockState with a refund, never a partial drink.
Q. Pseudocode — Critical Logic
CoffeeMachine.dispenseNow():
recipe = this.selectedBeverage.recipe
reserved = []
for (ingredient, qty) in recipe.requirements:
if not ingredient.tryReserve(qty): // atomic check-and-decrement
for r in reserved: r.ingredient.release(r.qty) // roll back partial reservation
this.paymentMethod.refund(recipe.price)
this.state = OutOfStockState
return
reserved.add((ingredient, qty))
// all ingredients reserved successfully — safe to physically dispense
dispenser.pour(recipe)
this.state = IdleState
void dispenseNow() {
Recipe recipe = this.selectedBeverage.getRecipe();
List<Reservation> reserved = new ArrayList<>();
for (Map.Entry<Ingredient, Integer> entry : recipe.getRequirements().entrySet()) {
Ingredient ingredient = entry.getKey();
int qty = entry.getValue();
if (!ingredient.tryReserve(qty)) { // atomic check-and-decrement
for (Reservation r : reserved) r.getIngredient().release(r.getQty()); // roll back partial reservation
this.paymentMethod.refund(recipe.getPrice());
this.state = new OutOfStockState();
return;
}
reserved.add(new Reservation(ingredient, qty));
}
// all ingredients reserved successfully — safe to physically dispense
dispenser.pour(recipe);
this.state = new IdleState();
}
void CoffeeMachine::dispenseNow() {
Recipe recipe = selectedBeverage_->recipe();
std::vector<Reservation> reserved;
for (auto& [ingredient, qty] : recipe.requirements()) {
if (!ingredient->tryReserve(qty)) { // atomic check-and-decrement
for (auto& r : reserved) r.ingredient->release(r.qty); // roll back partial reservation
paymentMethod_->refund(recipe.price());
state_ = std::make_shared<OutOfStockState>();
return;
}
reserved.push_back({ingredient, qty});
}
// all ingredients reserved successfully — safe to physically dispense
dispenser_->pour(recipe);
state_ = std::make_shared<IdleState>();
}
dispenseNow() {
const recipe = this.selectedBeverage.recipe;
const reserved = [];
for (const [ingredient, qty] of recipe.requirements) {
if (!ingredient.tryReserve(qty)) { // atomic check-and-decrement
for (const r of reserved) r.ingredient.release(r.qty); // roll back partial reservation
this.paymentMethod.refund(recipe.price);
this.state = new OutOfStockState();
return;
}
reserved.push({ ingredient, qty });
}
// all ingredients reserved successfully — safe to physically dispense
this.dispenser.pour(recipe);
this.state = new IdleState();
}
dispenseNow(): void {
const recipe: Recipe = this.selectedBeverage.recipe;
const reserved: { ingredient: Ingredient; qty: number }[] = [];
for (const [ingredient, qty] of recipe.requirements) {
if (!ingredient.tryReserve(qty)) { // atomic check-and-decrement
for (const r of reserved) r.ingredient.release(r.qty); // roll back partial reservation
this.paymentMethod.refund(recipe.price);
this.state = new OutOfStockState();
return;
}
reserved.push({ ingredient, qty });
}
// all ingredients reserved successfully — safe to physically dispense
this.dispenser.pour(recipe);
this.state = new IdleState();
}
O(1) access when checking/reserving a recipe's requirements
A list with linear scan is fine at this tiny scale but needlessly slower
Atomic integer (or a mutex-guarded counter) per Ingredient
reserve()/release()
Guarantees no two concurrent dispense attempts both succeed against the same insufficient stock
A single global lock around the whole machine is simpler but serializes unrelated ingredients unnecessarily on a multi-slot machine
T. Complexity Analysis
Operation
Time
Space
dispenseNow()
O(ingredients in recipe)
O(ingredients in recipe) for the reservation rollback list
refill()
O(1)
—
U. Testing Strategy
Happy path: sufficient ingredients, successful payment → beverage dispensed, inventory decremented by exactly the recipe amounts.
Edge cases: exactly-enough inventory (boundary), payment succeeds but one ingredient is insufficient (must refund, not dispense a partial drink).
Illegal transitions: calling dispenseNow() while still Idle (nothing selected/paid) must be rejected by the current state object, not silently no-op.
Concurrency tests: two concurrent dispense attempts on a shared, barely-sufficient ingredient — exactly one succeeds, the other cleanly falls back to OutOfStockState with a refund, inventory never goes negative.
V. Interviewer Follow-Ups
Follow-Up
Testing
Design Change
Classes Changed
Pattern/Trade-off
"Support multiple brewing slots sharing one milk/coffee hopper."
Recognizing the check-then-act race at small scale
Atomic Ingredient.reserve(), same shape as Parking Lot's spot occupation
Ingredient
Same lesson, tiny domain — good for showing pattern transfer, not just memorized to one problem
"Add a loyalty-points payment option."
OCP under a real new requirement
New LoyaltyPointsPayment implementing PaymentStrategy
New class only
Validates the Strategy investment
"Alert a technician when milk drops below 10%."
Decoupling alerting from the core flow
Ingredient fires a threshold event an Observer listens for
New Observer hook, no changes to dispensing logic
Could route directly into Problem 7's NotificationService — a nice concrete cross-reference
"Why not make each beverage its own subclass with its own brew() override?"
Testing whether the candidate over-applies Template Method
N/A — the answer is to explain why not (see I)
N/A
A strong candidate defends the data-driven Recipe approach without being told it's wrong first
W. Concurrency
Shared mutable state: each Ingredient's quantity, if the machine has multiple brewing slots sharing one hopper.
The same check-then-act race, at coffee-machine scale: two slots both check "is there enough milk?", both see yes, both proceed — and the hopper goes negative. The fix is identical in shape to Parking Lot and Ride-Sharing: make check-and-decrement one atomic operation (tryReserve), not a separate read then write.
Rollback on partial failure: if a recipe needs three ingredients and the third fails to reserve, the first two reservations must be released back — an all-or-nothing reservation, mirroring Splitwise's all-or-nothing expense mutation.
Single-slot machines need none of this: worth stating plainly that if the clarifying question in E reveals a single brewing unit, this entire section is a non-issue — don't over-engineer concurrency that the requirements don't actually call for.
X. Persistence
Minimal for this problem: ingredient inventory levels need to survive a machine restart/power cycle (a simple local persisted counter, updated on every reserve/refill), and a sales/dispense log is useful for a technician's restocking schedule but isn't a hard requirement unless the round asks for reporting.
Y. Scaling Boundary
Layer
What Belongs Here
LLD
State machine, Ingredient/Recipe/PaymentStrategy classes, atomic reservation
HLD
Fleet-wide monitoring across many physical machines, remote restock scheduling, centralized sales analytics
Distributed Systems
Essentially not applicable — this is inherently a single-device problem; a strong candidate notes this rather than inventing distributed concerns that don't exist here
Z. Failure Scenarios
Physical jam mid-dispense (after ingredients reserved, before pour completes): must release reserved ingredients and refund — never leave inventory permanently short for a drink that was never actually made.
Payment succeeds but dispensing then fails: refund is mandatory, not optional — same principle as Ride-Sharing's "trip happened, payment is separate" but inverted (here, payment happened, "trip" did not).
Power loss mid-transaction: on restart, any in-flight reservation should be treated as failed/rolled-back rather than assumed committed.
AA. Common Bad Designs
A single status string with scattered if checks everywhere instead of a real State pattern — works for two states, rots badly past three or four.
Check-then-decrement ingredient logic — fine on a single-slot machine, breaks the instant a second slot shares the hopper.
A beverage-subclass-per-drink hierarchy for brewing — over-applies Template Method to what's actually just data variation (see I, M).
Charging payment before confirming ingredient sufficiency — creates an avoidable refund path that a reserve-before-charge (or check-before-charge) ordering would prevent entirely.
AB. Design Trade-Offs
Decision
Option A
Option B
Trade-off
Beverage variation modeling
Data-driven Recipe (chosen)
Beverage-subclass-per-drink with Template Method
Data-driven has zero behavioral variation to justify a hierarchy; subclassing would be pure ceremony here specifically because nothing actually overrides a step
Payment-before-check vs. check-before-payment
Charge first, refund on failure
Check ingredients first, then charge
Check-first avoids ever needing a refund path at all; charge-first is sometimes required if the payment terminal is a separate physical step that can't be "un-started"
Ingredient concurrency
Atomic per-ingredient reserve
Global machine lock
Per-ingredient is only worth it on multi-slot machines; a single-slot machine gets no benefit from either and shouldn't bother
AC. Interviewer Evaluation
Weak candidate: a status boolean, no separation between selection/payment/dispensing, dispenses without checking payment succeeded.
Average candidate: has recognizable State and Strategy usage but doesn't handle partial-failure refund/rollback.
Strong candidate: clean State machine, atomic ingredient reservation with rollback, correct payment/dispense ordering.
Exceptional candidate: all of the above, plus explicitly and correctly argues against Template Method for beverage variation, and scopes concurrency honestly (applying it only if multi-slot is actually in play).
AD. Interview Simulation
Interviewer: Design a coffee vending machine.
Candidate: The machine's behavior clearly depends on where it is in a flow — idle, selecting, dispensing — so I'd model that as a State pattern rather than a status flag, since the legal actions genuinely differ by state.
Interviewer: Would you make each beverage its own class, since they each brew differently?
Candidate: Actually no — every beverage follows the exact same steps: check ingredients, reserve them, combine, dispense. Only the quantities differ, and that's data, not behavior. I'd keep Recipe as a data object and use one shared brew() method rather than a beverage-subclass hierarchy with nothing to actually override.
Interviewer: What if the machine has two brewing heads sharing one milk container?
Candidate: Then reserving milk needs to be atomic — check-and-decrement in one step — or two concurrent brews could both see "enough milk" and overdraw it. Same race condition as the parking-spot problem, just a much smaller blast radius.
Interviewer: What happens if reservation succeeds for milk and coffee but fails for sugar?
Candidate: I'd roll back the milk and coffee reservations and refund the payment — an all-or-nothing reservation, so the machine never ends up in a state where it's charged the user for a drink it can't actually make.
Coffee/Ingredient/Payment/CoffeeMachine classes; CoffeeMachine as Singleton; synchronized methods on Ingredient for quantity updates; simulates concurrent requests via ExecutorService
Concrete, correct instinct on synchronizing ingredient updates — validates this document's atomic-reservation approach
No explicit State pattern — behavior isn't shown as delegated to state objects; Singleton for the machine itself is reasonable for a single physical device but isn't argued for the way this document argues it for Problem 6's logger registry
The synchronized-ingredient-update instinct, directly reflected in this document's Ingredient.reserve()
State drives legality; Recipe is pure data (Template Method explicitly rejected); Payment is a Strategy
Key patterns
State (headline), Strategy (payment), optional Observer (low-inventory alerts)
Most important decision
Atomic, all-or-nothing ingredient reservation with rollback on partial failure
Likely-changing behavior
Menu/recipes, payment methods, machine topology (single vs. multi-slot), alerting
Concurrency concern
Shared-hopper check-then-act race across multiple brewing slots — only relevant if multi-slot
Top follow-ups
Multi-slot sharing, new payment method, low-inventory alerting, defending the data-driven Recipe over subclassing
Common mistakes
Status-flag instead of State; check-then-decrement race; beverage-subclass-per-drink over-engineering; charging before confirming ingredients
What NOT to over-engineer
Don't add distributed-systems concerns to a single physical device, and don't add Template Method where there's no actual behavioral variation
AG. Full Implementation
A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.
// Coffee Vending Machine -- full LLD implementation with demo.
// Single-file Java translation of the Mermaid class diagram in section J.
import java.util.*;
public class p8 {
// ---------- Ingredient ----------
/** One stocked ingredient; owns its own quantity so reserve/release/refill stay in one place. */
static final class Ingredient {
final String name;
int quantity;
final int capacity;
Ingredient(String name, int quantity, int capacity) {
this.name = name; this.quantity = quantity; this.capacity = capacity;
}
/** Atomic check-and-decrement: never lets quantity go negative. */
boolean reserve(int qty) {
if (quantity < qty) return false;
quantity -= qty;
return true;
}
void release(int qty) { quantity = Math.min(capacity, quantity + qty); }
void refill(int qty) { quantity = Math.min(capacity, quantity + qty); }
public String toString() { return name + "(" + quantity + "/" + capacity + ")"; }
}
// ---------- Recipe ----------
/** Pure data: what a beverage needs and how much it costs. No computation. */
static final class Recipe {
final Map<Ingredient, Integer> requirements;
final int priceCents;
Recipe(Map<Ingredient, Integer> requirements, int priceCents) {
this.requirements = requirements; this.priceCents = priceCents;
}
}
// ---------- Beverage ----------
/** A sellable item. Composes its Recipe -- the recipe has no meaning outside the beverage. */
static final class Beverage {
final String name;
final Recipe recipe;
Beverage(String name, Recipe recipe) { this.name = name; this.recipe = recipe; }
public String toString() { return name; }
}
// ---------- PaymentStrategy (Strategy interface) ----------
interface PaymentStrategy {
void charge(int amountCents);
void refund(int amountCents);
}
static final class CashPayment implements PaymentStrategy {
int inserted = 0;
public void charge(int amountCents) {
if (inserted < amountCents) throw new IllegalStateException("Insufficient cash inserted");
inserted -= amountCents;
System.out.println(" [CASH] charged " + amountCents + "c, change=" + inserted + "c");
}
public void refund(int amountCents) {
inserted += amountCents;
System.out.println(" [CASH] refunded " + amountCents + "c");
}
void insert(int cents) { inserted += cents; }
}
static final class CardPayment implements PaymentStrategy {
public void charge(int amountCents) { System.out.println(" [CARD] charged " + amountCents + "c"); }
public void refund(int amountCents) { System.out.println(" [CARD] refunded " + amountCents + "c"); }
}
// ---------- CoffeeMachineState (State interface) ----------
interface CoffeeMachineState {
void selectBeverage(CoffeeMachine ctx, Beverage b);
void insertPayment(CoffeeMachine ctx, PaymentStrategy method, int amountCents);
void dispense(CoffeeMachine ctx);
void cancel(CoffeeMachine ctx);
}
static final class IdleState implements CoffeeMachineState {
public void selectBeverage(CoffeeMachine ctx, Beverage b) {
ctx.selectedBeverage = b;
ctx.setState(ctx.selectingState);
System.out.println(" Selected " + b + " -- please pay " + b.recipe.priceCents + "c");
}
public void insertPayment(CoffeeMachine ctx, PaymentStrategy method, int amountCents) {
System.out.println(" Reject: select a beverage first");
}
public void dispense(CoffeeMachine ctx) { System.out.println(" Reject: nothing selected"); }
public void cancel(CoffeeMachine ctx) { System.out.println(" Nothing to cancel"); }
}
static final class SelectingState implements CoffeeMachineState {
public void selectBeverage(CoffeeMachine ctx, Beverage b) { System.out.println(" Reject: already selecting"); }
public void insertPayment(CoffeeMachine ctx, PaymentStrategy method, int amountCents) {
int price = ctx.selectedBeverage.recipe.priceCents;
if (amountCents < price) { System.out.println(" Reject: insufficient payment"); return; }
ctx.paymentMethod = method;
method.charge(price);
ctx.setState(ctx.dispensingState);
System.out.println(" Payment accepted -> dispensing");
}
public void dispense(CoffeeMachine ctx) { System.out.println(" Reject: pay first"); }
public void cancel(CoffeeMachine ctx) {
ctx.selectedBeverage = null;
ctx.setState(ctx.idleState);
System.out.println(" Selection cancelled");
}
}
static final class DispensingState implements CoffeeMachineState {
public void selectBeverage(CoffeeMachine ctx, Beverage b) { System.out.println(" Reject: already dispensing"); }
public void insertPayment(CoffeeMachine ctx, PaymentStrategy method, int amountCents) { System.out.println(" Reject: already paid"); }
public void dispense(CoffeeMachine ctx) { ctx.dispenseNow(); }
public void cancel(CoffeeMachine ctx) {
ctx.paymentMethod.refund(ctx.selectedBeverage.recipe.priceCents);
ctx.selectedBeverage = null;
ctx.setState(ctx.idleState);
System.out.println(" Cancelled after payment; refunded");
}
}
static final class OutOfStockState implements CoffeeMachineState {
public void selectBeverage(CoffeeMachine ctx, Beverage b) { System.out.println(" Reject: machine out of stock"); }
public void insertPayment(CoffeeMachine ctx, PaymentStrategy method, int amountCents) { System.out.println(" Reject: machine out of stock"); }
public void dispense(CoffeeMachine ctx) { System.out.println(" Reject: machine out of stock"); }
public void cancel(CoffeeMachine ctx) {
ctx.selectedBeverage = null;
ctx.setState(ctx.idleState);
System.out.println(" Reset from out-of-stock -> idle");
}
}
// ---------- CoffeeMachine (context/facade) ----------
static final class CoffeeMachine {
CoffeeMachineState currentState;
final IdleState idleState = new IdleState();
final SelectingState selectingState = new SelectingState();
final DispensingState dispensingState = new DispensingState();
final OutOfStockState outOfStockState = new OutOfStockState();
final Map<String, Ingredient> ingredients = new HashMap<>();
final Map<String, Beverage> beverages = new HashMap<>();
Beverage selectedBeverage;
PaymentStrategy paymentMethod;
CoffeeMachine() { currentState = idleState; }
void setState(CoffeeMachineState state) { currentState = state; }
void addIngredient(Ingredient ing) { ingredients.put(ing.name, ing); }
void addBeverage(Beverage b) { beverages.put(b.name, b); }
void select(Beverage b) { currentState.selectBeverage(this, b); }
void pay(PaymentStrategy method, int amountCents) { currentState.insertPayment(this, method, amountCents); }
void dispense() { currentState.dispense(this); }
void cancel() { currentState.cancel(this); }
void refill(Ingredient ing, int qty) { ing.refill(qty); } // technician action, legal in any state
/**
* Reserve every required ingredient atomically-in-sequence; on the first shortfall, roll back
* whatever was already reserved, refund, and flip to OutOfStockState. Never dispenses a half-made drink.
*/
void dispenseNow() {
Recipe recipe = selectedBeverage.recipe;
List<Map.Entry<Ingredient, Integer>> reserved = new ArrayList<>();
for (Map.Entry<Ingredient, Integer> e : recipe.requirements.entrySet()) {
Ingredient ingredient = e.getKey();
int qty = e.getValue();
if (!ingredient.reserve(qty)) {
for (Map.Entry<Ingredient, Integer> r : reserved) r.getKey().release(r.getValue()); // roll back
paymentMethod.refund(recipe.priceCents);
setState(outOfStockState);
System.out.println(" Out of stock: missing " + ingredient.name + " -- refunded and rolled back");
return;
}
reserved.add(e);
}
// all ingredients reserved successfully -- safe to physically dispense
System.out.println(" Pouring " + selectedBeverage + "...");
selectedBeverage = null;
setState(idleState);
}
}
// ---------- Demo ----------
public static void main(String[] args) {
CoffeeMachine machine = new CoffeeMachine();
Ingredient water = new Ingredient("water", 500, 1000);
Ingredient milk = new Ingredient("milk", 500, 1000);
Ingredient coffee = new Ingredient("coffee", 100, 500);
Ingredient sugar = new Ingredient("sugar", 200, 500);
machine.addIngredient(water);
machine.addIngredient(milk);
machine.addIngredient(coffee);
machine.addIngredient(sugar);
Beverage latte = new Beverage("Latte",
new Recipe(Map.of(water, 100, milk, 150, coffee, 30), 250));
Beverage espresso = new Beverage("Espresso",
new Recipe(Map.of(water, 50, coffee, 40), 150));
machine.addBeverage(latte);
machine.addBeverage(espresso);
System.out.println("-- Happy path: buy a Latte with cash --");
machine.select(latte);
CashPayment cash = new CashPayment();
cash.insert(300);
machine.pay(cash, 300);
machine.dispense();
System.out.println("\n-- Buy several Espressos with card until coffee runs out --");
for (int i = 0; i < 12; i++) {
machine.select(espresso);
if (machine.currentState == machine.selectingState) {
CardPayment card = new CardPayment();
machine.pay(card, 150);
}
machine.dispense();
if (machine.currentState == machine.outOfStockState) {
System.out.println(" Machine reports out of stock after " + (i + 1) + " attempts");
break;
}
}
System.out.println("\n-- Technician refills coffee, machine recovers --");
machine.refill(coffee, 200);
machine.cancel(); // reset from OutOfStock -> Idle
machine.select(espresso);
CardPayment card2 = new CardPayment();
machine.pay(card2, 150);
machine.dispense();
System.out.println("\n-- Cancel after paying (refund) --");
machine.select(latte);
CashPayment cash2 = new CashPayment();
cash2.insert(250);
machine.pay(cash2, 250);
machine.cancel();
}
}
// Coffee Vending Machine -- full class-diagram implementation, single-threaded.
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <memory>
#include <stdexcept>
#include <algorithm>
// ---------- Ingredient ----------
// One stocked ingredient; owns its own quantity so reserve/release/refill stay in one place.
class Ingredient {
public:
std::string name;
int quantity;
int capacity;
Ingredient(std::string name_, int quantity_, int capacity_)
: name(std::move(name_)), quantity(quantity_), capacity(capacity_) {}
// Atomic check-and-decrement: never lets quantity go negative.
bool reserve(int qty) {
if (quantity < qty) return false;
quantity -= qty;
return true;
}
void release(int qty) { quantity = std::min(capacity, quantity + qty); }
void refill(int qty) { quantity = std::min(capacity, quantity + qty); }
};
// ---------- Recipe ----------
// Pure data: what a beverage needs and how much it costs. No computation.
struct Recipe {
std::vector<std::pair<std::shared_ptr<Ingredient>, int>> requirements;
int priceCents;
};
// ---------- Beverage ----------
// A sellable item. Composes its Recipe -- the recipe has no meaning outside the beverage.
struct Beverage {
std::string name;
Recipe recipe;
};
// ---------- PaymentStrategy (Strategy interface) ----------
struct PaymentStrategy {
virtual ~PaymentStrategy() = default;
virtual void charge(int amountCents) = 0;
virtual void refund(int amountCents) = 0;
};
struct CashPayment : PaymentStrategy {
int inserted = 0;
void insert(int cents) { inserted += cents; }
void charge(int amountCents) override {
if (inserted < amountCents) throw std::runtime_error("Insufficient cash inserted");
inserted -= amountCents;
std::cout << " [CASH] charged " << amountCents << "c, change=" << inserted << "c\n";
}
void refund(int amountCents) override {
inserted += amountCents;
std::cout << " [CASH] refunded " << amountCents << "c\n";
}
};
struct CardPayment : PaymentStrategy {
void charge(int amountCents) override { std::cout << " [CARD] charged " << amountCents << "c\n"; }
void refund(int amountCents) override { std::cout << " [CARD] refunded " << amountCents << "c\n"; }
};
class CoffeeMachine; // fwd decl
// ---------- CoffeeMachineState (State interface) ----------
struct CoffeeMachineState {
virtual ~CoffeeMachineState() = default;
virtual void selectBeverage(CoffeeMachine& ctx, std::shared_ptr<Beverage> b) = 0;
virtual void insertPayment(CoffeeMachine& ctx, std::shared_ptr<PaymentStrategy> method, int amountCents) = 0;
virtual void dispense(CoffeeMachine& ctx) = 0;
virtual void cancel(CoffeeMachine& ctx) = 0;
};
// ---------- CoffeeMachine (context/facade) ----------
class CoffeeMachine {
public:
std::shared_ptr<CoffeeMachineState> currentState;
std::shared_ptr<CoffeeMachineState> idleState, selectingState, dispensingState, outOfStockState;
std::map<std::string, std::shared_ptr<Ingredient>> ingredients;
std::map<std::string, std::shared_ptr<Beverage>> beverages;
std::shared_ptr<Beverage> selectedBeverage;
std::shared_ptr<PaymentStrategy> paymentMethod;
void setState(std::shared_ptr<CoffeeMachineState> state) { currentState = std::move(state); }
void addIngredient(std::shared_ptr<Ingredient> ing) { ingredients[ing->name] = ing; }
void addBeverage(std::shared_ptr<Beverage> b) { beverages[b->name] = b; }
void select(std::shared_ptr<Beverage> b) { currentState->selectBeverage(*this, std::move(b)); }
void pay(std::shared_ptr<PaymentStrategy> method, int amountCents) { currentState->insertPayment(*this, std::move(method), amountCents); }
void dispense() { currentState->dispense(*this); }
void cancel() { currentState->cancel(*this); }
void refill(std::shared_ptr<Ingredient> ing, int qty) { ing->refill(qty); } // technician action, legal in any state
// Reserve every required ingredient in sequence; on the first shortfall, roll back whatever
// was already reserved, refund, and flip to OutOfStockState. Never dispenses a half-made drink.
void dispenseNow() {
Recipe& recipe = selectedBeverage->recipe;
std::vector<std::pair<std::shared_ptr<Ingredient>, int>> reserved;
for (auto& req : recipe.requirements) {
if (!req.first->reserve(req.second)) {
for (auto& r : reserved) r.first->release(r.second); // roll back
paymentMethod->refund(recipe.priceCents);
setState(outOfStockState);
std::cout << " Out of stock: missing " << req.first->name << " -- refunded and rolled back\n";
return;
}
reserved.push_back(req);
}
std::cout << " Pouring " << selectedBeverage->name << "...\n";
selectedBeverage = nullptr;
setState(idleState);
}
};
struct IdleState : CoffeeMachineState {
void selectBeverage(CoffeeMachine& ctx, std::shared_ptr<Beverage> b) override {
ctx.selectedBeverage = b;
ctx.setState(ctx.selectingState);
std::cout << " Selected " << b->name << " -- please pay " << b->recipe.priceCents << "c\n";
}
void insertPayment(CoffeeMachine&, std::shared_ptr<PaymentStrategy>, int) override {
std::cout << " Reject: select a beverage first\n";
}
void dispense(CoffeeMachine&) override { std::cout << " Reject: nothing selected\n"; }
void cancel(CoffeeMachine&) override { std::cout << " Nothing to cancel\n"; }
};
struct SelectingState : CoffeeMachineState {
void selectBeverage(CoffeeMachine&, std::shared_ptr<Beverage>) override { std::cout << " Reject: already selecting\n"; }
void insertPayment(CoffeeMachine& ctx, std::shared_ptr<PaymentStrategy> method, int amountCents) override {
int price = ctx.selectedBeverage->recipe.priceCents;
if (amountCents < price) { std::cout << " Reject: insufficient payment\n"; return; }
ctx.paymentMethod = method;
method->charge(price);
ctx.setState(ctx.dispensingState);
std::cout << " Payment accepted -> dispensing\n";
}
void dispense(CoffeeMachine&) override { std::cout << " Reject: pay first\n"; }
void cancel(CoffeeMachine& ctx) override {
ctx.selectedBeverage = nullptr;
ctx.setState(ctx.idleState);
std::cout << " Selection cancelled\n";
}
};
struct DispensingState : CoffeeMachineState {
void selectBeverage(CoffeeMachine&, std::shared_ptr<Beverage>) override { std::cout << " Reject: already dispensing\n"; }
void insertPayment(CoffeeMachine&, std::shared_ptr<PaymentStrategy>, int) override { std::cout << " Reject: already paid\n"; }
void dispense(CoffeeMachine& ctx) override { ctx.dispenseNow(); }
void cancel(CoffeeMachine& ctx) override {
ctx.paymentMethod->refund(ctx.selectedBeverage->recipe.priceCents);
ctx.selectedBeverage = nullptr;
ctx.setState(ctx.idleState);
std::cout << " Cancelled after payment; refunded\n";
}
};
struct OutOfStockState : CoffeeMachineState {
void selectBeverage(CoffeeMachine&, std::shared_ptr<Beverage>) override { std::cout << " Reject: machine out of stock\n"; }
void insertPayment(CoffeeMachine&, std::shared_ptr<PaymentStrategy>, int) override { std::cout << " Reject: machine out of stock\n"; }
void dispense(CoffeeMachine&) override { std::cout << " Reject: machine out of stock\n"; }
void cancel(CoffeeMachine& ctx) override {
ctx.selectedBeverage = nullptr;
ctx.setState(ctx.idleState);
std::cout << " Reset from out-of-stock -> idle\n";
}
};
int main() {
CoffeeMachine machine;
machine.idleState = std::make_shared<IdleState>();
machine.selectingState = std::make_shared<SelectingState>();
machine.dispensingState = std::make_shared<DispensingState>();
machine.outOfStockState = std::make_shared<OutOfStockState>();
machine.currentState = machine.idleState;
auto water = std::make_shared<Ingredient>("water", 500, 1000);
auto milk = std::make_shared<Ingredient>("milk", 500, 1000);
auto coffee = std::make_shared<Ingredient>("coffee", 100, 500);
auto sugar = std::make_shared<Ingredient>("sugar", 200, 500);
machine.addIngredient(water);
machine.addIngredient(milk);
machine.addIngredient(coffee);
machine.addIngredient(sugar);
auto latte = std::make_shared<Beverage>();
latte->name = "Latte";
latte->recipe = Recipe{ {{water, 100}, {milk, 150}, {coffee, 30}}, 250 };
auto espresso = std::make_shared<Beverage>();
espresso->name = "Espresso";
espresso->recipe = Recipe{ {{water, 50}, {coffee, 40}}, 150 };
machine.addBeverage(latte);
machine.addBeverage(espresso);
std::cout << "-- Happy path: buy a Latte with cash --\n";
machine.select(latte);
auto cash = std::make_shared<CashPayment>();
cash->insert(300);
machine.pay(cash, 300);
machine.dispense();
std::cout << "\n-- Buy several Espressos with card until coffee runs out --\n";
for (int i = 0; i < 12; i++) {
machine.select(espresso);
if (machine.currentState == machine.selectingState) {
auto card = std::make_shared<CardPayment>();
machine.pay(card, 150);
}
machine.dispense();
if (machine.currentState == machine.outOfStockState) {
std::cout << " Machine reports out of stock after " << (i + 1) << " attempts\n";
break;
}
}
std::cout << "\n-- Technician refills coffee, machine recovers --\n";
machine.refill(coffee, 200);
machine.cancel(); // reset from OutOfStock -> Idle
machine.select(espresso);
auto card2 = std::make_shared<CardPayment>();
machine.pay(card2, 150);
machine.dispense();
std::cout << "\n-- Cancel after paying (refund) --\n";
machine.select(latte);
auto cash2 = std::make_shared<CashPayment>();
cash2->insert(250);
machine.pay(cash2, 250);
machine.cancel();
return 0;
}
// Coffee Vending Machine -- full class-diagram implementation, single-threaded.
// Node-style, plain classes, CommonJS.
// ---------- Ingredient ----------
// One stocked ingredient; owns its own quantity so reserve/release/refill stay in one place.
class Ingredient {
constructor(name, quantity, capacity) {
this.name = name;
this.quantity = quantity;
this.capacity = capacity;
}
// Atomic check-and-decrement: never lets quantity go negative.
reserve(qty) {
if (this.quantity < qty) return false;
this.quantity -= qty;
return true;
}
release(qty) { this.quantity = Math.min(this.capacity, this.quantity + qty); }
refill(qty) { this.quantity = Math.min(this.capacity, this.quantity + qty); }
toString() { return `${this.name}(${this.quantity}/${this.capacity})`; }
}
// ---------- Recipe ----------
// Pure data: what a beverage needs and how much it costs. No computation.
class Recipe {
constructor(requirements, priceCents) {
this.requirements = requirements; // Map<Ingredient, qty>
this.priceCents = priceCents;
}
}
// ---------- Beverage ----------
// A sellable item. Composes its Recipe -- the recipe has no meaning outside the beverage.
class Beverage {
constructor(name, recipe) { this.name = name; this.recipe = recipe; }
toString() { return this.name; }
}
// ---------- PaymentStrategy (Strategy interface) ----------
class PaymentStrategy {
charge(_amountCents) { throw new Error('charge() not implemented'); }
refund(_amountCents) { throw new Error('refund() not implemented'); }
}
class CashPayment extends PaymentStrategy {
constructor() { super(); this.inserted = 0; }
insert(cents) { this.inserted += cents; }
charge(amountCents) {
if (this.inserted < amountCents) throw new Error('Insufficient cash inserted');
this.inserted -= amountCents;
console.log(` [CASH] charged ${amountCents}c, change=${this.inserted}c`);
}
refund(amountCents) {
this.inserted += amountCents;
console.log(` [CASH] refunded ${amountCents}c`);
}
}
class CardPayment extends PaymentStrategy {
charge(amountCents) { console.log(` [CARD] charged ${amountCents}c`); }
refund(amountCents) { console.log(` [CARD] refunded ${amountCents}c`); }
}
// ---------- CoffeeMachineState (State interface) ----------
class CoffeeMachineState {
selectBeverage(_ctx, _b) { throw new Error('not implemented'); }
insertPayment(_ctx, _method, _amountCents) { throw new Error('not implemented'); }
dispense(_ctx) { throw new Error('not implemented'); }
cancel(_ctx) { throw new Error('not implemented'); }
}
class IdleState extends CoffeeMachineState {
selectBeverage(ctx, b) {
ctx.selectedBeverage = b;
ctx.setState(ctx.selectingState);
console.log(` Selected ${b} -- please pay ${b.recipe.priceCents}c`);
}
insertPayment() { console.log(' Reject: select a beverage first'); }
dispense() { console.log(' Reject: nothing selected'); }
cancel() { console.log(' Nothing to cancel'); }
}
class SelectingState extends CoffeeMachineState {
selectBeverage() { console.log(' Reject: already selecting'); }
insertPayment(ctx, method, amountCents) {
const price = ctx.selectedBeverage.recipe.priceCents;
if (amountCents < price) { console.log(' Reject: insufficient payment'); return; }
ctx.paymentMethod = method;
method.charge(price);
ctx.setState(ctx.dispensingState);
console.log(' Payment accepted -> dispensing');
}
dispense() { console.log(' Reject: pay first'); }
cancel(ctx) {
ctx.selectedBeverage = null;
ctx.setState(ctx.idleState);
console.log(' Selection cancelled');
}
}
class DispensingState extends CoffeeMachineState {
selectBeverage() { console.log(' Reject: already dispensing'); }
insertPayment() { console.log(' Reject: already paid'); }
dispense(ctx) { ctx.dispenseNow(); }
cancel(ctx) {
ctx.paymentMethod.refund(ctx.selectedBeverage.recipe.priceCents);
ctx.selectedBeverage = null;
ctx.setState(ctx.idleState);
console.log(' Cancelled after payment; refunded');
}
}
class OutOfStockState extends CoffeeMachineState {
selectBeverage() { console.log(' Reject: machine out of stock'); }
insertPayment() { console.log(' Reject: machine out of stock'); }
dispense() { console.log(' Reject: machine out of stock'); }
cancel(ctx) {
ctx.selectedBeverage = null;
ctx.setState(ctx.idleState);
console.log(' Reset from out-of-stock -> idle');
}
}
// ---------- CoffeeMachine (context/facade) ----------
class CoffeeMachine {
constructor() {
this.idleState = new IdleState();
this.selectingState = new SelectingState();
this.dispensingState = new DispensingState();
this.outOfStockState = new OutOfStockState();
this.currentState = this.idleState;
this.ingredients = new Map();
this.beverages = new Map();
this.selectedBeverage = null;
this.paymentMethod = null;
}
setState(state) { this.currentState = state; }
addIngredient(ing) { this.ingredients.set(ing.name, ing); }
addBeverage(b) { this.beverages.set(b.name, b); }
select(b) { this.currentState.selectBeverage(this, b); }
pay(method, amountCents) { this.currentState.insertPayment(this, method, amountCents); }
dispense() { this.currentState.dispense(this); }
cancel() { this.currentState.cancel(this); }
refill(ing, qty) { ing.refill(qty); } // technician action, legal in any state
// Reserve every required ingredient in sequence; on the first shortfall, roll back whatever
// was already reserved, refund, and flip to OutOfStockState. Never dispenses a half-made drink.
dispenseNow() {
const recipe = this.selectedBeverage.recipe;
const reserved = [];
for (const [ingredient, qty] of recipe.requirements) {
if (!ingredient.reserve(qty)) {
for (const [ing, q] of reserved) ing.release(q); // roll back
this.paymentMethod.refund(recipe.priceCents);
this.setState(this.outOfStockState);
console.log(` Out of stock: missing ${ingredient.name} -- refunded and rolled back`);
return;
}
reserved.push([ingredient, qty]);
}
console.log(` Pouring ${this.selectedBeverage}...`);
this.selectedBeverage = null;
this.setState(this.idleState);
}
}
// ---------- Demo ----------
function main() {
const machine = new CoffeeMachine();
const water = new Ingredient('water', 500, 1000);
const milk = new Ingredient('milk', 500, 1000);
const coffee = new Ingredient('coffee', 100, 500);
const sugar = new Ingredient('sugar', 200, 500);
machine.addIngredient(water);
machine.addIngredient(milk);
machine.addIngredient(coffee);
machine.addIngredient(sugar);
const latte = new Beverage('Latte', new Recipe(new Map([[water, 100], [milk, 150], [coffee, 30]]), 250));
const espresso = new Beverage('Espresso', new Recipe(new Map([[water, 50], [coffee, 40]]), 150));
machine.addBeverage(latte);
machine.addBeverage(espresso);
console.log('-- Happy path: buy a Latte with cash --');
machine.select(latte);
const cash = new CashPayment();
cash.insert(300);
machine.pay(cash, 300);
machine.dispense();
console.log('\n-- Buy several Espressos with card until coffee runs out --');
for (let i = 0; i < 12; i++) {
machine.select(espresso);
if (machine.currentState === machine.selectingState) {
const card = new CardPayment();
machine.pay(card, 150);
}
machine.dispense();
if (machine.currentState === machine.outOfStockState) {
console.log(` Machine reports out of stock after ${i + 1} attempts`);
break;
}
}
console.log('\n-- Technician refills coffee, machine recovers --');
machine.refill(coffee, 200);
machine.cancel(); // reset from OutOfStock -> Idle
machine.select(espresso);
const card2 = new CardPayment();
machine.pay(card2, 150);
machine.dispense();
console.log('\n-- Cancel after paying (refund) --');
machine.select(latte);
const cash2 = new CashPayment();
cash2.insert(250);
machine.pay(cash2, 250);
machine.cancel();
}
if (require.main === module) main();
module.exports = {
Ingredient, Recipe, Beverage, PaymentStrategy, CashPayment, CardPayment,
CoffeeMachineState, IdleState, SelectingState, DispensingState, OutOfStockState, CoffeeMachine,
};
// Coffee Vending Machine -- full class-diagram implementation, single-threaded.
// ---------- Ingredient ----------
// One stocked ingredient; owns its own quantity so reserve/release/refill stay in one place.
class Ingredient {
quantity: number;
constructor(public readonly name: string, quantity: number, public readonly capacity: number) {
this.quantity = quantity;
}
// Atomic check-and-decrement: never lets quantity go negative.
reserve(qty: number): boolean {
if (this.quantity < qty) return false;
this.quantity -= qty;
return true;
}
release(qty: number): void { this.quantity = Math.min(this.capacity, this.quantity + qty); }
refill(qty: number): void { this.quantity = Math.min(this.capacity, this.quantity + qty); }
toString(): string { return `${this.name}(${this.quantity}/${this.capacity})`; }
}
// ---------- Recipe ----------
// Pure data: what a beverage needs and how much it costs. No computation.
class Recipe {
constructor(public readonly requirements: Map<Ingredient, number>, public readonly priceCents: number) {}
}
// ---------- Beverage ----------
// A sellable item. Composes its Recipe -- the recipe has no meaning outside the beverage.
class Beverage {
constructor(public readonly name: string, public readonly recipe: Recipe) {}
toString(): string { return this.name; }
}
// ---------- PaymentStrategy (Strategy interface) ----------
interface PaymentStrategy {
charge(amountCents: number): void;
refund(amountCents: number): void;
}
class CashPayment implements PaymentStrategy {
inserted = 0;
insert(cents: number): void { this.inserted += cents; }
charge(amountCents: number): void {
if (this.inserted < amountCents) throw new Error('Insufficient cash inserted');
this.inserted -= amountCents;
console.log(` [CASH] charged ${amountCents}c, change=${this.inserted}c`);
}
refund(amountCents: number): void {
this.inserted += amountCents;
console.log(` [CASH] refunded ${amountCents}c`);
}
}
class CardPayment implements PaymentStrategy {
charge(amountCents: number): void { console.log(` [CARD] charged ${amountCents}c`); }
refund(amountCents: number): void { console.log(` [CARD] refunded ${amountCents}c`); }
}
// ---------- CoffeeMachineState (State interface) ----------
interface CoffeeMachineState {
selectBeverage(ctx: CoffeeMachine, b: Beverage): void;
insertPayment(ctx: CoffeeMachine, method: PaymentStrategy, amountCents: number): void;
dispense(ctx: CoffeeMachine): void;
cancel(ctx: CoffeeMachine): void;
}
class IdleState implements CoffeeMachineState {
selectBeverage(ctx: CoffeeMachine, b: Beverage): void {
ctx.selectedBeverage = b;
ctx.setState(ctx.selectingState);
console.log(` Selected ${b} -- please pay ${b.recipe.priceCents}c`);
}
insertPayment(): void { console.log(' Reject: select a beverage first'); }
dispense(): void { console.log(' Reject: nothing selected'); }
cancel(): void { console.log(' Nothing to cancel'); }
}
class SelectingState implements CoffeeMachineState {
selectBeverage(): void { console.log(' Reject: already selecting'); }
insertPayment(ctx: CoffeeMachine, method: PaymentStrategy, amountCents: number): void {
const price = ctx.selectedBeverage!.recipe.priceCents;
if (amountCents < price) { console.log(' Reject: insufficient payment'); return; }
ctx.paymentMethod = method;
method.charge(price);
ctx.setState(ctx.dispensingState);
console.log(' Payment accepted -> dispensing');
}
dispense(): void { console.log(' Reject: pay first'); }
cancel(ctx: CoffeeMachine): void {
ctx.selectedBeverage = null;
ctx.setState(ctx.idleState);
console.log(' Selection cancelled');
}
}
class DispensingState implements CoffeeMachineState {
selectBeverage(): void { console.log(' Reject: already dispensing'); }
insertPayment(): void { console.log(' Reject: already paid'); }
dispense(ctx: CoffeeMachine): void { ctx.dispenseNow(); }
cancel(ctx: CoffeeMachine): void {
ctx.paymentMethod!.refund(ctx.selectedBeverage!.recipe.priceCents);
ctx.selectedBeverage = null;
ctx.setState(ctx.idleState);
console.log(' Cancelled after payment; refunded');
}
}
class OutOfStockState implements CoffeeMachineState {
selectBeverage(): void { console.log(' Reject: machine out of stock'); }
insertPayment(): void { console.log(' Reject: machine out of stock'); }
dispense(): void { console.log(' Reject: machine out of stock'); }
cancel(ctx: CoffeeMachine): void {
ctx.selectedBeverage = null;
ctx.setState(ctx.idleState);
console.log(' Reset from out-of-stock -> idle');
}
}
// ---------- CoffeeMachine (context/facade) ----------
class CoffeeMachine {
readonly idleState: CoffeeMachineState = new IdleState();
readonly selectingState: CoffeeMachineState = new SelectingState();
readonly dispensingState: CoffeeMachineState = new DispensingState();
readonly outOfStockState: CoffeeMachineState = new OutOfStockState();
currentState: CoffeeMachineState = this.idleState;
readonly ingredients = new Map<string, Ingredient>();
readonly beverages = new Map<string, Beverage>();
selectedBeverage: Beverage | null = null;
paymentMethod: PaymentStrategy | null = null;
setState(state: CoffeeMachineState): void { this.currentState = state; }
addIngredient(ing: Ingredient): void { this.ingredients.set(ing.name, ing); }
addBeverage(b: Beverage): void { this.beverages.set(b.name, b); }
select(b: Beverage): void { this.currentState.selectBeverage(this, b); }
pay(method: PaymentStrategy, amountCents: number): void { this.currentState.insertPayment(this, method, amountCents); }
dispense(): void { this.currentState.dispense(this); }
cancel(): void { this.currentState.cancel(this); }
refill(ing: Ingredient, qty: number): void { ing.refill(qty); } // technician action, legal in any state
// Reserve every required ingredient in sequence; on the first shortfall, roll back whatever
// was already reserved, refund, and flip to OutOfStockState. Never dispenses a half-made drink.
dispenseNow(): void {
const recipe = this.selectedBeverage!.recipe;
const reserved: [Ingredient, number][] = [];
for (const [ingredient, qty] of recipe.requirements) {
if (!ingredient.reserve(qty)) {
for (const [ing, q] of reserved) ing.release(q); // roll back
this.paymentMethod!.refund(recipe.priceCents);
this.setState(this.outOfStockState);
console.log(` Out of stock: missing ${ingredient.name} -- refunded and rolled back`);
return;
}
reserved.push([ingredient, qty]);
}
console.log(` Pouring ${this.selectedBeverage}...`);
this.selectedBeverage = null;
this.setState(this.idleState);
}
}
// ---------- Demo ----------
function main(): void {
const machine = new CoffeeMachine();
const water = new Ingredient('water', 500, 1000);
const milk = new Ingredient('milk', 500, 1000);
const coffee = new Ingredient('coffee', 100, 500);
const sugar = new Ingredient('sugar', 200, 500);
machine.addIngredient(water);
machine.addIngredient(milk);
machine.addIngredient(coffee);
machine.addIngredient(sugar);
const latte = new Beverage('Latte', new Recipe(new Map([[water, 100], [milk, 150], [coffee, 30]]), 250));
const espresso = new Beverage('Espresso', new Recipe(new Map([[water, 50], [coffee, 40]]), 150));
machine.addBeverage(latte);
machine.addBeverage(espresso);
console.log('-- Happy path: buy a Latte with cash --');
machine.select(latte);
const cash = new CashPayment();
cash.insert(300);
machine.pay(cash, 300);
machine.dispense();
console.log('\n-- Buy several Espressos with card until coffee runs out --');
for (let i = 0; i < 12; i++) {
machine.select(espresso);
if (machine.currentState === machine.selectingState) {
const card = new CardPayment();
machine.pay(card, 150);
}
machine.dispense();
if (machine.currentState === machine.outOfStockState) {
console.log(` Machine reports out of stock after ${i + 1} attempts`);
break;
}
}
console.log('\n-- Technician refills coffee, machine recovers --');
machine.refill(coffee, 200);
machine.cancel(); // reset from OutOfStock -> Idle
machine.select(espresso);
const card2 = new CardPayment();
machine.pay(card2, 150);
machine.dispense();
console.log('\n-- Cancel after paying (refund) --');
machine.select(latte);
const cash2 = new CashPayment();
cash2.insert(250);
machine.pay(cash2, 250);
machine.cancel();
}
main();