Object-Oriented Design Interview

Course Content

Object-Oriented Design Interview

14 sections · 29 lessons

Shipping Locker: lifecycles, allocation, access codes and extensions


The familiar parts of this design — sizes, fitting, claiming a free compartment — carry straight over from the parking lot. This lesson is about the parts that do not: two interlocking lifecycles, some of whose transitions are driven only by a clock, a drop-off that takes two steps, and access codes that have to be secure as well as correct.

The parcel and locker lifecycle

Two lifecycles that interlock: the locker's, which repeats forever, and the parcel's, which runs once. Drawing both and showing where they touch is the point of this step.

Locker states

StateMeaningCan be allocated?
FREEEmpty and availableYes
RESERVEDAllocated to a courier who has not yet dropped offNo
OCCUPIEDHolds a parcel awaiting collectionNo
EXPIREDHolds a parcel past its deadline, awaiting reclaimNo
OUT_OF_SERVICEBroken lock, jammed door, or under maintenanceNo

The RESERVED state is the one candidates leave out, and it matters. There is a real gap — seconds to a minute — between the system saying "use locker A14" and the courier physically closing the door. Without a reserved state, a second courier can be sent to the same locker during that gap. This is the same claim-do-not-peek idea as the parking lot spot assignment, made visible as a state.

Parcel states

IN_TRANSIT → AWAITING_PICKUP → COLLECTED, with a branch to EXPIRED → RETURNED.

LockerFREEOCCUPIEDOUT OF SERVICEparcel depositedcollected, or returned tocourierfault reportedParcelIN TRANSITAWAITING PICKUPCOLLECTEDRETURNEDdeposited; code sentcorrect codecode expired (72 h)the same event moves both machinesexpiry frees the locker and returns the parcelTwo machines, kept in step by shared events. Modelling the parcel and the locker as one state machine is what makes the 72-hour return rule impossible to express.
A locker is free or occupied; a parcel is in transit, waiting, collected or returned — the same event advances both.

Why two machines rather than one

The temptation is to give the locker a status that covers everything and drop the parcel lifecycle. It fails for a concrete reason: a parcel exists before it reaches a locker and after it leaves one. Its state must be answerable when no locker is involved — "where is my parcel?" is a question about the parcel, not about a compartment.

Keeping them separate means each machine has a clean set of states, and the interaction is three well-defined moments rather than a fog of combined statuses.

The transitions driven by time

Two transitions have no actor: the reservation timeout (a courier who never arrives) and the pickup deadline. Both need the same mechanism decision as the movie booking seat hold — a periodic sweep, a lazy evaluation on read, or both.

Recommend both, for the same reason: lazy evaluation keeps correctness (an expired code can never validate, even if the sweeper is down), and the sweep keeps the availability index honest so a free locker is actually offered to the next courier.

Allocation and access, in code

Two methods: allocating a locker at drop-off, and validating a code at pickup.

Drop-off to pickup, end to endMeasurethe parcelStrategypicks a lockerReserve itatomicallyIssue ahashed codeValidateat pickupSmallest-fitting locker is one strategy; nearest-to-entrance is another, and both plug in.
The code is stored as a hash, so a leaked database still cannot open a single locker.

Allocation behind a Strategy

The rule today is "smallest locker that fits". Requirement N4 said it must be replaceable, so it is an interface — and the justification is the one from Abstraction:

"Allocation is the thing the network operator will want to tune. Smallest-fitting is right for capacity, but an accessibility rule — never put a parcel above shoulder height for a recipient who needs it low — is a business decision that will arrive later. One interface, one class per rule."

Java
public interface AllocationStrategy {    Optional<Locker> allocate(LockerBank bank, ParcelSize size);}public class SmallestFittingStrategy implements AllocationStrategy {    public Optional<Locker> allocate(LockerBank bank, ParcelSize size) {        for (LockerSize candidate : LockerSize.fittingSizes(size)) {   // smallest first            Optional<Locker> locker = bank.claimFree(candidate);       // claim, do not peek            if (locker.isPresent()) return locker;        }        return Optional.empty();    }}

claimFree removes the locker from the free queue as it returns it — the same claim-do-not-peek move as the parking lot spot assignment, and it is again what removes most of the race before any lock exists.

Drop-off, end to end

Java
public Reservation dropOff(Parcel parcel, String bankId, Courier courier) {    LockerBank bank = bankRepository.find(bankId);    Locker locker = strategy.allocate(bank, parcel.getSize())        .orElseThrow(() -> new NoLockerAvailableException(bankId, parcel.getSize()));    locker.reserve(courier, clock.now().plus(RESERVATION_WINDOW));    // FREE -> RESERVED    Reservation reservation = new Reservation(        idGenerator.next(), parcel, locker,        clock.now(), clock.now().plus(rules.pickupWindow()));         // e.g. 3 days    lockerController.open(locker.getId());                            // hardware    reservationRepository.save(reservation);    return reservation;}public void confirmDeposit(String reservationId) {                    // door closed    Reservation reservation = reservationRepository.find(reservationId);    reservation.getLocker().occupy(reservation);                      // RESERVED -> OCCUPIED    reservation.getParcel().markAwaitingPickup();    AccessCode code = accessCodeService.issueFor(reservation);    notifier.notifyRecipient(reservation, code);}

Two things to narrate:

  • Drop-off is two steps, not one. Reserve and open; then, when the door sensor reports closed, occupy and notify. If the courier walks away without depositing, the reservation times out and the locker returns to FREE — the dashed transition in the lifecycle diagram above.
  • The notification happens after the door closes, never before. A recipient who receives a code for an empty locker will arrive and find nothing.

Access codes: generation, storage, validation

Java
public class AccessCodeService {    private static final int CODE_LENGTH = 6;    private final SecureRandom random = new SecureRandom();    public AccessCode issueFor(Reservation reservation) {        reservation.invalidateExistingCodes();               // resends invalidate the old code        String value = randomDigits(CODE_LENGTH);        AccessCode code = new AccessCode(hash(value), reservation.getExpiresAt());        reservation.addCode(code);        return code.withPlaintextForDelivery(value);         // plaintext exists only here    }    public boolean validate(String bankId, String attempt) {        Optional<Reservation> match = reservationRepository            .findActiveByBankAndCodeHash(bankId, hash(attempt));        if (match.isEmpty()) { rateLimiter.recordFailure(bankId); return false; }        Reservation reservation = match.get();        if (!reservation.getActiveCode().validate(attempt, clock.now())) return false;        reservation.getActiveCode().markUsed();              // single use, enforced here        return true;    }}

Four decisions worth saying out loud, and each is one line of code:

  1. SecureRandom, not Random. A predictable code is a parcel someone else collects. This is a one-word change with a real security consequence, and interviewers notice it.
  2. Store a hash, not the code. Anyone reading the database can otherwise open every locker in the network.
  3. Single-use is enforced in validate, at the moment of success, not by the caller. Behaviour with the data it needs (Phase 4: assigning responsibilities).
  4. Rate-limit failed attempts per bank. A six-digit code is one in a million, but a keypad accepting unlimited guesses reduces that to patience. Three failures then a lockout window is the standard shape.

A six-digit code is a reasonable choice to defend out loud: short enough to type on a keypad in the rain, and with rate limiting, sufficient. Without rate limiting it would not be.

Extensions

1. Returns — the reverse flow

A customer returns an item: they book a slot, get a code, put the parcel in, and a courier collects it later. The states are the same and the actors swap:

Five follow-ups, one of them the reverse flowLocker extensionsReturns: actors swapChilled and oversizedCapacity across a siteNotification retriesNever collected
Returns need no new states — the same lifecycle runs with courier and recipient swapped.
DeliveryReturn
Who depositsCourierCustomer
Who collectsRecipientCourier
Who needs a codeRecipient (to open)Customer (to open and deposit)
Locker statesFREE → RESERVED → OCCUPIED → FREEIdentical

The design point: this is not a new state machine, it is the same one with different actors. If your model has CourierDropOff and RecipientPickup as classes, this extension is painful; if it has Reservation with a depositor and a collector, it is a configuration change. Say which your design is — and if it is the first, say what you would change.

2. Refrigerated and oversized lockers

The same problem as electric-vehicle charging spots in the parking lot extensions, and the same answer: features, not subclasses.

Java
public class Locker {    private final LockerSize size;    private final Set<LockerFeature> features;    // REFRIGERATED, GROUND_LEVEL, EXTRA_SECURE}public class Parcel {    private final Set<LockerFeature> required;    // empty for most parcels}

Allocation gains one filter: the locker must fit the size and carry every required feature. A RefrigeratedLocker extends Locker gets you stuck the moment a parcel needs refrigerated and ground-level.

Naming the transfer explicitly — "this is the same shape as electric-vehicle spots in the parking lot problem" — is worth saying. It is evidence of the transfer skill this section opened with.

3. Capacity planning across a location

If a location is full of small lockers and every parcel is medium, the network operator is losing money. Two ways to attack it:

  • Reporting: a rejected drop-off is a data point. Record every NoLockerAvailableException with the size requested and the time, and the demand mix falls out of the log. This is one line of code and the highest-value thing you can offer.
  • Adjustable lockers: some real networks have removable dividers, so two small lockers can merge into one medium. That makes size mutable, which breaks the assumption that a locker's size is final — mention it as a design consequence rather than implementing it.

4. Notification retries

The recipient's message may fail — a wrong number, a full inbox, a network problem. Three things follow:

  • Notification is an interface with implementations per channel (message, email, app push), which is the polymorphism example from Polymorphism nearly verbatim.
  • Retry with backoff, and fall back to another channel after N failures.
  • The deadline does not start until the notification succeeds. This is the good detail: if a recipient is never told, the three-day clock counting down is unfair. Either start the clock on successful delivery of the notification, or extend the deadline when notification is late. Raising this unprompted is a strong move because it is a policy question hiding in a technical one.

5. The recipient who never collects

The complete lifecycle, and worth walking through because it is the question that closes this problem:

  1. Day 3: parcel expires. Locker moves to EXPIRED, parcel to EXPIRED. The code stops validating.
  2. The recipient is notified that collection has failed.
  3. A courier is dispatched to reclaim. They authenticate as a courier — a different credential path from a recipient code — open the locker, and take the parcel.
  4. Locker returns to FREE. Parcel moves to RETURNED_TO_SENDER.
  5. The locker is not free before step 3. This is the part people get wrong: expiry does not free the compartment, because the parcel is physically still inside it. Only a human removing it does.

That last sentence is the one to say. It is a small piece of physical reasoning that a purely software model misses, and it is exactly the kind of detail that distinguishes a candidate who thought about the machine from one who thought about the diagram.