ATM OOD: If You Don’t Model Transaction Atomicity, You’ll Fail the Interview


ATM OOD: If You Don’t Model Transaction Atomicity, You’ll Fail the Interview
When designing an ATM system, the single most important idea for withdrawals is atomicity. A withdrawal is not "dispense cash then update balance" or the reverse — it is a single logical transaction that must either commit fully or roll back completely. If you treat the two steps as separate operations, you'll open the door to race conditions, hardware failures, and inconsistent customer balances — and you'll lose points in an interview.
Why atomicity matters
Common failure modes when atomicity is ignored:
- Concurrent withdrawals: two ATMs read the same balance and both debit it, causing an overdraft.
- Hardware failure: the account is debited but the dispenser jams or loses power, so no cash is dispensed.
- Network/transient failures: responses lost or duplicated, leading to ambiguous state at both the bank and ATM.
These show why a withdrawal must be modeled and persisted as a transaction with clear states and transitions so you can audit, recover, and reconcile.
Make Transaction the orchestrator (OOD guidance)
In your object-oriented design, create a Transaction (or WithdrawalTransaction) object that orchestrates the entire flow. It should own the state machine and be persisted, so every step is durably recorded.
Suggested state machine (persisted):
- INITIATED — transaction created, waiting to start
- AUTHORIZED — account has been checked and funds reserved/debited
- CASH_RESERVED — ATM cash is reserved/locked for this transaction (optional depending on hardware)
- DISPENSED — cash successfully dispensed
- COMPLETED — final success (ledger and cashier agree)
- FAILED — terminal failure (with failure reason)
Persist transitions like: INITIATED → AUTHORIZED → CASH_RESERVED → DISPENSED → COMPLETED, or roll back to FAILED if something breaks.
Persisting the state gives you an audit trail and allows recovery jobs to resume or compensate after failures.
Implementation considerations
Database transaction boundary: perform account debit and write the Transaction record inside a single DB transaction when possible (so both the ledger change and the transaction state are atomic in the same store).
Cross-system work (bank ledger vs ATM hardware): if the debit and the physical dispenser reservation are in different systems, you must coordinate. Options:
- Two-phase commit (2PC): strong consistency but complex and often undesirable across heterogeneous systems.
- Saga pattern / compensating actions: prefer sagas for long-running operations (reserve/dispense) — record intents, perform stepwise actions, and define compensations for failures.
Idempotency: give every withdraw request a unique transaction ID. Make each step idempotent so retries don’t cause double debits or double dispenses.
Concurrency control: use optimistic locking (version columns) or DB row-level locks when reserving funds to avoid lost-update problems.
Timeouts and retries: define timeouts for steps like CASH_RESERVED — if the ATM never reports DISPENSED, schedule a reconciler to either retry, cancel, or escalate.
Reconciliation and compensation: build background jobs that scan incomplete transactions and either roll them back (refund) or finalize them (if the dispenser later reports success). Manual intervention should be traceable.
Example pseudocode (simplified)
class WithdrawalTransaction {
enum State { INITIATED, AUTHORIZED, CASH_RESERVED, DISPENSED, COMPLETED, FAILED }
String id; // client-provided idempotency key
State state;
Money amount;
AccountId account;
FailureReason failure;
void start() {
persist(State.INITIATED);
authorizeAndDebit();
reserveCashOnATM();
dispenseAndConfirm();
commit();
}
void authorizeAndDebit() {
// Inside DB transaction: check balance, debit, write state=AUTHORIZED
}
void reserveCashOnATM() {
// Call ATM hardware/service. Persist CASH_RESERVED when success.
}
void dispenseAndConfirm() {
// Ask ATM to dispense. On success mark DISPENSED, else set FAILED and run compensation.
}
void commit() {
// Finalize state->COMPLETED
}
}
What to say in the interview
- Draw the state machine and explain why each state is persisted.
- Describe how you'd make steps idempotent and how you'd handle retries/timeouts.
- Explain your choice for cross-system coordination: why you’d use a saga vs 2PC in this context.
- Talk about reconciliation: a background process that fixes in-doubt transactions and produces an audit log for manual review.
Quick checklist for a solid interview answer
- Show a persisted transaction state machine.
- Mention idempotency keys and unique transaction IDs.
- Discuss concurrency control for balances (locks/optimistic concurrency).
- Explain cross-system coordination (sagas/compensation or 2PC) and trade-offs.
- Describe recovery, reconciliation, and auditability.
Modeling withdrawals as atomic, persisted transactions is the difference between a fragile design and a production-ready one — and it’s an interview-ready talking point that shows you think about correctness, failure modes, and recoverability.



