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! 🚀