Vending machine
Version 1 — Simple, cash-only (have this cold)¶
public class Product
{
public string ProductId { get; init; }
public string Name { get; init; }
}
public class Slot
{
public string SlotId { get; init; }
public Product Product { get; set; }
public decimal UnitPrice { get; set; }
public int Stock { get; set; }
public bool IsAvailable => Stock > 0;
public void Dispense()
{
if (!IsAvailable) throw new InvalidOperationException("Slot empty");
Stock -= 1;
}
}
public enum MachineState { Idle, HasMoney, Dispensing }
public class SelectionResult
{
public bool IsSuccess { get; init; }
public string Message { get; init; }
public decimal ChangeDue { get; init; }
public static SelectionResult OutOfStock() =>
new() { IsSuccess = false, Message = "Out of stock" };
public static SelectionResult InsufficientBalance(decimal shortBy) =>
new() { IsSuccess = false, Message = $"Insert {shortBy} more" };
public static SelectionResult Dispensed(decimal change) =>
new() { IsSuccess = true, Message = "Dispensed", ChangeDue = change };
}
public class VendingMachine
{
private readonly Dictionary<string, Slot> _slots;
private MachineState _state = MachineState.Idle;
private decimal _balance;
public VendingMachine(IEnumerable<Slot> slots)
{
_slots = slots.ToDictionary(s => s.SlotId);
}
public void InsertCoin(decimal amount)
{
_balance += amount;
_state = MachineState.HasMoney;
}
public SelectionResult SelectSlot(string slotId)
{
if (_state != MachineState.HasMoney)
throw new InvalidOperationException("Insert money first");
var slot = _slots.GetValueOrDefault(slotId)
?? throw new InvalidOperationException("No such slot");
if (!slot.IsAvailable)
return SelectionResult.OutOfStock();
if (_balance < slot.UnitPrice)
return SelectionResult.InsufficientBalance(slot.UnitPrice - _balance);
return Dispense(slot);
}
private SelectionResult Dispense(Slot slot)
{
_state = MachineState.Dispensing;
slot.Dispense();
var change = _balance - slot.UnitPrice;
_balance = 0;
_state = MachineState.Idle;
return SelectionResult.Dispensed(change);
}
public decimal Cancel()
{
var refund = _balance;
_balance = 0;
_state = MachineState.Idle;
return refund;
}
}
Version 2 — Multi-payment (Strategy + DI Factory)¶
public enum PaymentType { Cash, Upi }
public class PaymentResult
{
public bool IsSuccess { get; init; }
public string FailureReason { get; init; }
public static PaymentResult Success() => new() { IsSuccess = true };
public static PaymentResult Failed(string reason) => new() { IsSuccess = false, FailureReason = reason };
}
public interface IPaymentMethod
{
PaymentType Type { get; }
void Credit(decimal amount);
PaymentResult Settle(decimal amount);
decimal Refund();
}
public class CashPaymentMethod : IPaymentMethod
{
public PaymentType Type => PaymentType.Cash;
private decimal _wallet;
public void Credit(decimal amount) => _wallet += amount;
public PaymentResult Settle(decimal amount)
{
if (_wallet < amount) return PaymentResult.Failed($"Short by {amount - _wallet}");
_wallet -= amount;
return PaymentResult.Success();
}
public decimal Refund()
{
var leftover = _wallet;
_wallet = 0;
return leftover;
}
}
public interface IUpiGateway
{
(bool Success, string Reason, string TxnRef) ConfirmPayment(decimal amount);
void RefundFull(string txnRef, decimal amount);
}
public class UpiPaymentMethod : IPaymentMethod
{
public PaymentType Type => PaymentType.Upi;
private readonly IUpiGateway _gateway;
private decimal _wallet;
private string _txnRef;
public UpiPaymentMethod(IUpiGateway gateway) => _gateway = gateway;
public void Credit(decimal amount)
{
var (success, reason, txnRef) = _gateway.ConfirmPayment(amount);
if (!success) throw new InvalidOperationException(reason);
_wallet += amount;
_txnRef = txnRef;
}
public PaymentResult Settle(decimal amount)
{
if (_wallet < amount) return PaymentResult.Failed($"Short by {amount - _wallet}");
_wallet -= amount;
return PaymentResult.Success();
}
public decimal Refund()
{
var leftover = _wallet;
_wallet = 0;
if (leftover > 0) _gateway.RefundFull(_txnRef, leftover);
return leftover;
}
}
public interface IPaymentMethodFactory
{
IPaymentMethod Create(PaymentType type);
}
public class PaymentMethodFactory : IPaymentMethodFactory
{
private readonly IServiceProvider _serviceProvider;
private readonly Dictionary<PaymentType, Type> _typeMap;
public PaymentMethodFactory(IServiceProvider serviceProvider, IEnumerable<IPaymentMethod> discovery)
{
_serviceProvider = serviceProvider;
_typeMap = discovery.ToDictionary(m => m.Type, m => m.GetType());
}
public IPaymentMethod Create(PaymentType type) =>
(IPaymentMethod)_serviceProvider.GetRequiredService(_typeMap[type]);
}
public enum MachineState { Idle, PaymentSelected, Credited }
public class VendingMachine
{
private readonly Dictionary<string, Slot> _slots;
private readonly IPaymentMethodFactory _paymentFactory;
private IPaymentMethod _paymentMethod;
private MachineState _state = MachineState.Idle;
public VendingMachine(IEnumerable<Slot> slots, IPaymentMethodFactory paymentFactory)
{
_slots = slots.ToDictionary(s => s.SlotId);
_paymentFactory = paymentFactory;
}
public void SelectPaymentMethod(PaymentType type)
{
if (_state != MachineState.Idle)
throw new InvalidOperationException("Transaction already in progress");
_paymentMethod = _paymentFactory.Create(type);
_state = MachineState.PaymentSelected;
}
public void Credit(decimal amount)
{
if (_state != MachineState.PaymentSelected && _state != MachineState.Credited)
throw new InvalidOperationException("Select a payment method first");
_paymentMethod.Credit(amount);
_state = MachineState.Credited;
}
public SelectionResult SelectSlot(string slotId)
{
if (_state != MachineState.Credited)
throw new InvalidOperationException("Insert/credit money first");
var slot = _slots.GetValueOrDefault(slotId)
?? throw new InvalidOperationException("No such slot");
if (!slot.IsAvailable)
return SelectionResult.OutOfStock();
var result = _paymentMethod.Settle(slot.UnitPrice);
if (!result.IsSuccess)
return SelectionResult.PaymentFailed(result.FailureReason);
slot.Dispense();
var change = _paymentMethod.Refund();
_paymentMethod = null;
_state = MachineState.Idle;
return SelectionResult.Dispensed(change);
}
public decimal Cancel()
{
var refund = _paymentMethod?.Refund() ?? 0;
_paymentMethod = null;
_state = MachineState.Idle;
return refund;
}
}
(SelectionResult needs a PaymentFailed factory method added alongside the ones already shown in Version 1 — public static SelectionResult PaymentFailed(string reason) => new() { IsSuccess = false, Message = reason };)
DI registration for Version 2:
services.AddTransient<IPaymentMethod, CashPaymentMethod>();
services.AddTransient<IPaymentMethod, UpiPaymentMethod>();
services.AddSingleton<IPaymentMethodFactory, PaymentMethodFactory>();
Version 1 is what to lead with in the actual interview. Version 2 is your answer to "how would you extend this."