Amazon Locker
A self-service pickup point: a courier drops a package into whichever locker fits it, the system texts the customer a code, and the customer punches that code into a keypad to get their package back. The whole design lives or dies on one question - who is allowed to open a given door, and for how long.
Requirements
Functional
- A courier drops off a package for a customer; the system picks a free locker of a suitable size and stores the package inside it.
- The customer receives a pickup code for that package.
- The customer enters the pickup code at the bank's keypad; if it matches an occupied locker, that locker opens and is freed for the next package.
- A locker bank has a fixed set of lockers in several sizes (small, medium, large).
Non-functional
- A pickup code must not unlock the wrong locker, and must not still work after the package has already been collected.
- Assigning a locker should never hand out one that's already holding another package, even if drop-offs and pickups happen back-to-back at the same bank.
Design
LockerBank is the only class that knows about physical lockers - it hands one out through
a pluggable LockerAssignmentStrategy and takes it back through verifyAndOpen. A Package
never opens its own locker; it just sits inside one until a matching code shows up at the
bank.
- 1The courier only ever talks to the bank, never picks a locker itself.
- 2The bank delegates the "which locker" decision entirely.
- 3The chosen locker is filled and marked occupied.
- 4A pickup code is minted and tied to this delivery, not to the locker.
- 5Later, the customer presents only the code - never a locker number.
- 6A matching code frees the locker for the next delivery.
The pickup code is generated once, at drop-off, and is checked against exactly the locker it
was minted for. Delivery is the record that ties a courier's drop-off to a customer's
eventual pickup, independent of which physical locker ends up being used.
Class diagram
Code
Design decisions
LockerAssignmentStrategyis its own interface, not a method onLockerBank. The obvious first cut is smallest-fit (give the package the smallest locker it fits in, so large lockers stay free for large packages); a real bank might instead balance by location within the bank to keep foot traffic spread out. Pulling it out means that swap never touches drop-off or pickup logic - the parallel toparking-lot.mdx'sFeeStrategyis intentional, both are "the one thing that changes" wrapped so nothing else has to.- The pickup code lives on the
Delivery, not theLocker. A locker is just a numbered box; it has no idea whose package is inside or what code opens it. That knowledge belongs to the delivery record, so a locker can be reused for a completely different customer the moment it's freed, with zero cleanup on the locker itself. verifyAndOpentakes a code, not a locker id. If the caller had to say "open locker 17" the system couldn't tell a legitimate pickup from someone trying doors at random. Code lookup means the only path to opening a locker is knowing the secret that was texted to the rightful customer.- What's missing for a real system: codes need an expiry (a package sitting for two weeks should trigger a return-to-sender flow, not stay claimable forever), and concurrent drop-offs at the same bank need the assignment step to be atomic - two couriers requesting a locker in the same instant must never both get handed locker 17.
Common follow-ups
- What happens if a customer never picks up their package? Nothing in this design ever
reclaims the locker -
byCodeholds theDeliveryforever and the locker stays occupied indefinitely. A real system needs a background sweep comparingDelivery.droppedAtagainst an expiry window, and on expiry, calling the samerelease()path a successful pickup uses, plus a separate return-to-sender workflow that doesn't touchLockerBankat all. - How would you support one bank running low on large lockers while a nearby bank has
free ones?
LockerAssignmentStrategy.selectLockeronly sees one bank's lockers today. Extending it to span banks means the strategy interface would need to accept a list of banks (orLockerBankwould delegate to a higher-levelLockerNetworkthat tries the nearest bank first, then the next) - the strategy's contract doesn't have to change, only what's fed into it. - Two couriers request a locker for the same size at the exact same instant - what stops
them both getting locker 17? As written, nothing does:
selectLockerfinds a free locker andstore()fills it as two separate steps, so two threads can both pass the "is it free" check before either callsstore. Fixing it means making "find and claim" one atomic operation onLockeritself - the same compare-and-set shape used for seat locking inmovie-booking.mdx. - How would you let a customer extend their pickup window from their phone? Add an
extend(code)method toLockerBankthat looks up theDeliveryby code and pushes out whatever expiry timestamp the sweep above checks - no change needed toLockerorPackage, since the pickup window is a property of theDeliveryrecord, not the physical locker.
Check yourself
Why does the pickup code live on `Delivery` instead of on `Locker`?