Skip to main content

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.

CourierLockerBankAssignmentStrategyLockerCustomerdropOff(package)1selectLocker(size)2store(package)3new Delivery(code)4verifyAndOpen(code)5release()6
  1. 1The courier only ever talks to the bank, never picks a locker itself.
  2. 2The bank delegates the "which locker" decision entirely.
  3. 3The chosen locker is filled and marked occupied.
  4. 4A pickup code is minted and tied to this delivery, not to the locker.
  5. 5Later, the customer presents only the code - never a locker number.
  6. 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

«interface»LockerAssignmentStrategy+ selectLocker(lockers, size): LockerLockerBank- lockers: List<Locker>- strategy: LockerAssignmentStrategy- active: Map<string, Delivery>+ dropOff(pkg): Delivery+ verifyAndOpen(code): PackageSmallestFitStrategy+ selectLocker(lockers, size): LockerLocker- id: string- size: LockerSize- contents: Package+ isEmpty(): bool+ store(pkg)+ release(): PackagePackage- id: string- size: LockerSize- customerId: stringDelivery- pkg: Package- locker: Locker- pickupCode: string- droppedAt: datetime
implementsusescreates
LockerBank assigns and releases; LockerAssignmentStrategy picks the locker; Delivery ties a drop-off to its pickup code.

Code

import java.time.LocalDateTime;
import java.util.*;
 
enum LockerSize { SMALL, MEDIUM, LARGE }
 
class Package {
final String id;
final LockerSize size;
final String customerId;
 
Package(String id, LockerSize size, String customerId) {
this.id = id;
this.size = size;
this.customerId = customerId;
}
}
 
class Locker {
final String id;
final LockerSize size;
private Package contents;
 
Locker(String id, LockerSize size) {
this.id = id;
this.size = size;
}
 
boolean isEmpty() {
return contents == null;
}
 
boolean fits(LockerSize needed) {
return isEmpty() && size.ordinal() >= needed.ordinal();
}
 
void store(Package pkg) {
this.contents = pkg;
}
 
Package release() {
Package pkg = contents;
contents = null;
return pkg;
}
}
 
interface LockerAssignmentStrategy {
Optional<Locker> selectLocker(List<Locker> lockers, LockerSize size);
}
 
class SmallestFitStrategy implements LockerAssignmentStrategy {
public Optional<Locker> selectLocker(List<Locker> lockers, LockerSize size) {
return lockers.stream()
.filter(l -> l.fits(size))
.min(Comparator.comparing(l -> l.size));
}
}
 
class Delivery {
final Package pkg;
final Locker locker;
final String pickupCode;
final LocalDateTime droppedAt;
 
Delivery(Package pkg, Locker locker, String pickupCode) {
this.pkg = pkg;
this.locker = locker;
this.pickupCode = pickupCode;
this.droppedAt = LocalDateTime.now();
}
}
 
class LockerBank {
private final List<Locker> lockers;
private final LockerAssignmentStrategy strategy;
private final Map<String, Delivery> byCode = new HashMap<>();
 
LockerBank(List<Locker> lockers, LockerAssignmentStrategy strategy) {
this.lockers = lockers;
this.strategy = strategy;
}
 
Delivery dropOff(Package pkg) {
Locker locker = strategy.selectLocker(lockers, pkg.size)
.orElseThrow(() -> new IllegalStateException("No locker free for size " + pkg.size));
locker.store(pkg);
String code = UUID.randomUUID().toString().substring(0, 6).toUpperCase();
Delivery delivery = new Delivery(pkg, locker, code);
byCode.put(code, delivery);
return delivery;
}
 
Package verifyAndOpen(String code) {
Delivery delivery = byCode.remove(code);
if (delivery == null) throw new IllegalArgumentException("Invalid or used code");
return delivery.locker.release();
}
}

Design decisions

  • LockerAssignmentStrategy is its own interface, not a method on LockerBank. 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 to parking-lot.mdx's FeeStrategy is intentional, both are "the one thing that changes" wrapped so nothing else has to.
  • The pickup code lives on the Delivery, not the Locker. 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.
  • verifyAndOpen takes 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 - byCode holds the Delivery forever and the locker stays occupied indefinitely. A real system needs a background sweep comparing Delivery.droppedAt against an expiry window, and on expiry, calling the same release() path a successful pickup uses, plus a separate return-to-sender workflow that doesn't touch LockerBank at all.
  • How would you support one bank running low on large lockers while a nearby bank has free ones? LockerAssignmentStrategy.selectLocker only sees one bank's lockers today. Extending it to span banks means the strategy interface would need to accept a list of banks (or LockerBank would delegate to a higher-level LockerNetwork that 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: selectLocker finds a free locker and store() fills it as two separate steps, so two threads can both pass the "is it free" check before either calls store. Fixing it means making "find and claim" one atomic operation on Locker itself - the same compare-and-set shape used for seat locking in movie-booking.mdx.
  • How would you let a customer extend their pickup window from their phone? Add an extend(code) method to LockerBank that looks up the Delivery by code and pushes out whatever expiry timestamp the sweep above checks - no change needed to Locker or Package, since the pickup window is a property of the Delivery record, not the physical locker.

Check yourself

Question 1 of 3

Why does the pickup code live on `Delivery` instead of on `Locker`?