🎯 LLD 🏠 Home

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

  1. Quick Start (5 min)
  2. System Overview
  3. Requirements & Scope
  4. Architecture & Design Patterns
  5. Core Entities & UML Diagram
  6. Implementation Guide
  7. Interview Q&A
  8. Scaling Q&A
  9. 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:

  1. Read Replicas: Queries → read-only replicas, Writes → primary with row locks
  2. Caching: Available slots with 5-min TTL, 99% hit rate
  3. Async Processing: Queue notifications, return success immediately
  4. Horizontal Scaling: Multiple locations handle independently
  5. 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! 🚀