Design Amazon Locker
OOP design for a package locker system covering locker size allocation, OTP-based access, package lifecycle tracking, expiry handling, and multi-location management.
The Problem
Your company operates thousands of self-service package lockers in grocery stores, apartment lobbies, and transit stations. Customers choose a locker location at checkout, and a delivery driver drops the package into an assigned locker. The customer receives a one-time pickup code, opens the locker, and grabs their package. Simple in concept, messy in implementation.
The current system assigns lockers randomly, wasting large lockers on tiny packages and forcing drivers to reattempt when no suitable locker is free. Packages that sit uncollected for days block lockers for everyone else, and there is no automated expiry or return-to-warehouse flow.
Design the core classes for a package locker system that handles locker allocation by size, OTP-based pickup, package lifecycle tracking, and expiry handling across multiple locations.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions.
You: "What locker sizes does the system support, and how do we decide which size fits a package?"
Interviewer: "Three sizes: small, medium, and large. Each has fixed dimensions. A package fits the smallest locker whose dimensions can contain it."
Good. That tells us we need a size-matching algorithm, not just random assignment. Now confirm the pickup flow.
You: "How does a customer open their locker? Is it a numeric code, a QR code, or something else?"
Interviewer: "A six-digit one-time code. The customer enters it on the locker's keypad."
So we need code generation, validation, and single-use enforcement. Now think about error cases.
You: "What happens if a package is not picked up within a certain time?"
Interviewer: "It expires after 72 hours. The system marks it for return to the warehouse and frees the locker."
That means we need an expiry checker running on a schedule. Now clarify boundaries.
You: "Should we handle multiple locations, or is this a single locker unit?"
Interviewer: "Multiple locations. Each location has a different number of lockers in various sizes."
You: "Does the system need to handle concurrent deliveries? Two drivers arriving at the same location simultaneously?"
Interviewer: "Yes. Allocation must be thread-safe so two drivers don't get assigned the same locker."
You: "Should we send notifications (email, SMS) to the customer?"
Interviewer: "Yes, notify on package delivery, a reminder before expiry, and when the package actually expires."
That means we need an observer or event system for notifications. One more boundary question.
You: "Are returns or drop-offs in scope? Can a customer use a locker to return a package?"
Interviewer: "Out of scope for the base design. Mention it as an extension."
Perfect. You have clarified scope and ruled out unnecessary complexity.
Final Requirements
Functional Requirements:
- The system manages multiple locker locations, each with lockers of sizes SMALL, MEDIUM, and LARGE
- When a package is ready for delivery, the system allocates the smallest available locker that fits the package dimensions
- On successful allocation, the system generates a unique six-digit pickup code
- The customer enters the code at the locker kiosk to open the assigned locker
- Once picked up, the locker becomes available for the next package
- Packages not picked up within 72 hours are marked expired, and the locker is freed
Non-Functional Requirements:
- Thread-safe locker allocation (concurrent drivers at the same location)
- Extensibility for new locker sizes, notification channels, and allocation strategies
- Observable lifecycle events (delivered, picked up, expired) for notifications
Out of Scope:
- Payment processing and pricing
- Customer return/drop-off flow
- Physical hardware API integration
- Delivery driver routing
Example Inputs and Outputs
Scenario 1: Successful delivery and pickup
- Input: Package (20x15x10 cm) assigned to location "LOC-001"
- System allocates locker S-03 (small, 25x20x15 cm), generates code
847291 - Customer enters
847291at the kiosk - Expected: Locker opens. Package state transitions ASSIGNED to DELIVERED to PICKED_UP. Locker returns to AVAILABLE.
Scenario 2: No suitable locker available
- Input: Package (60x40x30 cm) assigned to location "LOC-001", but all large lockers are occupied
- Expected: Allocation fails. System returns an error. Package remains unassigned for retry or rerouting.
Scenario 3: Package expires
- Input: Package was delivered 72 hours ago, customer never picked it up
- Expected: Expiry checker marks the package EXPIRED, sends a notification, frees the locker, and queues the package for warehouse return.
Try It Yourself
Try it yourself
Before reading the solution, spend 15-20 minutes sketching your own class diagram. Focus on locker state transitions and the allocation strategy. How would you ensure a small package never wastes a large locker? Compare your approach with the walkthrough below.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Look for nouns in your requirements: locker, location, package, code, customer. Each noun is a candidate for a class, but not every noun deserves its own class.
A common mistake is putting locker management, allocation, and notifications into one god class. Good design means each class has a single, clear job.
| Entity | Responsibility | Key attributes |
|---|---|---|
| LockerSystem | Top-level orchestrator. Manages all locations and coordinates allocation. | locations, allocationStrategy |
| LockerLocation | A physical site with lockers. Tracks which lockers are available. | locationId, address, lockers |
| Locker | A single compartment. Knows its size and current state. | lockerId, size, state, currentPackage |
| LockerSize | Enum defining dimensions for SMALL, MEDIUM, LARGE. | width, height, depth |
| Package | A customer's package with dimensions and lifecycle state. | packageId, dimensions, state, assignedLocker, pickupCode |
| PackageState | Enum tracking lifecycle: CREATED, ASSIGNED, DELIVERED, PICKED_UP, EXPIRED. | (enum values) |
| DeliveryCode | Value object wrapping a six-digit code with generation and validation. | code, createdAt, expiresAt |
| Notification | Event fired on lifecycle transitions. | type, recipient, message |
Notice we separated LockerLocation from Locker because a location is a container of lockers with its own identity (address, hours), while a Locker is a physical compartment. Merging them violates SRP because location-level queries ("how many free lockers?") would be entangled with compartment-level state ("is this locker occupied?").
Step 2: Define Relationships and Class Design
Class Diagram
Class Interface Derivation
LockerSystem (The Orchestrator)
The top-level class that ties everything together. It coordinates allocation, pickup, and expiry across all locations.
Deriving state from requirements:
| Requirement | What LockerSystem must track |
|---|---|
| "Multiple locker locations" | A map of all locations by ID |
| "Allocate smallest available locker" | An allocation strategy |
| "Notify customer on delivery/expiry" | A notification service |
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Package ready for delivery" | allocateLocker(pkg, locationId) |
| "Customer enters code" | pickupPackage(locationId, code) |
| "Expired packages freed" | handleExpiredPackages() |
Locker
A single physical compartment. It knows its size and whether it currently holds a package.
Deriving state from requirements:
| Requirement | What Locker must track |
|---|---|
| "Three sizes with dimensions" | The size enum (carries dimensions) |
| "Locker is available or occupied" | Current state |
| "Package sits inside until pickup" | Reference to the current package |
We keep the Locker simple: it knows its own state and delegates size-matching to the LockerSize enum. The allocation decision lives in the strategy, not here.
Package
The data carrier for a customer's shipment. It tracks its own lifecycle state and the code needed for pickup.
My recommendation: keep Package as a data-heavy class with minimal behavior. The lifecycle transitions (ASSIGNED, DELIVERED, PICKED_UP, EXPIRED) are the most interesting part. We validate transitions so you can not skip from CREATED straight to PICKED_UP.
Key Relationship Decisions
- LockerSystem owns LockerLocations (composition): locations do not exist outside the system.
- Locker references Package (optional association): a locker can be empty. When a package is picked up, this reference is cleared.
- AllocationStrategy is an interface: the system can swap algorithms without touching LockerSystem. Classic Strategy pattern.
Step 3: Choose Design Patterns
Pattern: Strategy for Locker Allocation
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.