Question Banks
LLD Problems (20 Complete Designs)
20 low-level design problems with full class hierarchies, patterns, Python code, and evaluation criteria
Problems List
- URL Shortener Service
- Rate Limiter
- LRU Cache
- Parking Lot System
- Elevator System
- Pub/Sub Messaging
- Task Scheduler (Cron)
- Snake & Ladder Game
- Online Bookstore
- Splitwise / Expense Tracker
- Hotel Booking System
- Movie Ticket Booking (BookMyShow)
- Logger Framework
- Notification Service
- Payment Gateway
- File System (Linux-like)
- ATM Machine
- Vending Machine (State Pattern)
- Chat Application (LLD)
- API Rate Limiter + API Gateway Design
1. URL Shortener Service
Requirements
- Shorten long URLs to a 7-character alphanumeric code
- Redirect short URL to original with 301/302 response
- Optional custom aliases and expiration dates
- Analytics: track click count, referrer, geo data
- High read throughput (100:1 read-to-write ratio)
Class Diagram
URLShortenerService
├── create_short_url(long_url, custom_alias?, expiry?) -> ShortURL
├── resolve(short_code) -> str
└── get_analytics(short_code) -> Analytics
ShortURL
├── short_code: str
├── long_url: str
├── created_at: datetime
├── expires_at: datetime | None
├── user_id: str | None
└── click_count: int
CodeGenerator (interface)
├── Base62Generator
├── MD5HashGenerator
└── SnowflakeGenerator
URLRepository (interface)
├── save(short_url: ShortURL)
├── find_by_code(code: str) -> ShortURL
└── delete(code: str)
AnalyticsCollector
├── record_click(short_code, metadata)
└── get_stats(short_code) -> Analytics Python Code
import hashlib
import time
import string
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
class CodeGenerator(ABC):
@abstractmethod
def generate(self, long_url: str) -> str:
pass
class Base62Generator(CodeGenerator):
CHARS = string.ascii_letters + string.digits
COUNTER = 0
def generate(self, long_url: str) -> str:
Base62Generator.COUNTER += 1
num = int(hashlib.md5(
f"{long_url}:{Base62Generator.COUNTER}".encode()
).hexdigest()[:12], 16)
code = []
for _ in range(7):
code.append(self.CHARS[num % 62])
num //= 62
return "".join(code)
class ShortURL:
def __init__(self, short_code: str, long_url: str, expires_at=None, user_id=None):
self.short_code = short_code
self.long_url = long_url
self.created_at = datetime.now()
self.expires_at = expires_at
self.user_id = user_id
self.click_count = 0
def is_expired(self) -> bool:
if self.expires_at is None:
return False
return datetime.now() > self.expires_at
class URLShortenerService:
def __init__(self):
self.store = {} # short_code -> ShortURL
self.generator = Base62Generator()
def create_short_url(self, long_url: str, custom_alias=None, ttl_days=None):
code = custom_alias or self.generator.generate(long_url)
if code in self.store:
raise ValueError("Alias already exists")
expires = datetime.now() + timedelta(days=ttl_days) if ttl_days else None
short_url = ShortURL(code, long_url, expires)
self.store[code] = short_url
return short_url
def resolve(self, short_code: str) -> str:
entry = self.store.get(short_code)
if not entry:
raise KeyError("URL not found")
if entry.is_expired():
del self.store[short_code]
raise KeyError("URL expired")
entry.click_count += 1
return entry.long_url
def get_analytics(self, short_code: str) -> dict:
entry = self.store.get(short_code)
if not entry:
raise KeyError("URL not found")
return {
"short_code": entry.short_code,
"clicks": entry.click_count,
"created": entry.created_at.isoformat(),
} Design Patterns
- Strategy: CodeGenerator allows swapping encoding algorithms
- Repository: URLRepository abstracts persistence (Redis, SQL, DynamoDB)
- Factory: Create ShortURL with builder-style optional params
Evaluation Criteria
- How does the candidate handle collisions in code generation?
- Caching strategy for hot URLs (read-heavy workload)
- Database choice justification (NoSQL for fast lookups)
- How to scale: consistent hashing for distributed storage
2. Rate Limiter
Requirements
- Limit requests per user/IP within a configurable time window
- Support Token Bucket and Sliding Window algorithms
- Configurable per-endpoint limits
- Distributed: consistent across multiple server instances
- Return 429 Too Many Requests with Retry-After header
Class Diagram
RateLimiter (interface)
├── allow(client_id: str) -> bool
└── implementations:
├── TokenBucketLimiter
├── SlidingWindowCounter
└── FixedWindowCounter
TokenBucket
├── capacity: int
├── tokens: float
├── refill_rate: float (tokens/sec)
├── last_refill: timestamp
└── consume() -> bool
RateLimitConfig
├── endpoint: str
├── max_requests: int
├── window_seconds: int
└── algorithm: str
RateLimiterMiddleware
├── limiters: Map<endpoint, RateLimiter>
└── handle_request(client_id, endpoint) -> Response Python Code
import time
import threading
from abc import ABC, abstractmethod
from collections import defaultdict
class RateLimiter(ABC):
@abstractmethod
def allow(self, client_id: str) -> bool:
pass
class TokenBucket:
def __init__(self, capacity: int, refill_rate: float):
self.capacity = capacity
self.tokens = capacity
self.refill_rate = refill_rate
self.last_refill = time.time()
self.lock = threading.Lock()
def consume(self) -> bool:
with self.lock:
self._refill()
if self.tokens >= 1:
self.tokens -= 1
return True
return False
def _refill(self):
now = time.time()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
self.last_refill = now
class TokenBucketLimiter(RateLimiter):
def __init__(self, capacity: int, refill_rate: float):
self.buckets = defaultdict(lambda: TokenBucket(capacity, refill_rate))
def allow(self, client_id: str) -> bool:
return self.buckets[client_id].consume()
class SlidingWindowCounter(RateLimiter):
def __init__(self, max_requests: int, window_seconds: int):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests = defaultdict(list)
self.lock = threading.Lock()
def allow(self, client_id: str) -> bool:
with self.lock:
now = time.time()
cutoff = now - self.window_seconds
self.requests[client_id] = [
t for t in self.requests[client_id] if t > cutoff
]
if len(self.requests[client_id]) < self.max_requests:
self.requests[client_id].append(now)
return True
return False
class RateLimiterMiddleware:
def __init__(self, config: dict):
self.limiters = {}
for endpoint, cfg in config.items():
if cfg["algorithm"] == "token_bucket":
self.limiters[endpoint] = TokenBucketLimiter(cfg["capacity"], cfg["refill_rate"])
else:
self.limiters[endpoint] = SlidingWindowCounter(cfg["max_requests"], cfg["window_seconds"])
def handle_request(self, client_id: str, endpoint: str) -> dict:
limiter = self.limiters.get(endpoint)
if limiter and not limiter.allow(client_id):
return {"status": 429, "headers": {"Retry-After": "60"}}
return {"status": 200} Design Patterns
- Strategy: Interchangeable rate limiting algorithms
- Factory: Middleware creates limiter based on config
- Decorator: Wraps request handlers transparently
Evaluation Criteria
- Thread safety for concurrent requests
- Token Bucket vs Sliding Window trade-offs (burst vs smooth)
- Distributed version with Redis (INCR + EXPIRE atomicity)
- Handling clock drift across distributed nodes
3. LRU Cache
Requirements
- O(1) get and put operations
- Fixed capacity with least-recently-used eviction
- Thread-safe for concurrent access
- Optional TTL (time-to-live) per entry
- Eviction callback to notify external systems
Class Diagram
LRUCache
├── capacity: int
├── cache: HashMap<key, Node>
├── head: Node (most recent)
├── tail: Node (least recent)
├── get(key) -> value
├── put(key, value, ttl?) -> void
└── _evict() -> void
Node (Doubly Linked List)
├── key: K
├── value: V
├── prev: Node
├── next: Node
└── expires_at: float | None
ThreadSafeLRUCache
├── wraps LRUCache
└── lock: threading.Lock Python Code
import time
import threading
class Node:
def __init__(self, key=None, value=None):
self.key = key
self.value = value
self.prev = None
self.next = None
self.expires_at = None
class LRUCache:
def __init__(self, capacity: int, on_evict=None):
self.capacity = capacity
self.cache = {}
self.on_evict = on_evict
self.head = Node() # dummy most-recent
self.tail = Node() # dummy least-recent
self.head.next = self.tail
self.tail.prev = self.head
def get(self, key):
if key not in self.cache:
return -1
node = self.cache[key]
if node.expires_at and time.time() > node.expires_at:
self._remove_node(node)
del self.cache[key]
return -1
self._remove_node(node)
self._add_to_front(node)
return node.value
def put(self, key, value, ttl_seconds=None):
if key in self.cache:
self._remove_node(self.cache[key])
node = Node(key, value)
if ttl_seconds:
node.expires_at = time.time() + ttl_seconds
self.cache[key] = node
self._add_to_front(node)
if len(self.cache) > self.capacity:
self._evict()
def _evict(self):
lru_node = self.tail.prev
self._remove_node(lru_node)
del self.cache[lru_node.key]
if self.on_evict:
self.on_evict(lru_node.key, lru_node.value)
def _add_to_front(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def _remove_node(self, node):
node.prev.next = node.next
node.next.prev = node.prev
class ThreadSafeLRUCache:
def __init__(self, capacity: int):
self._cache = LRUCache(capacity)
self._lock = threading.Lock()
def get(self, key):
with self._lock:
return self._cache.get(key)
def put(self, key, value, ttl_seconds=None):
with self._lock:
self._cache.put(key, value, ttl_seconds) Design Patterns
- Proxy: ThreadSafeLRUCache wraps LRUCache adding thread safety
- Observer: on_evict callback for external notification
- Template Method: Subclass to change eviction policy (LFU, FIFO)
Evaluation Criteria
- Understanding DLL + HashMap for O(1) operations
- Correct pointer manipulation (prev/next updates)
- Thread safety: RWLock vs mutex vs lock-free structures
- TTL: lazy expiration on access + background cleaner thread
4. Parking Lot System
Requirements
- Multi-floor parking with slot types: Compact, Regular, Large
- Vehicle types: Motorcycle, Car, Truck
- Issue ticket on entry, calculate fee on exit
- Track available slots per floor in real time
- Pluggable pricing: hourly, flat-rate, surge pricing
Class Diagram
ParkingLot (Singleton)
├── floors: List<ParkingFloor>
├── active_tickets: Map<str, Ticket>
├── pricing: PricingStrategy
├── park_vehicle(vehicle) -> Ticket
└── unpark_vehicle(ticket_id) -> float
ParkingFloor
├── floor_number: int
├── slots: List<ParkingSlot>
└── find_available_slot(slot_type) -> ParkingSlot
ParkingSlot
├── slot_id: str
├── slot_type: SlotType (COMPACT | REGULAR | LARGE)
├── is_occupied: bool
└── vehicle: Vehicle | None
Vehicle
├── license_plate: str
└── vehicle_type: VehicleType
PricingStrategy (interface)
├── HourlyPricing
└── FlatRatePricing Python Code
from abc import ABC, abstractmethod
from enum import Enum
from datetime import datetime
import uuid, math
class VehicleType(Enum):
MOTORCYCLE = 1
CAR = 2
TRUCK = 3
class SlotType(Enum):
COMPACT = 1
REGULAR = 2
LARGE = 3
VEHICLE_SLOT_MAP = {
VehicleType.MOTORCYCLE: SlotType.COMPACT,
VehicleType.CAR: SlotType.REGULAR,
VehicleType.TRUCK: SlotType.LARGE,
}
class Vehicle:
def __init__(self, plate: str, vtype: VehicleType):
self.license_plate = plate
self.vehicle_type = vtype
class ParkingSlot:
def __init__(self, slot_id: str, slot_type: SlotType):
self.slot_id = slot_id
self.slot_type = slot_type
self.is_occupied = False
self.vehicle = None
def park(self, vehicle: Vehicle):
self.vehicle = vehicle
self.is_occupied = True
def unpark(self):
self.vehicle = None
self.is_occupied = False
class PricingStrategy(ABC):
@abstractmethod
def calculate_fee(self, hours: float, vtype: VehicleType) -> float:
pass
class HourlyPricing(PricingStrategy):
RATES = {VehicleType.MOTORCYCLE: 10, VehicleType.CAR: 20, VehicleType.TRUCK: 30}
def calculate_fee(self, hours, vtype):
return math.ceil(hours) * self.RATES[vtype]
class Ticket:
def __init__(self, vehicle: Vehicle, slot: ParkingSlot):
self.ticket_id = str(uuid.uuid4())
self.vehicle = vehicle
self.slot = slot
self.entry_time = datetime.now()
class ParkingFloor:
def __init__(self, floor_num: int, slots: list):
self.floor_number = floor_num
self.slots = slots
def find_available_slot(self, slot_type: SlotType):
for s in self.slots:
if s.slot_type == slot_type and not s.is_occupied:
return s
return None
class ParkingLot:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, '_initialized'):
self.floors = []
self.active_tickets = {}
self.pricing = HourlyPricing()
self._initialized = True
def park_vehicle(self, vehicle: Vehicle) -> Ticket:
slot_type = VEHICLE_SLOT_MAP[vehicle.vehicle_type]
for floor in self.floors:
slot = floor.find_available_slot(slot_type)
if slot:
slot.park(vehicle)
ticket = Ticket(vehicle, slot)
self.active_tickets[ticket.ticket_id] = ticket
return ticket
raise Exception("No available slot")
def unpark_vehicle(self, ticket_id: str) -> float:
ticket = self.active_tickets.pop(ticket_id)
ticket.slot.unpark()
hours = (datetime.now() - ticket.entry_time).total_seconds() / 3600
return self.pricing.calculate_fee(hours, ticket.vehicle.vehicle_type) Design Patterns
- Singleton: ParkingLot single instance for the system
- Strategy: PricingStrategy swappable pricing algorithms
- Factory: VehicleFactory for creating vehicles from input
Evaluation Criteria
- Core entity identification and relationships
- Flexible slot assignment (extensible for EV charging slots)
- Thread safety for concurrent entry/exit
- Extensibility: new vehicle types, new pricing models
5. Elevator System
Requirements
- Multiple elevators in a building with N floors
- External up/down buttons per floor, internal floor-select panel
- Optimal scheduling to minimize average wait time
- States: IDLE, MOVING_UP, MOVING_DOWN, DOOR_OPEN
- Emergency stop and overweight detection
Class Diagram
ElevatorSystem
├── elevators: List<Elevator>
├── scheduler: ElevatorScheduler
└── request_elevator(floor, direction)
Elevator
├── id: int
├── current_floor: int
├── state: ElevatorState
├── up_stops: SortedList
└── down_stops: SortedList
ElevatorScheduler
├── assign(request, elevators) -> Elevator
└── _score(elevator, request) -> int
Request
├── floor: int
├── direction: Direction
└── timestamp: datetime Python Code
from enum import Enum
from sortedcontainers import SortedList
class Direction(Enum):
UP = 1
DOWN = 2
class ElevatorState(Enum):
IDLE = 1
MOVING_UP = 2
MOVING_DOWN = 3
class Request:
def __init__(self, floor: int, direction: Direction):
self.floor = floor
self.direction = direction
class Elevator:
def __init__(self, eid: int):
self.id = eid
self.current_floor = 0
self.state = ElevatorState.IDLE
self.up_stops = SortedList()
self.down_stops = SortedList()
def add_destination(self, floor: int):
if floor > self.current_floor:
self.up_stops.add(floor)
elif floor < self.current_floor:
self.down_stops.add(floor)
def move(self):
if self.state == ElevatorState.MOVING_UP and self.up_stops:
self.current_floor = self.up_stops.pop(0)
elif self.state == ElevatorState.MOVING_DOWN and self.down_stops:
self.current_floor = self.down_stops.pop(-1)
if not self.up_stops and not self.down_stops:
self.state = ElevatorState.IDLE
class ElevatorScheduler:
def assign(self, request: Request, elevators: list) -> Elevator:
best, best_score = None, float('inf')
for e in elevators:
score = self._score(e, request)
if score < best_score:
best_score = score
best = e
return best
def _score(self, elevator: Elevator, request: Request) -> int:
dist = abs(elevator.current_floor - request.floor)
if elevator.state == ElevatorState.IDLE:
return dist
if (elevator.state == ElevatorState.MOVING_UP and
request.direction == Direction.UP and
elevator.current_floor <= request.floor):
return dist
if (elevator.state == ElevatorState.MOVING_DOWN and
request.direction == Direction.DOWN and
elevator.current_floor >= request.floor):
return dist
return dist + 100
class ElevatorSystem:
def __init__(self, num_elevators: int):
self.elevators = [Elevator(i) for i in range(num_elevators)]
self.scheduler = ElevatorScheduler()
def request_elevator(self, floor: int, direction: Direction):
req = Request(floor, direction)
elevator = self.scheduler.assign(req, self.elevators)
elevator.add_destination(floor)
if elevator.state == ElevatorState.IDLE:
elevator.state = (ElevatorState.MOVING_UP if floor > elevator.current_floor
else ElevatorState.MOVING_DOWN) Design Patterns
- State: Elevator behavior governed by current state
- Strategy: Pluggable scheduling algorithms (LOOK, SCAN, zone-based)
- Observer: Floor displays subscribe to position changes
Evaluation Criteria
- Scheduling algorithm handles edge cases (all busy, same floor)
- Correct state transitions
- Extensibility: express elevators, VIP priority, weight limits
- Concurrency: multiple simultaneous requests
6. Pub/Sub Messaging System
Requirements
- Publishers send messages to named topics
- Subscribers register to topics and receive all messages
- Support multiple subscribers per topic
- Message ordering guarantee within a topic
- At-least-once delivery with retry on failure
Class Diagram
MessageBroker
├── topics: Map<str, Topic>
├── create_topic(name) -> Topic
├── publish(topic_name, payload)
└── subscribe(topic_name, subscriber)
Topic
├── name: str
├── subscribers: List<Subscriber>
├── messages: Deque<Message>
└── publish(message)
Message
├── id: str
├── payload: Any
├── timestamp: datetime
└── headers: dict
Subscriber (interface)
├── on_message(message) -> None
├── PrintSubscriber
└── WebhookSubscriber Python Code
import uuid
import threading
from abc import ABC, abstractmethod
from collections import deque
from datetime import datetime
class Message:
def __init__(self, payload, headers=None):
self.id = str(uuid.uuid4())
self.payload = payload
self.headers = headers or {}
self.timestamp = datetime.now()
class Subscriber(ABC):
def __init__(self, sub_id: str):
self.id = sub_id
@abstractmethod
def on_message(self, message: Message):
pass
class PrintSubscriber(Subscriber):
def on_message(self, message: Message):
print(f"[{self.id}] {message.payload}")
class Topic:
def __init__(self, name: str):
self.name = name
self.subscribers = []
self.messages = deque(maxlen=10000)
self.lock = threading.Lock()
def add_subscriber(self, subscriber: Subscriber):
with self.lock:
self.subscribers.append(subscriber)
def publish(self, message: Message):
with self.lock:
self.messages.append(message)
for sub in self.subscribers:
try:
sub.on_message(message)
except Exception as e:
print(f"Delivery failed to {sub.id}: {e}")
class MessageBroker:
def __init__(self):
self.topics = {}
self.lock = threading.Lock()
def create_topic(self, name: str) -> Topic:
with self.lock:
if name not in self.topics:
self.topics[name] = Topic(name)
return self.topics[name]
def publish(self, topic_name: str, payload):
topic = self.topics.get(topic_name)
if not topic:
raise ValueError(f"Topic '{topic_name}' not found")
topic.publish(Message(payload))
def subscribe(self, topic_name: str, subscriber: Subscriber):
topic = self.topics.get(topic_name)
if not topic:
raise ValueError(f"Topic '{topic_name}' not found")
topic.add_subscriber(subscriber) Design Patterns
- Observer: Core pub/sub pattern topic notifies subscribers
- Strategy: Delivery modes (push/pull, sync/async)
- Decorator: Add retry, logging, metrics around delivery
Evaluation Criteria
- Thread safety in publish/subscribe
- Handling slow subscribers (async delivery, backpressure)
- Dead letter queue for persistent failures
- Consumer groups for load-balanced consumption
7. Task Scheduler (Cron)
Requirements
- Schedule tasks to run at specified intervals or cron expressions
- Support one-time and recurring tasks
- Handle task failures with configurable retry policy
- Concurrent execution with thread pool
- Priority-based execution when tasks compete for resources
Class Diagram
TaskScheduler
├── task_queue: PriorityQueue<ScheduledTask>
├── thread_pool: ThreadPoolExecutor
├── schedule(task, trigger) -> str
├── cancel(task_id) -> bool
└── _run_loop()
ScheduledTask
├── task_id: str
├── task: Callable
├── trigger: Trigger
├── priority: int
├── next_run: datetime
├── retry_policy: RetryPolicy
└── status: TaskStatus
Trigger (interface)
├── IntervalTrigger(seconds)
├── CronTrigger(expression)
└── OneTimeTrigger(run_at)
RetryPolicy
├── max_retries: int
├── backoff_seconds: float
└── should_retry(attempt) -> bool Python Code
import heapq
import threading
import time
import uuid
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from concurrent.futures import ThreadPoolExecutor
from enum import Enum
class TaskStatus(Enum):
PENDING = 1
RUNNING = 2
COMPLETED = 3
FAILED = 4
class Trigger(ABC):
@abstractmethod
def next_fire_time(self, after: datetime) -> datetime:
pass
class IntervalTrigger(Trigger):
def __init__(self, seconds: int):
self.interval = timedelta(seconds=seconds)
def next_fire_time(self, after: datetime) -> datetime:
return after + self.interval
class OneTimeTrigger(Trigger):
def __init__(self, run_at: datetime):
self.run_at = run_at
self._fired = False
def next_fire_time(self, after: datetime) -> datetime:
if self._fired:
return None
self._fired = True
return self.run_at
class RetryPolicy:
def __init__(self, max_retries=3, backoff=2.0):
self.max_retries = max_retries
self.backoff = backoff
def should_retry(self, attempt: int) -> bool:
return attempt < self.max_retries
def get_delay(self, attempt: int) -> float:
return self.backoff ** attempt
class ScheduledTask:
def __init__(self, func, trigger: Trigger, priority=0):
self.task_id = str(uuid.uuid4())
self.func = func
self.trigger = trigger
self.priority = priority
self.next_run = trigger.next_fire_time(datetime.now())
self.retry_policy = RetryPolicy()
self.status = TaskStatus.PENDING
def __lt__(self, other):
if self.next_run == other.next_run:
return self.priority > other.priority
return self.next_run < other.next_run
class TaskScheduler:
def __init__(self, max_workers=4):
self.queue = []
self.pool = ThreadPoolExecutor(max_workers=max_workers)
self.lock = threading.Lock()
self.running = False
self.tasks = {}
def schedule(self, func, trigger: Trigger, priority=0) -> str:
task = ScheduledTask(func, trigger, priority)
with self.lock:
heapq.heappush(self.queue, task)
self.tasks[task.task_id] = task
return task.task_id
def cancel(self, task_id: str) -> bool:
with self.lock:
if task_id in self.tasks:
self.tasks[task_id].status = TaskStatus.COMPLETED
return True
return False
def start(self):
self.running = True
threading.Thread(target=self._run_loop, daemon=True).start()
def _run_loop(self):
while self.running:
with self.lock:
if not self.queue:
time.sleep(0.1)
continue
task = self.queue[0]
if task.next_run > datetime.now():
time.sleep(0.1)
continue
heapq.heappop(self.queue)
if task.status == TaskStatus.COMPLETED:
continue
self.pool.submit(self._execute_task, task)
def _execute_task(self, task: ScheduledTask):
task.status = TaskStatus.RUNNING
try:
task.func()
task.status = TaskStatus.COMPLETED
next_time = task.trigger.next_fire_time(datetime.now())
if next_time:
task.next_run = next_time
task.status = TaskStatus.PENDING
with self.lock:
heapq.heappush(self.queue, task)
except Exception:
task.status = TaskStatus.FAILED Design Patterns
- Strategy: Trigger implementations define scheduling behavior
- Command: Tasks encapsulate executable operations
- Observer: Notify on task completion/failure
Evaluation Criteria
- Priority queue for efficient next-task lookup
- Thread pool sizing and task isolation
- Retry with exponential backoff
- Distributed scheduler: leader election, task sharding
8. Snake & Ladder Game
Requirements
- Board with configurable size (default 100 squares)
- Configurable snakes (head > tail) and ladders (bottom < top)
- 2-4 players taking turns rolling a single die
- Player must land exactly on last square to win
- Track game history and moves
Class Diagram
Game
├── board: Board
├── players: List<Player>
├── dice: Dice
├── current_turn: int
├── play_turn() -> MoveResult
└── is_game_over() -> bool
Board
├── size: int
├── snakes: Map<int, int> (head -> tail)
├── ladders: Map<int, int> (bottom -> top)
└── get_final_position(pos) -> int
Player
├── name: str
├── position: int
└── move(steps) -> int
Dice
├── faces: int
└── roll() -> int Python Code
import random
from dataclasses import dataclass
class Dice:
def __init__(self, faces=6):
self.faces = faces
def roll(self) -> int:
return random.randint(1, self.faces)
class Player:
def __init__(self, name: str):
self.name = name
self.position = 0
@dataclass
class MoveResult:
player: str
roll: int
old_pos: int
new_pos: int
hit_snake: bool = False
hit_ladder: bool = False
class Board:
def __init__(self, size=100, snakes=None, ladders=None):
self.size = size
self.snakes = snakes or {} # {head: tail}
self.ladders = ladders or {} # {bottom: top}
self._validate()
def _validate(self):
for head, tail in self.snakes.items():
assert head > tail, "Snake head must be above tail"
for bottom, top in self.ladders.items():
assert bottom < top, "Ladder bottom must be below top"
def get_final_position(self, pos: int) -> tuple:
hit_snake, hit_ladder = False, False
if pos in self.snakes:
pos = self.snakes[pos]
hit_snake = True
elif pos in self.ladders:
pos = self.ladders[pos]
hit_ladder = True
return pos, hit_snake, hit_ladder
class Game:
def __init__(self, players: list, board: Board):
self.board = board
self.players = [Player(name) for name in players]
self.dice = Dice()
self.current_turn = 0
self.winner = None
self.history = []
def play_turn(self) -> MoveResult:
player = self.players[self.current_turn % len(self.players)]
roll = self.dice.roll()
old_pos = player.position
new_pos = old_pos + roll
if new_pos > self.board.size:
# Must land exactly on last square
new_pos = old_pos
else:
new_pos, hit_snake, hit_ladder = self.board.get_final_position(new_pos)
player.position = new_pos
result = MoveResult(player.name, roll, old_pos, new_pos)
self.history.append(result)
if new_pos == self.board.size:
self.winner = player
self.current_turn += 1
return result
def is_game_over(self) -> bool:
return self.winner is not None
def play(self):
while not self.is_game_over():
result = self.play_turn()
print(f"{result.player} rolled {result.roll}: "
f"{result.old_pos} -> {result.new_pos}")
print(f"Winner: {self.winner.name}") Design Patterns
- Strategy: Dice can be swapped (loaded dice for testing)
- Template Method: Game loop structure fixed, steps customizable
- Builder: Board configuration with fluent API
Evaluation Criteria
- Clean separation of Board, Game logic, and Players
- Edge cases: exact landing, multiple snakes/ladders on same path
- Extensibility: power-ups, multiple dice, special squares
- Testability: injectable dice for deterministic tests
9. Online Bookstore
Requirements
- Search books by title, author, ISBN, category
- Shopping cart with add/remove/update quantity
- Inventory management with stock tracking
- Order placement with payment integration
- User accounts with order history
Class Diagram
BookStore
├── catalog: BookCatalog
├── inventory: InventoryManager
├── users: Map<str, User>
└── place_order(user_id, cart) -> Order
Book
├── isbn: str
├── title: str
├── author: str
├── price: float
├── category: Category
└── published_year: int
BookCatalog
├── books: List<Book>
├── search(query, field) -> List<Book>
└── get_by_isbn(isbn) -> Book
Cart
├── user_id: str
├── items: Map<str, CartItem>
├── add_item(book, qty)
├── remove_item(isbn)
└── get_total() -> float
Order
├── order_id: str
├── items: List<OrderItem>
├── total: float
├── status: OrderStatus
└── placed_at: datetime Python Code
import uuid
from enum import Enum
from datetime import datetime
from dataclasses import dataclass, field
class Category(Enum):
FICTION = 1
NON_FICTION = 2
SCIENCE = 3
TECHNOLOGY = 4
class OrderStatus(Enum):
PENDING = 1
CONFIRMED = 2
SHIPPED = 3
DELIVERED = 4
CANCELLED = 5
@dataclass
class Book:
isbn: str
title: str
author: str
price: float
category: Category
@dataclass
class CartItem:
book: Book
quantity: int
class Cart:
def __init__(self, user_id: str):
self.user_id = user_id
self.items: dict = {}
def add_item(self, book: Book, qty: int = 1):
if book.isbn in self.items:
self.items[book.isbn].quantity += qty
else:
self.items[book.isbn] = CartItem(book, qty)
def remove_item(self, isbn: str):
self.items.pop(isbn, None)
def get_total(self) -> float:
return sum(item.book.price * item.quantity for item in self.items.values())
def clear(self):
self.items.clear()
class InventoryManager:
def __init__(self):
self.stock = {} # isbn -> quantity
def add_stock(self, isbn: str, qty: int):
self.stock[isbn] = self.stock.get(isbn, 0) + qty
def check_availability(self, isbn: str, qty: int) -> bool:
return self.stock.get(isbn, 0) >= qty
def reserve(self, isbn: str, qty: int) -> bool:
if self.check_availability(isbn, qty):
self.stock[isbn] -= qty
return True
return False
class Order:
def __init__(self, user_id: str, items: list, total: float):
self.order_id = str(uuid.uuid4())
self.user_id = user_id
self.items = items
self.total = total
self.status = OrderStatus.PENDING
self.placed_at = datetime.now()
class BookStore:
def __init__(self):
self.books = {} # isbn -> Book
self.inventory = InventoryManager()
self.orders = []
def add_book(self, book: Book, stock: int):
self.books[book.isbn] = book
self.inventory.add_stock(book.isbn, stock)
def search(self, query: str) -> list:
query_lower = query.lower()
return [b for b in self.books.values()
if query_lower in b.title.lower() or query_lower in b.author.lower()]
def place_order(self, user_id: str, cart: Cart) -> Order:
for isbn, item in cart.items.items():
if not self.inventory.reserve(isbn, item.quantity):
raise Exception(f"Insufficient stock for {isbn}")
order = Order(user_id, list(cart.items.values()), cart.get_total())
order.status = OrderStatus.CONFIRMED
self.orders.append(order)
cart.clear()
return order Design Patterns
- Repository: BookCatalog, InventoryManager abstract data access
- State: Order status transitions with validation
- Observer: Notify user on order status changes
Evaluation Criteria
- Inventory reservation and race condition handling
- Cart persistence (session vs database)
- Search optimization (indexing, full-text search)
- Order state machine with valid transitions only
10. Splitwise / Expense Tracker
Requirements
- Users create groups and add expenses
- Split types: equal, exact amounts, percentage-based
- Track balances between pairs of users
- Simplify debts (minimize number of transactions)
- Expense history and settlement tracking
Class Diagram
ExpenseService
├── users: Map<str, User>
├── groups: Map<str, Group>
├── add_expense(payer, amount, participants, split_type)
├── get_balance(user_id) -> Map<str, float>
└── simplify_debts(group_id) -> List<Transaction>
Expense
├── expense_id: str
├── payer: User
├── amount: float
├── participants: List<User>
├── split_strategy: SplitStrategy
└── created_at: datetime
SplitStrategy (interface)
├── EqualSplit
├── ExactSplit(amounts: Map)
└── PercentSplit(percentages: Map)
Group
├── group_id: str
├── name: str
├── members: List<User>
└── expenses: List<Expense> Python Code
import uuid
from abc import ABC, abstractmethod
from collections import defaultdict
class SplitStrategy(ABC):
@abstractmethod
def calculate_shares(self, amount: float, participants: list) -> dict:
"""Returns {user_id: amount_owed}"""
pass
class EqualSplit(SplitStrategy):
def calculate_shares(self, amount, participants):
share = round(amount / len(participants), 2)
return {p: share for p in participants}
class ExactSplit(SplitStrategy):
def __init__(self, exact_amounts: dict):
self.exact_amounts = exact_amounts
def calculate_shares(self, amount, participants):
total = sum(self.exact_amounts.values())
assert abs(total - amount) < 0.01, "Exact amounts must sum to total"
return self.exact_amounts
class PercentSplit(SplitStrategy):
def __init__(self, percentages: dict):
self.percentages = percentages
def calculate_shares(self, amount, participants):
assert abs(sum(self.percentages.values()) - 100) < 0.01
return {uid: round(amount * pct / 100, 2)
for uid, pct in self.percentages.items()}
class Expense:
def __init__(self, payer_id: str, amount: float, participants: list, strategy: SplitStrategy):
self.expense_id = str(uuid.uuid4())
self.payer_id = payer_id
self.amount = amount
self.participants = participants
self.strategy = strategy
self.shares = strategy.calculate_shares(amount, participants)
class ExpenseService:
def __init__(self):
self.balances = defaultdict(lambda: defaultdict(float))
self.expenses = []
def add_expense(self, payer_id: str, amount: float, participants: list, strategy: SplitStrategy):
expense = Expense(payer_id, amount, participants, strategy)
self.expenses.append(expense)
for user_id, share in expense.shares.items():
if user_id != payer_id:
self.balances[user_id][payer_id] += share
self.balances[payer_id][user_id] -= share
def get_balance(self, user_id: str) -> dict:
"""Returns net balance with each user (positive = they owe you)."""
return dict(self.balances.get(user_id, {} ))
def simplify_debts(self) -> list:
"""Minimize transactions using net settlement."""
net = defaultdict(float)
for user, debts in self.balances.items():
for other, amount in debts.items():
net[user] += amount
creditors = [(uid, amt) for uid, amt in net.items() if amt < 0]
debtors = [(uid, amt) for uid, amt in net.items() if amt > 0]
creditors.sort(key=lambda x: x[1])
debtors.sort(key=lambda x: x[1], reverse=True)
transactions = []
i, j = 0, 0
while i < len(debtors) and j < len(creditors):
debtor, debt = debtors[i]
creditor, credit = creditors[j]
settle = min(debt, -credit)
transactions.append((debtor, creditor, round(settle, 2)))
debtors[i] = (debtor, debt - settle)
creditors[j] = (creditor, credit + settle)
if debtors[i][1] < 0.01:
i += 1
if creditors[j][1] > -0.01:
j += 1
return transactions Design Patterns
- Strategy: SplitStrategy for different split algorithms
- Observer: Notify users when balance changes
- Repository: Expense storage abstraction
Evaluation Criteria
- Correct balance tracking between user pairs
- Debt simplification algorithm (greedy approach)
- Handling floating-point precision in money calculations
- Concurrency: simultaneous expense additions
11. Hotel Booking System
Requirements
- Search available rooms by date range and room type
- Room types: Standard, Deluxe, Suite with different pricing
- Reservation with check-in/check-out management
- Handle overbooking prevention with concurrency control
- Cancellation with refund policy (full/partial/none by days)
Class Diagram
Hotel
├── rooms: List<Room>
├── reservations: List<Reservation>
├── search_available(check_in, check_out, room_type) -> List<Room>
├── book_room(guest, room, dates) -> Reservation
└── cancel_reservation(reservation_id) -> Refund
Room
├── room_id: str
├── room_type: RoomType
├── floor: int
├── price_per_night: float
└── is_available(check_in, check_out) -> bool
Reservation
├── reservation_id: str
├── guest: Guest
├── room: Room
├── check_in: date
├── check_out: date
├── status: ReservationStatus
└── total_cost: float
Guest
├── guest_id: str
├── name: str
├── email: str
└── phone: str Python Code
import uuid
import threading
from enum import Enum
from datetime import date, timedelta
class RoomType(Enum):
STANDARD = 1
DELUXE = 2
SUITE = 3
class ReservationStatus(Enum):
CONFIRMED = 1
CHECKED_IN = 2
CHECKED_OUT = 3
CANCELLED = 4
class Room:
def __init__(self, room_id: str, room_type: RoomType, price: float):
self.room_id = room_id
self.room_type = room_type
self.price_per_night = price
self.booked_dates = set() # set of dates
def is_available(self, check_in: date, check_out: date) -> bool:
nights = (check_out - check_in).days
for i in range(nights):
if check_in + timedelta(days=i) in self.booked_dates:
return False
return True
def reserve_dates(self, check_in: date, check_out: date):
nights = (check_out - check_in).days
for i in range(nights):
self.booked_dates.add(check_in + timedelta(days=i))
def release_dates(self, check_in: date, check_out: date):
nights = (check_out - check_in).days
for i in range(nights):
self.booked_dates.discard(check_in + timedelta(days=i))
class Guest:
def __init__(self, name: str, email: str):
self.guest_id = str(uuid.uuid4())
self.name = name
self.email = email
class Reservation:
def __init__(self, guest: Guest, room: Room, check_in: date, check_out: date):
self.reservation_id = str(uuid.uuid4())
self.guest = guest
self.room = room
self.check_in = check_in
self.check_out = check_out
self.status = ReservationStatus.CONFIRMED
nights = (check_out - check_in).days
self.total_cost = nights * room.price_per_night
class Hotel:
def __init__(self, name: str):
self.name = name
self.rooms = []
self.reservations = {}
self.lock = threading.Lock()
def add_room(self, room: Room):
self.rooms.append(room)
def search_available(self, check_in: date, check_out: date, room_type: RoomType = None):
results = []
for room in self.rooms:
if room_type and room.room_type != room_type:
continue
if room.is_available(check_in, check_out):
results.append(room)
return results
def book_room(self, guest: Guest, room_id: str, check_in: date, check_out: date) -> Reservation:
with self.lock:
room = next((r for r in self.rooms if r.room_id == room_id), None)
if not room:
raise ValueError("Room not found")
if not room.is_available(check_in, check_out):
raise ValueError("Room not available for these dates")
room.reserve_dates(check_in, check_out)
reservation = Reservation(guest, room, check_in, check_out)
self.reservations[reservation.reservation_id] = reservation
return reservation
def cancel_reservation(self, reservation_id: str) -> float:
with self.lock:
res = self.reservations.get(reservation_id)
if not res:
raise ValueError("Reservation not found")
res.room.release_dates(res.check_in, res.check_out)
res.status = ReservationStatus.CANCELLED
days_until = (res.check_in - date.today()).days
if days_until > 7:
return res.total_cost # full refund
elif days_until > 2:
return res.total_cost * 0.5
return 0.0 Design Patterns
- Repository: Room and Reservation storage abstraction
- Strategy: Cancellation refund policy strategies
- State: Reservation status transitions
Evaluation Criteria
- Concurrency control for double-booking prevention
- Date range overlap detection efficiency
- Refund policy flexibility
- Scaling: how to handle multiple hotels, chains
12. Movie Ticket Booking (BookMyShow)
Requirements
- Browse movies, theaters, and show timings
- Seat selection with real-time availability display
- Temporary seat lock during payment (5-minute hold)
- Payment processing and ticket generation
- Concurrency: prevent double-booking of same seat
Class Diagram
BookingService
├── search_movies(city, date) -> List<Movie>
├── get_shows(movie_id, city) -> List<Show>
├── get_available_seats(show_id) -> List<Seat>
├── hold_seats(show_id, seat_ids, user_id) -> HoldToken
├── confirm_booking(hold_token, payment) -> Ticket
└── release_expired_holds()
Show
├── show_id: str
├── movie: Movie
├── theater: Theater
├── screen: Screen
├── start_time: datetime
└── seats: Map<str, SeatStatus>
Seat
├── seat_id: str
├── row: str
├── number: int
├── category: SeatCategory (REGULAR | PREMIUM | VIP)
└── price: float
SeatStatus (AVAILABLE | HELD | BOOKED)
HoldToken {token, seats, expires_at} Python Code
import uuid
import threading
import time
from enum import Enum
from datetime import datetime, timedelta
class SeatCategory(Enum):
REGULAR = 1
PREMIUM = 2
VIP = 3
class SeatStatus(Enum):
AVAILABLE = 1
HELD = 2
BOOKED = 3
class Seat:
def __init__(self, seat_id: str, row: str, number: int, category: SeatCategory, price: float):
self.seat_id = seat_id
self.row = row
self.number = number
self.category = category
self.price = price
class Show:
def __init__(self, show_id: str, movie_name: str, start_time: datetime):
self.show_id = show_id
self.movie_name = movie_name
self.start_time = start_time
self.seat_status = {} # seat_id -> SeatStatus
self.seat_holds = {} # seat_id -> (user_id, expires_at)
self.lock = threading.Lock()
def initialize_seats(self, seats: list):
for seat in seats:
self.seat_status[seat.seat_id] = SeatStatus.AVAILABLE
def get_available_seats(self) -> list:
return [sid for sid, status in self.seat_status.items()
if status == SeatStatus.AVAILABLE]
class HoldToken:
def __init__(self, user_id: str, seat_ids: list, show_id: str):
self.token = str(uuid.uuid4())
self.user_id = user_id
self.seat_ids = seat_ids
self.show_id = show_id
self.expires_at = datetime.now() + timedelta(minutes=5)
def is_expired(self) -> bool:
return datetime.now() > self.expires_at
class BookingService:
def __init__(self):
self.shows = {} # show_id -> Show
self.holds = {} # token -> HoldToken
self.bookings = []
def hold_seats(self, show_id: str, seat_ids: list, user_id: str) -> HoldToken:
show = self.shows[show_id]
with show.lock:
for sid in seat_ids:
if show.seat_status.get(sid) != SeatStatus.AVAILABLE:
raise ValueError(f"Seat {sid} not available")
for sid in seat_ids:
show.seat_status[sid] = SeatStatus.HELD
show.seat_holds[sid] = (user_id, datetime.now() + timedelta(minutes=5))
hold = HoldToken(user_id, seat_ids, show_id)
self.holds[hold.token] = hold
return hold
def confirm_booking(self, token: str) -> dict:
hold = self.holds.pop(token, None)
if not hold:
raise ValueError("Invalid hold token")
if hold.is_expired():
self._release_hold(hold)
raise ValueError("Hold expired")
show = self.shows[hold.show_id]
with show.lock:
for sid in hold.seat_ids:
show.seat_status[sid] = SeatStatus.BOOKED
booking_id = str(uuid.uuid4())
self.bookings.append({"id": booking_id, "seats": hold.seat_ids})
return {"booking_id": booking_id, "seats": hold.seat_ids}
def _release_hold(self, hold: HoldToken):
show = self.shows[hold.show_id]
with show.lock:
for sid in hold.seat_ids:
if show.seat_status.get(sid) == SeatStatus.HELD:
show.seat_status[sid] = SeatStatus.AVAILABLE Design Patterns
- State: Seat transitions (AVAILABLE → HELD → BOOKED)
- Repository: Show and Booking data access abstraction
- Observer: Notify waitlisted users when seats become available
Evaluation Criteria
- Concurrency: lock granularity (per-show vs per-seat)
- Temporary hold mechanism with expiry
- Race condition prevention for same seat
- Scaling: distributed locks for multi-server deployment
13. Logger Framework
Requirements
- Multiple log levels: DEBUG, INFO, WARN, ERROR, FATAL
- Multiple output sinks: Console, File, Remote (HTTP/Syslog)
- Configurable format: timestamp, level, message, context
- Log rotation by size or time
- Thread-safe logging with minimal performance impact
Class Diagram
Logger (Singleton per name)
├── name: str
├── level: LogLevel
├── handlers: List<LogHandler>
├── formatter: LogFormatter
├── log(level, message, **context)
├── debug/info/warn/error/fatal(message)
└── add_handler(handler)
LogHandler (interface)
├── ConsoleHandler
├── FileHandler(path, max_bytes, backup_count)
└── HttpHandler(endpoint)
LogFormatter (interface)
├── SimpleFormatter: "[LEVEL] message"
├── JSONFormatter: {"ts":..., "level":..., "msg":...}
└── format(record) -> str
LogRecord
├── timestamp: datetime
├── level: LogLevel
├── message: str
├── logger_name: str
└── context: dict Python Code
import threading
from abc import ABC, abstractmethod
from datetime import datetime
from enum import IntEnum
import json
class LogLevel(IntEnum):
DEBUG = 10
INFO = 20
WARN = 30
ERROR = 40
FATAL = 50
class LogRecord:
def __init__(self, level: LogLevel, message: str, logger_name: str, **context):
self.timestamp = datetime.now()
self.level = level
self.message = message
self.logger_name = logger_name
self.context = context
class LogFormatter(ABC):
@abstractmethod
def format(self, record: LogRecord) -> str:
pass
class SimpleFormatter(LogFormatter):
def format(self, record: LogRecord) -> str:
ts = record.timestamp.strftime("%Y-%m-%d %H:%M:%S")
return f"[{ts}] [{record.level.name}] {record.logger_name}: {record.message}"
class JSONFormatter(LogFormatter):
def format(self, record: LogRecord) -> str:
return json.dumps({
"timestamp": record.timestamp.isoformat(),
"level": record.level.name,
"logger": record.logger_name,
"message": record.message,
**record.context
})
class LogHandler(ABC):
def __init__(self, formatter: LogFormatter = None):
self.formatter = formatter or SimpleFormatter()
@abstractmethod
def emit(self, record: LogRecord):
pass
class ConsoleHandler(LogHandler):
def emit(self, record: LogRecord):
print(self.formatter.format(record))
class FileHandler(LogHandler):
def __init__(self, filepath: str, max_bytes=10_000_000, formatter=None):
super().__init__(formatter)
self.filepath = filepath
self.max_bytes = max_bytes
self.lock = threading.Lock()
def emit(self, record: LogRecord):
with self.lock:
with open(self.filepath, "a") as f:
f.write(self.formatter.format(record) + "\n")
class Logger:
_instances = {}
_lock = threading.Lock()
@classmethod
def get_logger(cls, name: str) -> "Logger":
with cls._lock:
if name not in cls._instances:
cls._instances[name] = Logger(name)
return cls._instances[name]
def __init__(self, name: str):
self.name = name
self.level = LogLevel.DEBUG
self.handlers = []
def add_handler(self, handler: LogHandler):
self.handlers.append(handler)
def log(self, level: LogLevel, message: str, **context):
if level < self.level:
return
record = LogRecord(level, message, self.name, **context)
for handler in self.handlers:
handler.emit(record)
def debug(self, msg, **ctx): self.log(LogLevel.DEBUG, msg, **ctx)
def info(self, msg, **ctx): self.log(LogLevel.INFO, msg, **ctx)
def warn(self, msg, **ctx): self.log(LogLevel.WARN, msg, **ctx)
def error(self, msg, **ctx): self.log(LogLevel.ERROR, msg, **ctx)
def fatal(self, msg, **ctx): self.log(LogLevel.FATAL, msg, **ctx) Design Patterns
- Singleton: Logger.get_logger returns shared instance per name
- Strategy: LogFormatter and LogHandler are pluggable
- Chain of Responsibility: Multiple handlers process each record
- Observer: Handlers observe log events
Evaluation Criteria
- Thread safety with minimal contention
- Separation of formatting from output
- Log rotation implementation details
- Async logging for performance (queue-based)
14. Notification Service
Requirements
- Support channels: Email, SMS, Push, In-App
- Template-based notification content
- User preference management (opt-in/out per channel)
- Priority levels: critical (immediate), normal (batched), low (digest)
- Retry failed deliveries with exponential backoff
Class Diagram
NotificationService
├── send(user_id, notification_type, data)
├── channels: Map<ChannelType, NotificationChannel>
├── preferences: UserPreferenceStore
└── template_engine: TemplateEngine
NotificationChannel (interface)
├── EmailChannel
├── SMSChannel
├── PushChannel
└── InAppChannel
└── send(recipient, content) -> DeliveryResult
Notification
├── id: str
├── user_id: str
├── channel: ChannelType
├── content: str
├── priority: Priority
├── status: DeliveryStatus
└── attempts: int
Template
├── template_id: str
├── channel: ChannelType
├── body_template: str
└── render(data) -> str Python Code
import uuid
from abc import ABC, abstractmethod
from enum import Enum
from string import Template
from collections import defaultdict
class ChannelType(Enum):
EMAIL = 1
SMS = 2
PUSH = 3
IN_APP = 4
class Priority(Enum):
CRITICAL = 1
NORMAL = 2
LOW = 3
class DeliveryStatus(Enum):
PENDING = 1
SENT = 2
FAILED = 3
class NotificationChannel(ABC):
@abstractmethod
def send(self, recipient: str, content: str) -> bool:
pass
class EmailChannel(NotificationChannel):
def send(self, recipient: str, content: str) -> bool:
print(f"Email to {recipient}: {content}")
return True
class SMSChannel(NotificationChannel):
def send(self, recipient: str, content: str) -> bool:
print(f"SMS to {recipient}: {content[:160]}")
return True
class PushChannel(NotificationChannel):
def send(self, recipient: str, content: str) -> bool:
print(f"Push to {recipient}: {content}")
return True
class TemplateEngine:
def __init__(self):
self.templates = {}
def register(self, name: str, template_str: str):
self.templates[name] = Template(template_str)
def render(self, name: str, data: dict) -> str:
return self.templates[name].safe_substitute(data)
class UserPreferences:
def __init__(self):
self.prefs = defaultdict(lambda: {ch: True for ch in ChannelType})
def is_enabled(self, user_id: str, channel: ChannelType) -> bool:
return self.prefs[user_id].get(channel, True)
def opt_out(self, user_id: str, channel: ChannelType):
self.prefs[user_id][channel] = False
class Notification:
def __init__(self, user_id: str, channel: ChannelType, content: str, priority: Priority):
self.id = str(uuid.uuid4())
self.user_id = user_id
self.channel = channel
self.content = content
self.priority = priority
self.status = DeliveryStatus.PENDING
self.attempts = 0
class NotificationService:
def __init__(self):
self.channels = {
ChannelType.EMAIL: EmailChannel(),
ChannelType.SMS: SMSChannel(),
ChannelType.PUSH: PushChannel(),
}
self.preferences = UserPreferences()
self.templates = TemplateEngine()
self.queue = []
def send(self, user_id: str, template_name: str, data: dict,
channels: list = None, priority: Priority = Priority.NORMAL):
channels = channels or [ChannelType.EMAIL, ChannelType.PUSH]
content = self.templates.render(template_name, data)
for ch_type in channels:
if not self.preferences.is_enabled(user_id, ch_type):
continue
notification = Notification(user_id, ch_type, content, priority)
self._deliver(notification)
def _deliver(self, notification: Notification, max_retries=3):
channel = self.channels.get(notification.channel)
if not channel:
return
for attempt in range(max_retries):
notification.attempts = attempt + 1
if channel.send(notification.user_id, notification.content):
notification.status = DeliveryStatus.SENT
return
notification.status = DeliveryStatus.FAILED Design Patterns
- Strategy: NotificationChannel implementations per channel type
- Template Method: Template rendering with variable substitution
- Observer: Users subscribe/unsubscribe to notification types
- Decorator: Retry logic wraps channel delivery
Evaluation Criteria
- Clean separation of channel, template, preferences
- Retry mechanism with exponential backoff
- Handling user preferences and opt-out
- Async delivery for non-critical notifications
15. Payment Gateway
Requirements
- Process payments via multiple providers (Stripe, PayPal, UPI)
- Support payment methods: credit card, debit card, wallet, UPI
- Transaction lifecycle: initiate → authorize → capture → settle
- Refund processing (full and partial)
- Idempotency: prevent duplicate charges on retry
Class Diagram
PaymentGateway
├── process_payment(order, method, amount) -> Transaction
├── refund(transaction_id, amount?) -> RefundResult
├── get_transaction(tx_id) -> Transaction
└── providers: Map<str, PaymentProvider>
PaymentProvider (interface)
├── StripeProvider
├── PayPalProvider
└── UPIProvider
├── authorize(amount, method) -> AuthResult
├── capture(auth_id, amount) -> CaptureResult
└── refund(tx_id, amount) -> RefundResult
Transaction
├── transaction_id: str
├── idempotency_key: str
├── amount: float
├── currency: str
├── status: TransactionStatus
├── provider: str
└── created_at: datetime
TransactionStatus: INITIATED | AUTHORIZED | CAPTURED | SETTLED | REFUNDED | FAILED Python Code
import uuid
import threading
from abc import ABC, abstractmethod
from enum import Enum
from datetime import datetime
class TransactionStatus(Enum):
INITIATED = 1
AUTHORIZED = 2
CAPTURED = 3
SETTLED = 4
REFUNDED = 5
FAILED = 6
class PaymentMethod(Enum):
CREDIT_CARD = 1
DEBIT_CARD = 2
UPI = 3
WALLET = 4
class Transaction:
def __init__(self, amount: float, currency: str, idempotency_key: str):
self.transaction_id = str(uuid.uuid4())
self.idempotency_key = idempotency_key
self.amount = amount
self.currency = currency
self.status = TransactionStatus.INITIATED
self.provider = None
self.created_at = datetime.now()
class PaymentProvider(ABC):
@abstractmethod
def authorize(self, amount: float, method_details: dict) -> dict:
pass
@abstractmethod
def capture(self, auth_id: str, amount: float) -> dict:
pass
@abstractmethod
def refund(self, transaction_id: str, amount: float) -> dict:
pass
class StripeProvider(PaymentProvider):
def authorize(self, amount, method_details):
# Simulate Stripe API call
return {"auth_id": str(uuid.uuid4()), "success": True}
def capture(self, auth_id, amount):
return {"capture_id": str(uuid.uuid4()), "success": True}
def refund(self, transaction_id, amount):
return {"refund_id": str(uuid.uuid4()), "success": True}
class PaymentGateway:
def __init__(self):
self.providers = {"stripe": StripeProvider()}
self.transactions = {}
self.idempotency_store = {} # key -> transaction_id
self.lock = threading.Lock()
def process_payment(self, amount: float, currency: str, method: PaymentMethod,
method_details: dict, provider_name: str,
idempotency_key: str) -> Transaction:
with self.lock:
# Idempotency check
if idempotency_key in self.idempotency_store:
existing_tx_id = self.idempotency_store[idempotency_key]
return self.transactions[existing_tx_id]
tx = Transaction(amount, currency, idempotency_key)
provider = self.providers[provider_name]
tx.provider = provider_name
# Authorize
auth_result = provider.authorize(amount, method_details)
if not auth_result["success"]:
tx.status = TransactionStatus.FAILED
self.transactions[tx.transaction_id] = tx
return tx
tx.status = TransactionStatus.AUTHORIZED
# Capture
capture_result = provider.capture(auth_result["auth_id"], amount)
if capture_result["success"]:
tx.status = TransactionStatus.CAPTURED
else:
tx.status = TransactionStatus.FAILED
self.transactions[tx.transaction_id] = tx
self.idempotency_store[idempotency_key] = tx.transaction_id
return tx
def refund(self, transaction_id: str, amount: float = None) -> dict:
tx = self.transactions.get(transaction_id)
if not tx or tx.status != TransactionStatus.CAPTURED:
raise ValueError("Invalid transaction for refund")
refund_amount = amount or tx.amount
provider = self.providers[tx.provider]
result = provider.refund(transaction_id, refund_amount)
if result["success"]:
tx.status = TransactionStatus.REFUNDED
return result Design Patterns
- Strategy: PaymentProvider implementations for each gateway
- State: Transaction status machine with valid transitions
- Factory: Select provider based on payment method/region
- Idempotency Key: Prevent duplicate processing on retries
Evaluation Criteria
- Idempotency handling for network retries
- Transaction state machine correctness
- Provider abstraction for easy addition of new gateways
- Error handling: partial captures, timeout scenarios
16. File System (Linux-like)
Requirements
- Hierarchical directory structure with files and folders
- Operations: create, read, write, delete, move, list
- Path resolution (absolute and relative)
- Permissions model (read/write/execute for owner/group/others)
- Support file metadata (size, created, modified timestamps)
Class Diagram
FileSystem
├── root: Directory
├── create_file(path, content) -> File
├── create_directory(path) -> Directory
├── read_file(path) -> str
├── write_file(path, content)
├── delete(path)
├── list_directory(path) -> List<FSEntry>
└── move(src, dst)
FSEntry (abstract)
├── name: str
├── parent: Directory
├── created_at: datetime
├── modified_at: datetime
├── permissions: Permissions
├── subclasses: File, Directory
File extends FSEntry
├── content: str
├── size: int
└── read() / write(content)
Directory extends FSEntry
├── children: Map<str, FSEntry>
├── add_child(entry)
├── remove_child(name)
└── get_child(name) -> FSEntry Python Code
from abc import ABC
from datetime import datetime
class FSEntry(ABC):
def __init__(self, name: str, parent=None):
self.name = name
self.parent = parent
self.created_at = datetime.now()
self.modified_at = datetime.now()
def get_path(self) -> str:
if self.parent is None:
return "/"
parent_path = self.parent.get_path()
if parent_path == "/":
return f"/{self.name}"
return f"{parent_path}/{self.name}"
class File(FSEntry):
def __init__(self, name: str, parent=None, content=""):
super().__init__(name, parent)
self.content = content
@property
def size(self) -> int:
return len(self.content)
def read(self) -> str:
return self.content
def write(self, content: str):
self.content = content
self.modified_at = datetime.now()
class Directory(FSEntry):
def __init__(self, name: str, parent=None):
super().__init__(name, parent)
self.children = {}
def add_child(self, entry: FSEntry):
if entry.name in self.children:
raise FileExistsError(f"'{entry.name}' already exists")
entry.parent = self
self.children[entry.name] = entry
def remove_child(self, name: str):
if name not in self.children:
raise FileNotFoundError(f"'{name}' not found")
del self.children[name]
def get_child(self, name: str) -> FSEntry:
if name not in self.children:
raise FileNotFoundError(f"'{name}' not found")
return self.children[name]
def list_children(self) -> list:
return list(self.children.keys())
class FileSystem:
def __init__(self):
self.root = Directory("")
def _resolve_path(self, path: str) -> FSEntry:
if path == "/":
return self.root
parts = [p for p in path.strip("/").split("/") if p]
current = self.root
for part in parts:
if not isinstance(current, Directory):
raise NotADirectoryError(f"'{current.name}' is not a directory")
current = current.get_child(part)
return current
def _resolve_parent(self, path: str):
parts = [p for p in path.strip("/").split("/") if p]
name = parts[-1]
parent_path = "/" + "/".join(parts[:-1]) if len(parts) > 1 else "/"
parent = self._resolve_path(parent_path)
if not isinstance(parent, Directory):
raise NotADirectoryError("Parent is not a directory")
return parent, name
def create_file(self, path: str, content: str = "") -> File:
parent, name = self._resolve_parent(path)
f = File(name, parent, content)
parent.add_child(f)
return f
def create_directory(self, path: str) -> Directory:
parent, name = self._resolve_parent(path)
d = Directory(name, parent)
parent.add_child(d)
return d
def read_file(self, path: str) -> str:
entry = self._resolve_path(path)
if not isinstance(entry, File):
raise IsADirectoryError("Cannot read a directory")
return entry.read()
def write_file(self, path: str, content: str):
entry = self._resolve_path(path)
if not isinstance(entry, File):
raise IsADirectoryError("Cannot write to a directory")
entry.write(content)
def delete(self, path: str):
parent, name = self._resolve_parent(path)
parent.remove_child(name)
def list_directory(self, path: str) -> list:
entry = self._resolve_path(path)
if not isinstance(entry, Directory):
raise NotADirectoryError("Not a directory")
return entry.list_children() Design Patterns
- Composite: File and Directory share FSEntry interface; Directory contains children
- Iterator: Traversal of directory tree
- Template Method: Path resolution reused across operations
Evaluation Criteria
- Composite pattern for file/directory hierarchy
- Path resolution correctness (edge cases: trailing slashes, relative paths)
- Permissions model design
- Handling symbolic links, hard links as extensions
17. ATM Machine
Requirements
- Authenticate user with card + PIN
- Operations: check balance, withdraw, deposit, transfer
- Dispense cash using available denominations (greedy algorithm)
- Transaction limits per day
- Handle hardware states: card reader, cash dispenser, receipt printer
Class Diagram
ATM
├── card_reader: CardReader
├── cash_dispenser: CashDispenser
├── state: ATMState
├── authenticate(card, pin) -> Account
├── withdraw(amount) -> CashBundle
├── deposit(amount)
├── check_balance() -> float
└── transfer(to_account, amount)
ATMState (State Pattern)
├── IdleState
├── CardInsertedState
├── AuthenticatedState
└── TransactionState
CashDispenser
├── denominations: Map<int, int> (value -> count)
├── dispense(amount) -> Map<int, int>
└── can_dispense(amount) -> bool
Account
├── account_id: str
├── pin_hash: str
├── balance: float
├── daily_withdrawn: float
└── daily_limit: float Python Code
from abc import ABC, abstractmethod
from enum import Enum
class ATMStateType(Enum):
IDLE = 1
CARD_INSERTED = 2
AUTHENTICATED = 3
TRANSACTION = 4
class Account:
def __init__(self, account_id: str, pin: str, balance: float, daily_limit=50000):
self.account_id = account_id
self.pin = pin
self.balance = balance
self.daily_limit = daily_limit
self.daily_withdrawn = 0.0
def can_withdraw(self, amount: float) -> bool:
return (self.balance >= amount and
self.daily_withdrawn + amount <= self.daily_limit)
def debit(self, amount: float):
self.balance -= amount
self.daily_withdrawn += amount
def credit(self, amount: float):
self.balance += amount
class CashDispenser:
def __init__(self, denominations: dict):
# {2000: 10, 500: 20, 200: 50, 100: 100}
self.denominations = denominations
def can_dispense(self, amount: int) -> bool:
remaining = amount
for denom in sorted(self.denominations.keys(), reverse=True):
count = min(remaining // denom, self.denominations[denom])
remaining -= count * denom
return remaining == 0
def dispense(self, amount: int) -> dict:
if not self.can_dispense(amount):
raise ValueError("Cannot dispense exact amount")
result = {}
remaining = amount
for denom in sorted(self.denominations.keys(), reverse=True):
count = min(remaining // denom, self.denominations[denom])
if count > 0:
result[denom] = count
self.denominations[denom] -= count
remaining -= count * denom
return result
class ATMState(ABC):
@abstractmethod
def insert_card(self, atm, card_number: str): pass
@abstractmethod
def enter_pin(self, atm, pin: str): pass
@abstractmethod
def withdraw(self, atm, amount: float): pass
@abstractmethod
def eject_card(self, atm): pass
class IdleState(ATMState):
def insert_card(self, atm, card_number):
atm.current_card = card_number
atm.set_state(CardInsertedState())
def enter_pin(self, atm, pin):
raise Exception("Insert card first")
def withdraw(self, atm, amount):
raise Exception("Insert card first")
def eject_card(self, atm):
raise Exception("No card inserted")
class CardInsertedState(ATMState):
def insert_card(self, atm, card_number):
raise Exception("Card already inserted")
def enter_pin(self, atm, pin):
account = atm.accounts.get(atm.current_card)
if account and account.pin == pin:
atm.current_account = account
atm.set_state(AuthenticatedState())
else:
atm.set_state(IdleState())
raise Exception("Invalid PIN")
def withdraw(self, atm, amount):
raise Exception("Enter PIN first")
def eject_card(self, atm):
atm.current_card = None
atm.set_state(IdleState())
class AuthenticatedState(ATMState):
def insert_card(self, atm, card_number):
raise Exception("Card already inserted")
def enter_pin(self, atm, pin):
raise Exception("Already authenticated")
def withdraw(self, atm, amount):
account = atm.current_account
if not account.can_withdraw(amount):
raise Exception("Insufficient funds or daily limit exceeded")
cash = atm.dispenser.dispense(int(amount))
account.debit(amount)
return cash
def eject_card(self, atm):
atm.current_card = None
atm.current_account = None
atm.set_state(IdleState())
class ATM:
def __init__(self, denominations: dict):
self.dispenser = CashDispenser(denominations)
self.state = IdleState()
self.accounts = {} # card_number -> Account
self.current_card = None
self.current_account = None
def set_state(self, state: ATMState):
self.state = state
def insert_card(self, card_number: str):
self.state.insert_card(self, card_number)
def enter_pin(self, pin: str):
self.state.enter_pin(self, pin)
def withdraw(self, amount: float):
return self.state.withdraw(self, amount)
def eject_card(self):
self.state.eject_card(self) Design Patterns
- State: ATM behavior changes with current state (core pattern)
- Strategy: Cash dispensing algorithm (greedy, can swap to DP)
- Command: Each transaction type as a command object
Evaluation Criteria
- State pattern implementation with valid transitions
- Cash dispensing greedy algorithm correctness
- Handling edge cases: insufficient denominations, daily limits
- Thread safety for concurrent ATM access
18. Vending Machine (State Pattern)
Requirements
- Accept coins/notes and track inserted amount
- Display product catalog with prices and stock
- Dispense product when sufficient payment received
- Return change using available denominations
- Handle out-of-stock and insufficient payment gracefully
Class Diagram
VendingMachine
├── inventory: Map<str, Product>
├── state: VendingState
├── inserted_amount: float
├── insert_money(amount)
├── select_product(code) -> Product
├── dispense() -> Product
└── cancel() -> float (refund)
VendingState (interface)
├── IdleState
├── HasMoneyState
├── DispensingState
└── each defines: insert_money, select, dispense, cancel
Product
├── code: str
├── name: str
├── price: float
└── quantity: int Python Code
from abc import ABC, abstractmethod
class Product:
def __init__(self, code: str, name: str, price: float, quantity: int):
self.code = code
self.name = name
self.price = price
self.quantity = quantity
class VendingState(ABC):
@abstractmethod
def insert_money(self, machine, amount: float): pass
@abstractmethod
def select_product(self, machine, code: str): pass
@abstractmethod
def dispense(self, machine): pass
@abstractmethod
def cancel(self, machine) -> float: pass
class IdleState(VendingState):
def insert_money(self, machine, amount):
machine.inserted_amount += amount
machine.set_state(HasMoneyState())
print(f"Inserted: {amount}. Total: {machine.inserted_amount}")
def select_product(self, machine, code):
raise Exception("Insert money first")
def dispense(self, machine):
raise Exception("Insert money first")
def cancel(self, machine):
print("Nothing to cancel")
return 0.0
class HasMoneyState(VendingState):
def insert_money(self, machine, amount):
machine.inserted_amount += amount
print(f"Total inserted: {machine.inserted_amount}")
def select_product(self, machine, code):
product = machine.inventory.get(code)
if not product:
raise Exception("Invalid product code")
if product.quantity <= 0:
raise Exception(f"{product.name} is out of stock")
if machine.inserted_amount < product.price:
shortfall = product.price - machine.inserted_amount
raise Exception(f"Insert {shortfall} more")
machine.selected_product = product
machine.set_state(DispensingState())
def dispense(self, machine):
raise Exception("Select a product first")
def cancel(self, machine):
refund = machine.inserted_amount
machine.inserted_amount = 0
machine.set_state(IdleState())
print(f"Refunded: {refund}")
return refund
class DispensingState(VendingState):
def insert_money(self, machine, amount):
raise Exception("Please wait, dispensing...")
def select_product(self, machine, code):
raise Exception("Already dispensing")
def dispense(self, machine):
product = machine.selected_product
product.quantity -= 1
change = machine.inserted_amount - product.price
machine.inserted_amount = 0
machine.selected_product = None
machine.set_state(IdleState())
print(f"Dispensed: {product.name}. Change: {change}")
return product, change
def cancel(self, machine):
raise Exception("Cannot cancel during dispensing")
class VendingMachine:
def __init__(self):
self.inventory = {}
self.state = IdleState()
self.inserted_amount = 0.0
self.selected_product = None
def set_state(self, state: VendingState):
self.state = state
def add_product(self, product: Product):
self.inventory[product.code] = product
def insert_money(self, amount: float):
self.state.insert_money(self, amount)
def select_product(self, code: str):
self.state.select_product(self, code)
def dispense(self):
return self.state.dispense(self)
def cancel(self) -> float:
return self.state.cancel(self) Design Patterns
- State: Core pattern machine behavior changes with state
- Strategy: Change-making algorithm (can be greedy or DP)
- Singleton: Machine instance per physical unit
Evaluation Criteria
- State pattern with correct transitions and guards
- Handling all edge cases per state (invalid actions)
- Change calculation with available denominations
- Concurrency: multiple users (physical constraint makes this simpler)
19. Chat Application (LLD)
Requirements
- One-to-one and group messaging
- Message types: text, image, file attachment
- Online/offline status and last-seen tracking
- Message delivery status: sent, delivered, read
- Message history with pagination
Class Diagram
ChatService
├── users: Map<str, User>
├── conversations: Map<str, Conversation>
├── send_message(sender_id, conv_id, content) -> Message
├── create_group(creator, name, members) -> Conversation
├── get_messages(conv_id, page, size) -> List<Message>
└── mark_as_read(user_id, conv_id, message_id)
Conversation
├── conversation_id: str
├── type: ConversationType (DIRECT | GROUP)
├── participants: List<str>
├── messages: List<Message>
└── last_message: Message
Message
├── message_id: str
├── sender_id: str
├── content: MessageContent
├── timestamp: datetime
├── status: Map<user_id, DeliveryStatus>
MessageContent (interface)
├── TextContent(text: str)
├── ImageContent(url: str, thumbnail: str)
└── FileContent(url: str, filename: str, size: int)
User
├── user_id: str
├── name: str
├── status: OnlineStatus
└── last_seen: datetime Python Code
import uuid
from abc import ABC
from enum import Enum
from datetime import datetime
from collections import defaultdict
class OnlineStatus(Enum):
ONLINE = 1
OFFLINE = 2
AWAY = 3
class DeliveryStatus(Enum):
SENT = 1
DELIVERED = 2
READ = 3
class ConversationType(Enum):
DIRECT = 1
GROUP = 2
class MessageContent(ABC):
pass
class TextContent(MessageContent):
def __init__(self, text: str):
self.text = text
class ImageContent(MessageContent):
def __init__(self, url: str, thumbnail: str = None):
self.url = url
self.thumbnail = thumbnail
class User:
def __init__(self, user_id: str, name: str):
self.user_id = user_id
self.name = name
self.status = OnlineStatus.OFFLINE
self.last_seen = datetime.now()
class Message:
def __init__(self, sender_id: str, content: MessageContent):
self.message_id = str(uuid.uuid4())
self.sender_id = sender_id
self.content = content
self.timestamp = datetime.now()
self.delivery_status = {} # user_id -> DeliveryStatus
class Conversation:
def __init__(self, conv_type: ConversationType, participants: list):
self.conversation_id = str(uuid.uuid4())
self.type = conv_type
self.participants = participants
self.messages = []
self.last_message = None
def add_message(self, message: Message):
self.messages.append(message)
self.last_message = message
for pid in self.participants:
if pid != message.sender_id:
message.delivery_status[pid] = DeliveryStatus.SENT
class ChatService:
def __init__(self):
self.users = {}
self.conversations = {}
self.user_conversations = defaultdict(list) # user_id -> [conv_ids]
def register_user(self, user_id: str, name: str) -> User:
user = User(user_id, name)
self.users[user_id] = user
return user
def create_direct_conversation(self, user1_id: str, user2_id: str) -> Conversation:
conv = Conversation(ConversationType.DIRECT, [user1_id, user2_id])
self.conversations[conv.conversation_id] = conv
self.user_conversations[user1_id].append(conv.conversation_id)
self.user_conversations[user2_id].append(conv.conversation_id)
return conv
def create_group(self, creator_id: str, name: str, member_ids: list) -> Conversation:
participants = [creator_id] + member_ids
conv = Conversation(ConversationType.GROUP, participants)
self.conversations[conv.conversation_id] = conv
for uid in participants:
self.user_conversations[uid].append(conv.conversation_id)
return conv
def send_message(self, sender_id: str, conv_id: str, content: MessageContent) -> Message:
conv = self.conversations.get(conv_id)
if not conv:
raise ValueError("Conversation not found")
if sender_id not in conv.participants:
raise PermissionError("Not a participant")
message = Message(sender_id, content)
conv.add_message(message)
return message
def mark_as_read(self, user_id: str, conv_id: str, message_id: str):
conv = self.conversations.get(conv_id)
for msg in conv.messages:
if msg.message_id == message_id:
msg.delivery_status[user_id] = DeliveryStatus.READ
break
def get_messages(self, conv_id: str, page: int = 1, size: int = 20) -> list:
conv = self.conversations.get(conv_id)
if not conv:
return []
start = max(0, len(conv.messages) - page * size)
end = len(conv.messages) - (page - 1) * size
return conv.messages[start:end]
def set_user_online(self, user_id: str):
self.users[user_id].status = OnlineStatus.ONLINE
def set_user_offline(self, user_id: str):
user = self.users[user_id]
user.status = OnlineStatus.OFFLINE
user.last_seen = datetime.now() Design Patterns
- Observer: Real-time message delivery to online users
- Strategy: MessageContent subtypes for different media
- Repository: Conversation and Message persistence
- Mediator: ChatService coordinates between users and conversations
Evaluation Criteria
- Message delivery guarantees (at-least-once for offline users)
- Efficient message pagination (cursor-based vs offset)
- Online status propagation to contacts
- Group message fan-out optimization
20. API Rate Limiter + API Gateway Design
Requirements
- Unified gateway for routing, authentication, and rate limiting
- Per-client API keys with configurable quotas
- Route-based throttling (different limits per endpoint)
- Request/response transformation and logging
- Circuit breaker for downstream service failures
Class Diagram
APIGateway
├── router: Router
├── auth: AuthMiddleware
├── rate_limiter: RateLimiterMiddleware
├── circuit_breaker: CircuitBreaker
├── handle_request(request) -> Response
└── register_route(path, backend, config)
Router
├── routes: Map<str, RouteConfig>
└── resolve(path) -> BackendService
CircuitBreaker
├── state: CircuitState (CLOSED | OPEN | HALF_OPEN)
├── failure_count: int
├── threshold: int
├── timeout: int
├── call(func) -> result
└── record_result(success: bool)
APIKey
├── key: str
├── client_id: str
├── tier: Tier (FREE | PRO | ENTERPRISE)
├── rate_limit: int (requests/minute)
└── is_active: bool
Middleware (Chain of Responsibility)
├── AuthMiddleware -> RateLimitMiddleware -> LoggingMiddleware -> Router Python Code
import time
import threading
from enum import Enum
from abc import ABC, abstractmethod
from collections import defaultdict
class CircuitState(Enum):
CLOSED = 1
OPEN = 2
HALF_OPEN = 3
class Tier(Enum):
FREE = 1 # 100 req/min
PRO = 2 # 1000 req/min
ENTERPRISE = 3 # 10000 req/min
TIER_LIMITS = {Tier.FREE: 100, Tier.PRO: 1000, Tier.ENTERPRISE: 10000}
class APIKey:
def __init__(self, key: str, client_id: str, tier: Tier):
self.key = key
self.client_id = client_id
self.tier = tier
self.rate_limit = TIER_LIMITS[tier]
self.is_active = True
class CircuitBreaker:
def __init__(self, threshold=5, timeout=30):
self.state = CircuitState.CLOSED
self.failure_count = 0
self.threshold = threshold
self.timeout = timeout
self.last_failure_time = 0
self.lock = threading.Lock()
def can_execute(self) -> bool:
with self.lock:
if self.state == CircuitState.CLOSED:
return True
if self.state == CircuitState.OPEN:
if time.time() - self.last_failure_time > self.timeout:
self.state = CircuitState.HALF_OPEN
return True
return False
return True # HALF_OPEN allows one request
def record_success(self):
with self.lock:
self.failure_count = 0
self.state = CircuitState.CLOSED
def record_failure(self):
with self.lock:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.threshold:
self.state = CircuitState.OPEN
class RateLimiterPerKey:
def __init__(self):
self.windows = defaultdict(list) # api_key -> [timestamps]
self.lock = threading.Lock()
def allow(self, api_key: APIKey) -> bool:
with self.lock:
now = time.time()
window_start = now - 60 # 1-minute window
key = api_key.key
self.windows[key] = [t for t in self.windows[key] if t > window_start]
if len(self.windows[key]) < api_key.rate_limit:
self.windows[key].append(now)
return True
return False
class Middleware(ABC):
def __init__(self, next_handler=None):
self.next = next_handler
@abstractmethod
def handle(self, request: dict) -> dict:
pass
class AuthMiddleware(Middleware):
def __init__(self, api_keys: dict, next_handler=None):
super().__init__(next_handler)
self.api_keys = api_keys
def handle(self, request: dict) -> dict:
key = request.get("api_key")
if not key or key not in self.api_keys:
return {"status": 401, "body": "Unauthorized"}
api_key = self.api_keys[key]
if not api_key.is_active:
return {"status": 403, "body": "API key disabled"}
request["_api_key_obj"] = api_key
return self.next.handle(request) if self.next else request
class RateLimitMiddleware(Middleware):
def __init__(self, limiter: RateLimiterPerKey, next_handler=None):
super().__init__(next_handler)
self.limiter = limiter
def handle(self, request: dict) -> dict:
api_key = request.get("_api_key_obj")
if api_key and not self.limiter.allow(api_key):
return {"status": 429, "body": "Rate limit exceeded",
"headers": {"Retry-After": "60"}}
return self.next.handle(request) if self.next else request
class APIGateway:
def __init__(self):
self.api_keys = {}
self.limiter = RateLimiterPerKey()
self.circuit_breakers = defaultdict(CircuitBreaker)
self.routes = {} # path -> backend_url
self._build_chain()
def _build_chain(self):
self.rate_mw = RateLimitMiddleware(self.limiter)
self.auth_mw = AuthMiddleware(self.api_keys, self.rate_mw)
def register_key(self, api_key: APIKey):
self.api_keys[api_key.key] = api_key
self._build_chain()
def register_route(self, path: str, backend: str):
self.routes[path] = backend
def handle_request(self, request: dict) -> dict:
result = self.auth_mw.handle(request)
if result.get("status") in (401, 403, 429):
return result
# Route to backend with circuit breaker
path = request.get("path", "/")
cb = self.circuit_breakers[path]
if not cb.can_execute():
return {"status": 503, "body": "Service unavailable (circuit open)"}
try:
response = {"status": 200, "body": f"Routed to {self.routes.get(path, 'default')}"}
cb.record_success()
return response
except Exception:
cb.record_failure()
return {"status": 502, "body": "Backend error"} Design Patterns
- Chain of Responsibility: Middleware pipeline (Auth → Rate Limit → Route)
- Circuit Breaker: Protect against cascading downstream failures
- Strategy: Different rate limiting per API tier
- Proxy: Gateway acts as reverse proxy to backend services
Evaluation Criteria
- Middleware chain design and extensibility
- Circuit breaker state transitions (CLOSED → OPEN → HALF_OPEN)
- Per-client tiered rate limiting
- Distributed deployment: sticky sessions, shared state via Redis
- Observability: request logging, metrics, tracing