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! πŸš€