Deep Dive
Why State Pattern for VendingMachine?
- One of the non-functional requirements of the system is to ensure correctness - which can be enforced only by restricting the individual states from defining no more than the rules of the business logic they are responsible for and transitioning to a fixed set of states.
- Alternative is to use if/elses or switch-case blocks to change states. But this pattern involves changing the code when a new state needs to be added.
- Using the state pattern allows addition of a new state by just creating a new class and not changing any of the existing classes or logic.
Using Strategy Pattern for PaymentMethods
- At any point, VendingMachine shouldn't care about the specific payment method being used. It should directly call the methods exposed by the PaymentMethod interface and let the underlying concrete implementation of the interface take care of the payment.
- There can be more than one implementation of PaymentMethod like QRPayment, CardPayment, CashPayment etc. each having a completely different processing logic.
Factory Method - Payment Handler Creation
- Each payment method implementation requires different constructor arguments.
- The factory encapsulates the configuration knowledge so that the VendingMachine doesn't need to know implementation details of every payment type.
Observer pattern - Decoupling Inventory Monitoring
- When inventory drops below a certain point, the maintenance needs to be notified.
- Every consumer like analytics, remote monitoring, dynamic pricing requires editing Inventory.
- With observer, Inventory fires an event and registered listeners handle it independently.
SOLID Guidelines
Single Responsibility Principle
- Each state class handles only behavior for that state. IdleState only knows what to do when the machine is idle.
- Inventory manages stock.
- CoinReservoir manages change.
- VendingMachine orchestrates.
- Product is a catalog entity.
- Slot is a physical position.
- None except the state classes know about the state transitions.
Open/Closed Principle
- Adding a new MaintenanceState only requires creating a new class. No changes to VendingMachine, existing states, Inventory or payment classes.
- Adding a new payment method like WalletPayment requires a new class. No change to VendingMachine or any state.
- Adding a new pricing strategy involves creating a new class and zero changes to Inventory or Slot.
Liskov Substitution Principle
- Any VendingMachineState sub-class can replace the VendingMachine state using the machine.setState method. The machine doesn't know or care what concrete state it holds.
- Any PaymentMethod can be injected. VendingMachine only calls processPayment() and refund(), without knowing if it is coins or card.
Interface Segregation Principle
- PaymentMethod interface only has methods that are relevant for any payment type.
- Every payment type needs to process payment and refund an amount if needed.
Dependency Inversion Principle
- VendingMachine depends on the PaymentMethod interface and not the concrete implementations of it.
- VendingMachine depends on the abstract VendingMachine state and not on individual states like IdleState or HasMoneyState.
Scalability and Flexibility
Thread Safe State Transitions
- Problem: If a coin sensor fires an interrupt while the main service is processing a button press, without synchronization, the running total can be read between an increment, causing the machine to reject a valid purchase.
- Solution: Simplest approach is to make all VendingMachine actionable methods synchronized.
- Every operation acquires the same lock, serializing all access.
- This limits throughput, but for a single physical machine with one user at a time, this is sufficient.
- Trade-off: Prevents wasted CPU cycles from retrying, but severly limits throughput.
Compare and swap for networked machines
- Problem: For machines with a remote monitoring server query inventory every 30 seconds, the synchronized approach blocks reads during dispense operations.
- Solution: Use CAS (compare and swap) on individual slots
- The server reads quantity without locking.
- Dispense uses CAS - if another dispense happened simultaneously, compareAndSet fails and retries. Reads are never blocked.
- Trade-off: Eliminates blocking and context-switching overhead, allowing high concurrency. However, under high write contention, the constant (busy-waiting) retrying can spike CPU usage.
Dynamic Pricing and Remote Configuration
- The modular design supports runtime changes.
- Swapping a PricingStrategy requires changing one reference.
- Adding a new PaymentMethod requires registering one new class with the factory.
- Remote configuration pushes new products and price changes without touching the state machine.
Future Improvements
- Remote inventory monitoring
- Predictive re-stocking with ML
- Mobile payment and loyalty programs
- Dynamic pricing based on demand