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.
0%0 of 122 pages studied