Interview decision
Represent only behavior-changing state, define legal transitions, and make failure leave the model valid and explainable.
Mental modelState is a controlled lifecycle; invariants are the conditions every successful operation must preserve; errors are part of the interface.
Use it to
- Protect vending inventory and money through purchase failure.
- Prevent delivered packages from returning to in-transit.
- Reject overlapping confirmed reservations.
Keep out of scope
- Recording every historical event as a boolean.
- Building a generic state-machine framework for a small lifecycle.
See the responsibility boundary
A PurchaseSession progresses from collecting money to selected, dispensed, completed, or cancelled while inventory changes only during a successful guarded purchase.
type PurchaseResult = Dispensed | OutOfStock | InsufficientFunds;
interface PurchaseSession {
select(slot: SlotId): PurchaseResult;
cancel(): Refund;
}Represent only meaningful state
State is valuable when behavior changes because of it. Name allowed transitions and reject actions that would violate an invariant.
A package moves from created to scheduled to in-transit to delivered; delivery is rejected until scheduling has happened.
Interviewer follow-ups
- Which transitions are valid?
- What must remain true after each transition?
Do not over-engineer: Do not build a full state-machine framework for two states with one guarded operation.
Put invariants near the state they protect
An invariant is a condition that remains true after every permitted operation. Keeping it near the entity prevents callers from assembling invalid intermediate states.
A RoomCalendar refuses overlapping confirmed reservations; callers cannot append directly to its confirmed list.
Interviewer follow-ups
- Where can an invalid state be prevented earliest?
- Does this rule require another object’s context?
Do not over-engineer: Do not duplicate the same validation in every caller and entity without explaining why.
Design failure behavior deliberately
Errors reveal the contract. Decide whether an operation returns a result, raises a domain error, retries a local transient action, or leaves state unchanged. Explain atomicity when multiple changes occur together.
A vending purchase returns InsufficientFunds or OutOfStock without decrementing inventory. A payment gateway timeout is surfaced as pending rather than treated as success.
Interviewer follow-ups
- What stays unchanged when this fails?
- How can a caller distinguish a conflict from a retryable failure?
Do not over-engineer: Do not add network retry machinery when the exercise only requires an interface boundary.
Validate the design, not the diagram
Walk one representative flow
- Accept money into the active session.
- Validate slot and available inventory.
- Validate funds and compute change.
- Atomically dispense, decrement inventory, and complete the session.
Tests that protect the reasoning
- Out-of-stock leaves money and inventory unchanged.
- Successful purchase decrements exactly one item and returns exact change.
- A completed session rejects another selection.
Evolve it without losing the boundary
- Follow-up change
- Add card authorization with a pending outcome and explain how it changes lifecycle and cancellation semantics.
- Concurrency, when relevant
- A race is natural only if multiple sessions can claim one slot; protect the inventory mutation at the owning machine or inventory boundary.
- System Design boundary
- Payment networks, remote inventory, and durable event history are outside the in-process lifecycle unless explicitly requested.
What depth looks like by level
- Entry
- Name states, legal transitions, and basic failures.
- Mid
- Protect invariants and define atomic changes.
- Senior
- Handle retries, idempotency, and concurrency proportionately.
- Staff+
- Explain external failure ownership and operational implications without turning the answer into distributed architecture.