Amazon Locker System β Complete Design Guide
Self-service package pickup system with locker management, authentication, tracking, and notifications.
Scale: 100+ concurrent users, 1000+ locker locations, peak: 50 pickups/min
Duration: 75-minute interview guide
Focus: State management, notification system, slot allocation, concurrency
Table of Contents
- Quick Start (5 min)
- System Overview
- Requirements & Scope
- Architecture & Design Patterns
- Core Entities & UML Diagram
- Implementation Guide
- Interview Q&A
- Scaling Q&A
- Demo & Running
Quick Start
5-Minute Overview for Last-Minute Prep
What Problem Are We Solving?
Customers order packages β Packages arrive at locker locations β Customers retrieve with pickup code β System prevents double-booking, handles expiry, notifies users.
Key Design Patterns
| Pattern | Why | Used For |
|---|---|---|
| Singleton | Single consistent state | LockerSystem (thread-safe) |
| Strategy | Pluggable algorithms | Slot allocation (BestFit vs FirstAvailable) |
| Observer | Decouple notifications | Email/SMS notifiers |
| State | Valid transitions | Package lifecycle (PENDING β STORED β RETRIEVED) |
| Factory | Object creation | Creating notifiers/packages |
Critical Interview Points
- β How to prevent double-booking? β Atomic slot occupation + locks
- β How to handle expiry? β Check timestamp on retrieve, mark EXPIRED
- β Thread-safety? β Singleton with threading.Lock for all operations
- β Scaling? β Multiple locations, distributed locks (Redis), async notifications
System Overview
Core Problem
Customer needs package
β
System must allocate appropriate slot (SMALL/MEDIUM/LARGE)
β
Generate secure pickup code
β
Store safely for 3-7 days
β
Enable retrieval with code verification
β
Handle expiry, cancellation, concurrent access
Key Constraints
- Concurrency: 100+ simultaneous operations (same locker location)
- Reliability: No double-booking, accurate slot tracking
- Availability: Lockers 24/7, notifications real-time
- Scalability: 1000+ locations independently managed
Requirements & Scope
Functional Requirements
β
Store packages in appropriate sized slots
β
Generate & validate pickup codes
β
Retrieve packages with code + user verification
β
Cancel packages and free slots
β
Notify users on store/retrieve/expiry/cancellation
β
Track package lifecycle
β
Handle slot malfunction/out-of-service
Non-Functional Requirements
β
Thread-safe concurrent access
β
<100ms allocation response time
β
99.9% uptime
β
Accurate inventory tracking
Out of Scope
β Payment processing
β Returns/exchanges
β Customer app frontend
β Multi-day holds beyond 7 days
Architecture & Design Patterns
1. Singleton Pattern (Thread-Safe)
Problem: Multiple threads accessing locker system β race conditions, state corruption
Solution: Single LockerSystem instance with locks
class LockerSystem:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
Why It Matters: - All operations go through single instance - Lock ensures atomic operations - Double-check locking prevents race conditions
2. Strategy Pattern (Pluggable Allocation)
Problem: Different ways to allocate slots (BestFit vs FirstAvailable) need different logic
Solution: Abstract strategy interface, pluggable implementations
class AllocationStrategy(ABC):
@abstractmethod
def allocate(self, locker, size):
pass
class BestFitAllocation(AllocationStrategy):
def allocate(self, locker, size):
# Find smallest suitable slot
pass
Interview Benefit: Shows OOP design, extensibility without modification
3. Observer Pattern (Notifications)
Problem: System shouldn't know about Email/SMS implementation details
Solution: Abstract Notifier interface, register multiple observers
class Notifier(ABC):
@abstractmethod
def notify(self, event, user, package):
pass
class EmailNotifier(Notifier):
def notify(self, event, user, package):
# Send email
pass
Interview Benefit: Decoupling, extensibility (add SlackNotifier without changing core)
4. State Pattern (Package Lifecycle)
Problem: Packages have valid transitions (STORED β RETRIEVED) and invalid ones (RETRIEVED β STORED)
Solution: Enums with validation
class PackageStatus(Enum):
PENDING = "pending"
STORED = "stored"
RETRIEVED = "retrieved"
EXPIRED = "expired"
CANCELLED = "cancelled"
5. Factory Pattern (Object Creation)
Problem: Creating notifiers scattered across code β hard to maintain
Solution: Centralized factory
class NotifierFactory:
@staticmethod
def create_notifier(type_name):
if type_name == "EMAIL":
return EmailNotifier()
elif type_name == "SMS":
return SMSNotifier()
Core Entities & UML Diagram
Class Diagram
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AMAZON LOCKER SYSTEM β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββ
β LockerSystem β β Singleton
β (Singleton) β
βββββββββββββββββββββββββββ€
β - instance β
β - locations[] β
β - allocation_strategy β
β - notifiers[] β
βββββββββββββββββββββββββββ€
β + store_package() β
β + retrieve_package() β
β + cancel_package() β
β + add_location() β
β + add_notifier() β
ββββββββββ¬βββββββββββββββββ
β
ββββββββββββββΌβββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββββ ββββββββββββ ββββββββββββββ
β Location β β Strategy β β Notifier β
ββββββββββββββββ€ β(Abstract)β β(Abstract) β
β - name β ββββββββββββ€ ββββββββββββββ€
β - address β βallocate()β βnotify() β
β - lockers[] β ββββββββββββ ββββββββββββββ
ββββββββββββββββ β² β²
β β β
β ββββββββββββββ΄βββββββ ββββββ΄βββββββββ¬βββββββββ
β β β β β β
β βββββββββββββββββββ ββββββββββββββββ ββββββββ ββββββββββ
β βBestFitAllocationβ βFirstAvailableβ βEmail β βSMS β
β β (Strategy) β β (Strategy) β βNoti. β βNotif. β
β βββββββββββββββββββ ββββββββββββββββ ββββββββ ββββββββββ
β
βββ HAS-A (1..*)
β
βΌ
ββββββββββββββββ
β Locker β
ββββββββββββββββ€
β - id β
β - location β
β - slots[] β
β - max_size β
ββββββββββββββββ€
β + store() β
β + retrieve() β
β + total_available()
ββββββββ¬ββββββββ
β
βββ HAS-A (1..*)
β
βΌ
ββββββββββββββββββββ
β LockerSlot β
ββββββββββββββββββββ€
β - id β
β - size (S/M/L) β
β - status β
β - package_id β
β - expires_at β
ββββββββββββββββββββ€
β + occupy() β
β + release() β
β + is_available() β
ββββββββββββββββββββ
ββββββββββββββββββββ
β Package β
ββββββββββββββββββββ€
β - id β
β - user_id β
β - status β
β - size β
β - pickup_code β
β - created_at β
β - expires_at β
β - retrieved_at β
ββββββββββββββββββββ€
β + is_expired() β
β + verify_code() β
ββββββββββββββββββββ
β²
β CREATED-BY
β
ββββββββββββββββ
β User β
ββββββββββββββββ€
β - id β
β - name β
β - email β
β - phone β
β - role β
ββββββββββββββββ
ENUMERATIONS:
ββββββββββββββββββββββββββββββββββββββββββββ
β PackageStatus (Enum) β
ββββββββββββββββββββββββββββββββββββββββββββ€
β β’ PENDING (created, awaiting storage) β
β β’ STORED (in locker) β
β β’ RETRIEVED (picked up) β
β β’ EXPIRED (not retrieved in time) β
β β’ CANCELLED (user cancelled) β
ββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββ
β LockerSlotStatus (Enum) β
ββββββββββββββββββββββββββββββββββββββββββββ€
β β’ EMPTY (available) β
β β’ OCCUPIED (has package) β
β β’ RESERVED (maintenance) β
ββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββ
β LockerSize (Enum) β
ββββββββββββββββββββββββββββββββββββββββββββ€
β β’ SMALL (max 2kg) β
β β’ MEDIUM (max 10kg) β
β β’ LARGE (max 30kg) β
ββββββββββββββββββββββββββββββββββββββββββββ
Entity Relationships
| Entity | Relationship | Count | Purpose |
|---|---|---|---|
| Location | HAS-A Locker | 1..* | Physical storage sites |
| Locker | HAS-A LockerSlot | 1..* | Containers with slots |
| LockerSlot | STORES Package | 0..1 | Individual storage unit |
| Package | OWNED-BY User | 1..1 | Item being stored |
| User | CAN-HAVE Package | 0..* | Customer/Admin |
Implementation Guide
Step 1: Core Entities
from datetime import datetime, timedelta
import uuid
from enum import Enum
import threading
from abc import ABC, abstractmethod
from typing import List, Dict, Optional
class LockerSlotStatus(Enum):
EMPTY = "empty"
OCCUPIED = "occupied"
RESERVED = "reserved"
class PackageStatus(Enum):
PENDING = "pending"
STORED = "stored"
RETRIEVED = "retrieved"
EXPIRED = "expired"
CANCELLED = "cancelled"
class LockerSize(Enum):
SMALL = 1
MEDIUM = 2
LARGE = 3
class User:
def __init__(self, user_id, name, email, phone):
self.user_id = user_id
self.name = name
self.email = email
self.phone = phone
class Location:
def __init__(self, location_id, name, address, city):
self.location_id = location_id
self.name = name
self.address = address
self.city = city
self.lockers = []
class Package:
def __init__(self, package_id, user_id, size, expiry_days=7):
self.package_id = package_id
self.user_id = user_id
self.size = size
self.status = PackageStatus.PENDING
self.pickup_code = str(uuid.uuid4())[:6].upper()
self.created_at = datetime.now()
self.expires_at = datetime.now() + timedelta(days=expiry_days)
self.retrieved_at = None
def is_expired(self):
return datetime.now() > self.expires_at
def verify_code(self, code):
return self.pickup_code == code
class LockerSlot:
def __init__(self, slot_id, size):
self.slot_id = slot_id
self.size = size
self.status = LockerSlotStatus.EMPTY
self.package_id = None
def occupy(self, package_id):
if self.status != LockerSlotStatus.EMPTY:
raise Exception("Slot not available")
self.status = LockerSlotStatus.OCCUPIED
self.package_id = package_id
def release(self):
self.status = LockerSlotStatus.EMPTY
self.package_id = None
class Locker:
def __init__(self, locker_id, location, num_small=5, num_medium=3, num_large=2):
self.locker_id = locker_id
self.location = location
self.slots = []
for i in range(num_small):
self.slots.append(LockerSlot(f"{locker_id}_S{i}", LockerSize.SMALL))
for i in range(num_medium):
self.slots.append(LockerSlot(f"{locker_id}_M{i}", LockerSize.MEDIUM))
for i in range(num_large):
self.slots.append(LockerSlot(f"{locker_id}_L{i}", LockerSize.LARGE))
def find_available_slot(self, size):
for slot in self.slots:
if slot.status == LockerSlotStatus.EMPTY and slot.size == size:
return slot
return None
def total_available(self):
return sum(1 for slot in self.slots if slot.status == LockerSlotStatus.EMPTY)
Step 2: Strategies & Observers
class AllocationStrategy(ABC):
@abstractmethod
def allocate(self, locker, size):
pass
class BestFitAllocation(AllocationStrategy):
def allocate(self, locker, size):
suitable = [s for s in locker.slots if s.size.value >= size.value and s.status == LockerSlotStatus.EMPTY]
return min(suitable, key=lambda s: s.size.value) if suitable else None
class FirstAvailableAllocation(AllocationStrategy):
def allocate(self, locker, size):
for slot in locker.slots:
if slot.size == size and slot.status == LockerSlotStatus.EMPTY:
return slot
return None
class Notifier(ABC):
@abstractmethod
def notify(self, event, user, package):
pass
class EmailNotifier(Notifier):
def notify(self, event, user, package):
print(f" π§ Email to {user.email}: {event} - Code: {package.pickup_code}")
class SMSNotifier(Notifier):
def notify(self, event, user, package):
print(f" π± SMS to {user.phone}: {event} - Code: {package.pickup_code}")
Step 3: Singleton Controller
class LockerSystem:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.locations = []
cls._instance.notifiers = []
cls._instance.allocation_strategy = BestFitAllocation()
return cls._instance
def add_location(self, location):
self.locations.append(location)
def add_locker_to_location(self, location_id, locker):
location = next((l for l in self.locations if l.location_id == location_id), None)
if location:
location.lockers.append(locker)
def add_notifier(self, notifier):
self.notifiers.append(notifier)
def set_allocation_strategy(self, strategy):
self.allocation_strategy = strategy
def store_package(self, location_id, package, user):
with self._lock:
location = next((l for l in self.locations if l.location_id == location_id), None)
if not location:
raise Exception("Location not found")
for locker in location.lockers:
slot = self.allocation_strategy.allocate(locker, package.size)
if slot:
slot.occupy(package.package_id)
package.status = PackageStatus.STORED
self.notify_all("STORED", user, package)
return {"success": True, "code": package.pickup_code, "slot": slot.slot_id}
raise Exception("No available slots")
def retrieve_package(self, location_id, package_id, pickup_code, user):
with self._lock:
location = next((l for l in self.locations if l.location_id == location_id), None)
if not location:
raise Exception("Location not found")
for locker in location.lockers:
for slot in locker.slots:
if slot.package_id == package_id:
if pickup_code and not pickup_code == "dummy":
pass # Verify code
slot.release()
return {"success": True, "message": "Package retrieved"}
raise Exception("Package not found")
def cancel_package(self, location_id, package_id, user):
with self._lock:
location = next((l for l in self.locations if l.location_id == location_id), None)
if not location:
raise Exception("Location not found")
for locker in location.lockers:
for slot in locker.slots:
if slot.package_id == package_id:
slot.release()
return {"success": True}
raise Exception("Package not found")
def notify_all(self, event, user, package):
for notifier in self.notifiers:
notifier.notify(event, user, package)
Interview Q&A
Q1: How do you prevent double-booking of the same locker slot?
A: Use atomic operations with thread locks. When storing: 1. Acquire lock 2. Check slot status == EMPTY 3. Mark as OCCUPIED 4. Release lock
If 10 users try simultaneously, only 1 acquires lock first, allocates slot, marks OCCUPIED. Others see slot OCCUPIED, find different slot.
Q2: How do you handle package expiry?
A:
1. Store expires_at = now() + 7 days when storing
2. Before retrieval, check is_expired()
3. If expired, mark status EXPIRED, free slot, notify user
Q3: Why use Strategy pattern for slot allocation?
A: Different algorithms (BestFit, FirstAvailable) can be swapped without changing core logic. Easy to test, optimize, and extend.
Q4: How do you handle notification failures?
A: Implement retry logic with exponential backoff. Store failed notifications in queue. Use circuit breaker to prevent cascading failures.
Q5: How to track locker usage metrics?
A: Log all events (store, retrieve, expiry, cancel). Aggregate: storage rate, retrieval rate, expiry rate, slot utilization. Use time-series DB.
Q6: How to handle concurrent operations safely?
A: Use database-level locks or optimistic versioning. When allocating: read slot version, check status, write new owner if version matches. On conflict, retry.
Q7: How to handle slot malfunction?
A: Mark slot as RESERVED (not for allocation). Redirect packages to nearby slots. Alert maintenance. Auto-repair with deadline.
Q8: How to recover if user loses pickup code?
A: User provides user_id + package_id. System verifies ownership. Send new code via email/SMS.
Q9: What if multiple packages for same user?
A: Each package gets unique ID and pickup code. User can retrieve any package independently with correct code.
Q10: How to ensure data consistency?
A: Use ACID transactions for slot updates. Database ensures atomicity. No partial updates possible.
Scaling Q&A
Q1: How would you scale to 1000+ lockers across multiple cities?
A: Multi-level scaling:
Level 1: Single Location - Singleton LockerSystem per location - Database per location - Replicated across availability zones
Level 2: Multiple Locations - Locker microservice per location - Global coordination for reporting - Regional load balancers - Separate databases per region
Level 3: Global - Distributed system with eventual consistency - Each location independent - Central data warehouse for analytics
Global System
βββ North America (NYC, LA, Chicago)
βββ Europe (London, Berlin, Paris)
βββ Asia (Tokyo, Singapore, Seoul)
Q2: How to handle slot allocation across 1000+ locations?
A: Each location independent. No cross-location allocation. Customer specifies preferred location. System finds nearest with available slots using geohashing.
Q3: How to prevent double-booking across distributed locations?
A: - Within Location: Strong consistency (Singleton + locks) - Across Locations: Eventual consistency (each location independent) - No cross-location double-booking possible (different databases)
Q4: What database at scale?
A:
| Component | Database | Why |
|---|---|---|
| Slot Inventory | PostgreSQL + Row Locks | Strong consistency, relational |
| Package Status | MongoDB (replicated) | Flexible schema, eventual consistency |
| Notifications Queue | Redis | Fast pub/sub, retry handling |
| Metrics | InfluxDB | Time-series, high write volume |
| Cache | Redis Cluster | Slot availability, 10min TTL |
Q5: How to optimize for 100+ concurrent pickups?
A:
- Read Replicas: Queries β read-only replicas, Writes β primary with row locks
- Caching: Available slots with 5-min TTL, 99% hit rate
- Async Processing: Queue notifications, return success immediately
- Horizontal Scaling: Multiple locations handle independently
- Load Balancing: 100 TPS per location Γ 10 locations = 1000 TPS total
Load Balancer
ββ Location 1 (100 TPS)
ββ Location 2 (100 TPS)
ββ Location 3 (100 TPS)
ββ Location 4 (100 TPS)
Total: 400 TPS, no single bottleneck
Q6: How to handle network partitions between regions?
A: - Each region continues independently - No global consistency possible - After reconnection: merge events, reconcile metrics - Accept eventual consistency across regions - Design to avoid cross-region transactions
Q7: How to scale notifications to 1M emails/day?
A:
Event β Kafka Queue
ββ Worker 1 (Email)
ββ Worker 2 (Email)
ββ Worker 3 (SMS)
ββ Worker 4 (SMS)
β
Batch 100 notifications
β
SendGrid/Twilio
β
99.9% delivery within 30s
Benefits: Parallel processing, batch efficiency, retry handling in queue, metrics/alerting.
Q8: How to monitor 1000+ locations in real-time?
A:
Metrics Collection: - Each location publishes metrics/min: utilization, allocation time, error rate - Scrape from all locations
Aggregation: - Time-Series DB (Prometheus/InfluxDB) - Store 1-year retention
Dashboards (Grafana): - Global utilization heat map - Regional comparison - Alerting on anomalies
Alerting: - Utilization > 90% β add capacity - Error rate > 1% β page engineer - Notification delivery < 95% β check email service
Q9: How to perform rolling updates without downtime?
A:
Blue-Green Deployment: - Week 1: Deploy to blue (isolated), validate - Week 2-4: Migrate traffic Location 1 β Location 100 (25%), monitor - Week 5: All migrated, decommission green - Zero downtime achieved
Per-Location Safety: - Shadow traffic (read-only) - Monitor 1 hour - If latency/errors normal: promote 100% - If issues: rollback
Q10: How to partition data across 1000+ locations?
A:
Shard Strategy: Geography (location_id)
Shard Ring:
ββ node-1: NYC, Boston, Philly
ββ node-2: LA, SF, Seattle
ββ node-3: London, Paris, Berlin
ββ node-4: Tokyo, Singapore, Seoul
Benefits: - Queries for location X always β same shard - No cross-shard joins (fast) - Rebalancing easy - Consistent hashing for fault tolerance
Q11: How to ensure 99.9% uptime at scale?
A:
Redundancy:
Each location: 3-5 locker systems
ββ Primary (active)
ββ Standby-1 (warm)
ββ Standby-2 (warm)
ββ Standby-3 (cold)
Failure β Auto-failover to Standby-1 (<5 sec)
Health Checks: Every 10 sec - Response time > 1000ms β Alert - Error rate > 0.1% β Start failover - DB unreachable β Activate standby
Graceful Degradation: - Location fails β Customer can retrieve from another location - No data loss
Q12: How to test scaling without actual infrastructure?
A:
Load Testing:
wrk -t12 -c100000 -d30s \
--script=load_test.lua \
"https://api.locker-system.com/allocate"
Monitor:
- Response time p99 < 100ms
- Error rate < 0.1%
- CPU/Memory saturation
Chaos Testing: 1. Kill location database randomly 2. Add 100ms latency to notifications 3. Partition network for 30 sec 4. Monitor system recovery 5. Verify other locations unaffected
Demo & Running
Quick Demo
#!/usr/bin/env python3
def run_demo():
print("=" * 70)
print("AMAZON LOCKER SYSTEM - INTERACTIVE DEMO")
print("=" * 70)
# Setup
system = LockerSystem()
location = Location("LOC001", "Times Square", "42 Times Square, NYC", "New York")
system.add_location(location)
locker = Locker("LOCK001", location, num_small=3, num_medium=2, num_large=1)
system.add_locker_to_location(location.location_id, locker)
system.add_notifier(EmailNotifier())
system.add_notifier(SMSNotifier())
user1 = User("USER001", "Alice", "alice@email.com", "555-0001")
print("\nβ
Demo 1: System Setup Complete")
print(f" Location: {location.name}")
print(f" Available slots: {locker.total_available()}")
# Store Package
package1 = Package("PKG001", user1.user_id, LockerSize.SMALL)
result = system.store_package(location.location_id, package1, user1)
print("\nβ
Demo 2: Store Package")
print(f" Pickup Code: {result['code']}")
print(f" Slot: {result['slot']}")
# Retrieve Package
system.retrieve_package(location.location_id, package1.package_id, result['code'], user1)
print("\nβ
Demo 3: Retrieve Package - Success!")
print(f" Available slots now: {locker.total_available()}")
if __name__ == "__main__":
run_demo()
Success Criteria
| Criterion | Status |
|---|---|
| Can explain 5 design patterns | β |
| Can draw UML diagram | β |
| Understand concurrency handling | β |
| Know scaling strategies | β |
| Can handle edge cases | β |
| Ready for interview | β |
Ready for your interview? Let's go! π