MindForge
MindForge
DashboardHelp & Support
Dashboard
MindForge

Interview Preparation Bootcamp

Course progress

0%
0 of 177 modules complete
Start Here0/1
  • How to Use This Course
Data Structures & Algorithms0/27
  • Arrays and Hashing
  • Two Pointers
  • Sliding Window
  • Sliding Window: Harder Problems
  • Binary Search
  • Stacks
  • Practice Test: Arrays, Pointers, Windows, Search and Stacks
  • Trees: Traversals and Core Patterns
  • Binary Search Trees and Rebuilding a Tree
  • Tries (Prefix Trees)
  • Heaps and Priority Queues
  • Graphs: BFS and DFS
  • Topological Sort and Cycle Detection
  • Union-Find (Disjoint Set Union)
  • Practice Test: Trees, Heaps and Graphs
  • Dynamic Programming Basics
  • Dynamic Programming: Two Dimensions
  • Dynamic Programming: Hard String Matching
  • Backtracking
  • Backtracking: Constraint Satisfaction
  • Greedy Algorithms
  • Interval Problems
  • Bit Manipulation
  • Math and Geometry
  • String Manipulation
  • Pattern Recall and Mock Coding Rounds
  • Practice Test: DP, Backtracking, Greedy and More

System Design

Foundations0/16
  • The System Design Interview Framework
  • Back-of-the-Envelope Estimation
  • Networking and Protocols for Design
  • API Design and Idempotency
  • Load Balancing, Proxies, and Scaling Out
  • Caching and CDNs
  • Choosing a Database: Storage, Indexes and Transactions
  • Replication, Partitioning, and Sharding
  • CAP, PACELC, and Levels of Consistency
  • Messaging, Queues, and Event-Driven Architecture
  • Distributed Transactions and Data Integrity
  • Coordination, Consensus, and Distributed Locks
  • Reliability, Resilience, and Rate Limiting
  • Observability, Security, and Cost
  • System Design Cheat Sheet
  • Practice Test: System Design Foundations
Case Studies0/23
  • Design a URL Shortener
  • Design a Rate Limiter
  • Design a Notification Service
  • Design a Job Queue System
  • Design a Chat App (WhatsApp)
  • Design an Analytics Pipeline
  • Design a Multi-tenant SaaS Architecture
  • Design a Social Media Feed (Twitter/X)
  • Design a Search Autocomplete System
  • Design a File Storage Service (Dropbox)
  • Design Google Docs
  • Design a Code Review System
  • Design Netflix
  • Design YouTube
  • Design Spotify
  • Design Uber/Lyft
  • Design Airbnb
  • Design Ticketmaster (Eventbrite)
  • Design an E-commerce Cart, Checkout, and Catalog
  • Design a Payment System
  • Design a Logging and Monitoring System
  • Mock Design Rounds
  • Practice Test: System Design Case Studies
Low-Level Design0/19
  • The LLD Interview Framework and UML
  • OOP Foundations for Design
  • SOLID Principles
  • Creational Patterns
  • Structural Patterns
  • Behavioral Patterns I — Strategy, Observer, Command, State, Template Method
  • Behavioral Patterns II and Anti-Patterns
  • Concurrency in Low-Level Design
  • Design a Parking Lot
  • Design an Elevator System
  • Design a Movie Ticket Booking System
  • Design an Expense-Sharing App (Splitwise)
  • Design a Vending Machine and an ATM
  • Design a Rate Limiter and a Logging Framework
  • Design a Notification Service and an In-Memory Cache
  • Design Games — Tic-Tac-Toe, Snake and Ladder, Chess
  • Database Design for LLD
  • LLD Cheat Sheet and Recall Drill
  • Practice Test: Low-Level Design

Backend

Python0/5
  • How Python Runs Your Code
  • Functions Deep Dive: Closures, Generators, Context Managers, Scope
  • Classes Deep Dive: Method Types, MRO, Metaclasses
  • Standard Library and Serialization
  • Practice Test: Python
Django0/6
  • Django Request Lifecycle
  • Lab: Django Middleware — Request ID & Response Time
  • Django ORM and Querysets
  • Django's Cache Framework
  • Practice Test: Django
  • Django in Production: Debugging Labs
FastAPI0/5
  • FastAPI, ASGI, and Async
  • FastAPI Dependency Injection
  • FastAPI Background Tasks
  • Lab: FastAPI Async — Concurrent Aggregation
  • Practice Test: FastAPI
Databases (PostgreSQL)0/6
  • Indexing
  • Query Optimization
  • Transactions and Isolation
  • Migrations and Schema Changes
  • Hierarchical Data and Safe Concurrent Updates
  • Practice Test: Databases
Caching, Queues & Distributed Systems0/8
  • Redis Caching
  • Redis Advanced Patterns
  • Celery and Background Workers
  • Kafka and Message Queues
  • Async Pipelines
  • Distributed Systems Basics
  • Distributed Locks
  • Practice Test: Caching, Queues and Distributed Systems
APIs, Security & Deployment0/9
  • REST API Design
  • API Versioning and Error Handling
  • API Security and Authentication
  • WebSockets and Real-Time Systems
  • Docker and Containers
  • File Storage with S3
  • Testing Strategies
  • Mock Backend Round
  • Practice Test: APIs, Security and Deployment
Frontend0/38
  • HTML Document Structure and Semantics
  • CSS Box Model, Flexbox and Grid
  • CSS Architecture and Theming
  • CSS and Rendering Performance
  • Variables, Scope, Hoisting, and Truthiness
  • Functions, this, and Binding
  • Objects and Prototypal Inheritance
  • JavaScript Utility Patterns: Cloning, Caching, and Memory
  • Promises From Scratch
  • The Event Loop and Browser Rendering Pipeline
  • The DOM: Nodes, Events, and Traversal
  • Practice Test: HTML, CSS and JavaScript
  • React Rendering Fundamentals
  • React Hooks Internals
  • React Performance
  • Lab: React — Stateful Counter Component
  • State Management
  • State Machines in React
  • React Custom Hooks and Composition Patterns
  • TypeScript for React
  • React Testing
  • Practice Test: React
  • HTTP and Caching
  • Network Performance and Web Vitals
  • Code Splitting and Bundles
  • Virtualization
  • GraphQL and Data Fetching
  • Web Security
  • WebSockets and Real-time
  • SSR and Next.js
  • Micro-frontends
  • Build Tools and Bundling
  • Accessibility
  • Error Handling
  • Internationalization
  • Animation
  • Rapid Recall and Mock Frontend Rounds
  • Practice Test: Performance, Architecture and Production
Behavioral & Interview Day0/14
  • Resume and Project Walkthroughs
  • The STAR Method and Refining Your Stories
  • Conflict and Difficult Coworkers
  • Failures, Incidents, and Obstacles
  • Leadership, Mentorship, and Ownership
  • Teamwork, Communication, and Feedback
  • Growth and Handling Ambiguity
  • Career Goals, Why This Company, and Negotiation
  • Situational Questions
  • Company Research
  • Mock Behavioral Rounds
  • Interview Day
  • After the Interview
  • Practice Test: Behavioral and Interview Day
Design a Rate Limiter and a Logging Framework
Course contents

Interview Preparation Bootcamp

Course progress

0%
0 of 177 modules complete
Start Here0/1
  • How to Use This Course
Data Structures & Algorithms0/27
  • Arrays and Hashing
  • Two Pointers
  • Sliding Window
  • Sliding Window: Harder Problems
  • Binary Search
  • Stacks
  • Practice Test: Arrays, Pointers, Windows, Search and Stacks
  • Trees: Traversals and Core Patterns
  • Binary Search Trees and Rebuilding a Tree
  • Tries (Prefix Trees)
  • Heaps and Priority Queues
  • Graphs: BFS and DFS
  • Topological Sort and Cycle Detection
  • Union-Find (Disjoint Set Union)
  • Practice Test: Trees, Heaps and Graphs
  • Dynamic Programming Basics
  • Dynamic Programming: Two Dimensions
  • Dynamic Programming: Hard String Matching
  • Backtracking
  • Backtracking: Constraint Satisfaction
  • Greedy Algorithms
  • Interval Problems
  • Bit Manipulation
  • Math and Geometry
  • String Manipulation
  • Pattern Recall and Mock Coding Rounds
  • Practice Test: DP, Backtracking, Greedy and More

System Design

Foundations0/16
  • The System Design Interview Framework
  • Back-of-the-Envelope Estimation
  • Networking and Protocols for Design
  • API Design and Idempotency
  • Load Balancing, Proxies, and Scaling Out
  • Caching and CDNs
  • Choosing a Database: Storage, Indexes and Transactions
  • Replication, Partitioning, and Sharding
  • CAP, PACELC, and Levels of Consistency
  • Messaging, Queues, and Event-Driven Architecture
  • Distributed Transactions and Data Integrity
  • Coordination, Consensus, and Distributed Locks
  • Reliability, Resilience, and Rate Limiting
  • Observability, Security, and Cost
  • System Design Cheat Sheet
  • Practice Test: System Design Foundations
Case Studies0/23
  • Design a URL Shortener
  • Design a Rate Limiter
  • Design a Notification Service
  • Design a Job Queue System
  • Design a Chat App (WhatsApp)
  • Design an Analytics Pipeline
  • Design a Multi-tenant SaaS Architecture
  • Design a Social Media Feed (Twitter/X)
  • Design a Search Autocomplete System
  • Design a File Storage Service (Dropbox)
  • Design Google Docs
  • Design a Code Review System
  • Design Netflix
  • Design YouTube
  • Design Spotify
  • Design Uber/Lyft
  • Design Airbnb
  • Design Ticketmaster (Eventbrite)
  • Design an E-commerce Cart, Checkout, and Catalog
  • Design a Payment System
  • Design a Logging and Monitoring System
  • Mock Design Rounds
  • Practice Test: System Design Case Studies
Low-Level Design0/19
  • The LLD Interview Framework and UML
  • OOP Foundations for Design
  • SOLID Principles
  • Creational Patterns
  • Structural Patterns
  • Behavioral Patterns I — Strategy, Observer, Command, State, Template Method
  • Behavioral Patterns II and Anti-Patterns
  • Concurrency in Low-Level Design
  • Design a Parking Lot
  • Design an Elevator System
  • Design a Movie Ticket Booking System
  • Design an Expense-Sharing App (Splitwise)
  • Design a Vending Machine and an ATM
  • Design a Rate Limiter and a Logging Framework
  • Design a Notification Service and an In-Memory Cache
  • Design Games — Tic-Tac-Toe, Snake and Ladder, Chess
  • Database Design for LLD
  • LLD Cheat Sheet and Recall Drill
  • Practice Test: Low-Level Design

Backend

Python0/5
  • How Python Runs Your Code
  • Functions Deep Dive: Closures, Generators, Context Managers, Scope
  • Classes Deep Dive: Method Types, MRO, Metaclasses
  • Standard Library and Serialization
  • Practice Test: Python
Django0/6
  • Django Request Lifecycle
  • Lab: Django Middleware — Request ID & Response Time
  • Django ORM and Querysets
  • Django's Cache Framework
  • Practice Test: Django
  • Django in Production: Debugging Labs
FastAPI0/5
  • FastAPI, ASGI, and Async
  • FastAPI Dependency Injection
  • FastAPI Background Tasks
  • Lab: FastAPI Async — Concurrent Aggregation
  • Practice Test: FastAPI
Databases (PostgreSQL)0/6
  • Indexing
  • Query Optimization
  • Transactions and Isolation
  • Migrations and Schema Changes
  • Hierarchical Data and Safe Concurrent Updates
  • Practice Test: Databases
Caching, Queues & Distributed Systems0/8
  • Redis Caching
  • Redis Advanced Patterns
  • Celery and Background Workers
  • Kafka and Message Queues
  • Async Pipelines
  • Distributed Systems Basics
  • Distributed Locks
  • Practice Test: Caching, Queues and Distributed Systems
APIs, Security & Deployment0/9
  • REST API Design
  • API Versioning and Error Handling
  • API Security and Authentication
  • WebSockets and Real-Time Systems
  • Docker and Containers
  • File Storage with S3
  • Testing Strategies
  • Mock Backend Round
  • Practice Test: APIs, Security and Deployment
Frontend0/38
  • HTML Document Structure and Semantics
  • CSS Box Model, Flexbox and Grid
  • CSS Architecture and Theming
  • CSS and Rendering Performance
  • Variables, Scope, Hoisting, and Truthiness
  • Functions, this, and Binding
  • Objects and Prototypal Inheritance
  • JavaScript Utility Patterns: Cloning, Caching, and Memory
  • Promises From Scratch
  • The Event Loop and Browser Rendering Pipeline
  • The DOM: Nodes, Events, and Traversal
  • Practice Test: HTML, CSS and JavaScript
  • React Rendering Fundamentals
  • React Hooks Internals
  • React Performance
  • Lab: React — Stateful Counter Component
  • State Management
  • State Machines in React
  • React Custom Hooks and Composition Patterns
  • TypeScript for React
  • React Testing
  • Practice Test: React
  • HTTP and Caching
  • Network Performance and Web Vitals
  • Code Splitting and Bundles
  • Virtualization
  • GraphQL and Data Fetching
  • Web Security
  • WebSockets and Real-time
  • SSR and Next.js
  • Micro-frontends
  • Build Tools and Bundling
  • Accessibility
  • Error Handling
  • Internationalization
  • Animation
  • Rapid Recall and Mock Frontend Rounds
  • Practice Test: Performance, Architecture and Production
Behavioral & Interview Day0/14
  • Resume and Project Walkthroughs
  • The STAR Method and Refining Your Stories
  • Conflict and Difficult Coworkers
  • Failures, Incidents, and Obstacles
  • Leadership, Mentorship, and Ownership
  • Teamwork, Communication, and Feedback
  • Growth and Handling Ambiguity
  • Career Goals, Why This Company, and Negotiation
  • Situational Questions
  • Company Research
  • Mock Behavioral Rounds
  • Interview Day
  • After the Interview
  • Practice Test: Behavioral and Interview Day
Browsing without an account — progress is saved in this browser only.Sign in to save it

Design a Rate Limiter and a Logging Framework

Reading50 min

These are both "design a library, not an app" problems. That's exactly why they're asked: a library has no UI to hide behind, so the interface itself is the design, and how good your abstractions are shows up immediately. Both are also things you genuinely use every day — you've written log statements constantly, and a rate limiter is guarding every API you'll ever build.

Rate limiter — requirements and the interface

What it needs to do:

  • allow(key) -> bool: decide, right now, whether this request can go through.
  • Configurable limits, per key — per user, per API key, per IP — and per resource or endpoint.
  • Support several algorithms: fixed window, sliding window, token bucket.
  • Report how much budget is left, and when it resets, so clients can behave sensibly.
  • Work correctly across multiple servers.

Start with the interface. In a library problem, spend a full minute just on the method signature — most of the actual design lives right there:

class RateLimiter(ABC):
    def try_acquire(self, key: str, now: float, permits: int = 1) -> Decision: ...

@dataclass(frozen=True)
class Decision:
    allowed: bool
    remaining: int
    retry_after: float     # seconds until the next permit; 0 when allowed

Three choices here are worth defending out loud:

  • Return a Decision, not just a plain boolean. The caller needs a Retry-After header and a remaining-budget header — a bare yes/no forces a second call just to get that information.
  • Take now as a parameter, instead of reading the clock internally. Injecting time means every algorithm can be tested with exact, predictable numbers, with no actual waiting involved — a small habit that reads as real experience.
  • Take permits, so one especially expensive request can cost, say, 10 tokens instead of 1. Weighted limits are a very common follow-up question, and this costs nothing to support from the start.

Remember: a rate limiter's return value should carry enough information for the caller to set real HTTP headers, and its clock should be a parameter, not something it reads on its own.

Sign in to try this knowledge check.

Rate limiter — the algorithms, behind one interface

Each algorithm here is a Strategy. The limiter itself is just a thin coordinator sitting over per-key state.

Python
from abc import ABC, abstractmethod
from collections import deque
from dataclasses import dataclass
import threading


@dataclass(frozen=True)
class Decision:
    allowed: bool
    remaining: int
    retry_after: float


class RateLimiter(ABC):
    @abstractmethod
    def try_acquire(self, key: str, now: float, permits: int = 1) -> Decision: ...


class FixedWindowLimiter(RateLimiter):
    """The cheapest option. Flaw: up to 2x the limit can slip through at a window boundary."""
    def __init__(self, limit: int, window_seconds: float):
        self.limit, self.window = limit, window_seconds
        self._counts: dict[tuple[str, int], int] = {}
        self._lock = threading.Lock()

    def try_acquire(self, key, now, permits=1):
        bucket = int(now // self.window)
        with self._lock:
            used = self._counts.get((key, bucket), 0)
            if used + permits > self.limit:
                reset_at = (bucket + 1) * self.window
                return Decision(False, self.limit - used, reset_at - now)
            self._counts[(key, bucket)] = used + permits
            return Decision(True, self.limit - used - permits, 0.0)


class SlidingWindowLogLimiter(RateLimiter):
    """Exact, but memory grows with the request rate — good for low-volume, high-value limits."""
    def __init__(self, limit: int, window_seconds: float):
        self.limit, self.window = limit, window_seconds
        self._hits: dict[str, deque] = {}
        self._lock = threading.Lock()

    def try_acquire(self, key, now, permits=1):
        with self._lock:
            hits = self._hits.setdefault(key, deque())
            while hits and hits[0] <= now - self.window:
                hits.popleft()                       # drop everything outside the window
            if len(hits) + permits > self.limit:
                retry = hits[0] + self.window - now
                return Decision(False, self.limit - len(hits), max(0.0, retry))
            for _ in range(permits):
                hits.append(now)
            return Decision(True, self.limit - len(hits), 0.0)


class TokenBucketLimiter(RateLimiter):
    """The default choice: enforces an average rate while still allowing a short burst."""
    def __init__(self, rate_per_second: float, burst: int):
        self.rate, self.burst = rate_per_second, burst
        self._state: dict[str, tuple[float, float]] = {}   # key -> (tokens, last_refill)
        self._lock = threading.Lock()

    def try_acquire(self, key, now, permits=1):
        with self._lock:
            tokens, last = self._state.get(key, (float(self.burst), now))
            tokens = min(self.burst, tokens + (now - last) * self.rate)   # refill lazily
            if tokens >= permits:
                self._state[key] = (tokens - permits, now)
                return Decision(True, int(tokens - permits), 0.0)
            self._state[key] = (tokens, now)
            return Decision(False, int(tokens), (permits - tokens) / self.rate)


# --- fixed window: the boundary burst is visible right away ------------------
fw = FixedWindowLimiter(limit=2, window_seconds=60)
assert [fw.try_acquire("u1", t).allowed for t in (0, 1, 2)] == [True, True, False]
assert fw.try_acquire("u1", 59.9).allowed is False
assert fw.try_acquire("u1", 60.0).allowed is True          # new window: 2 more, right away
assert fw.try_acquire("u2", 0).allowed is True             # each key is isolated

# --- sliding log: exact, no boundary trick ------------------------------------
sw = SlidingWindowLogLimiter(limit=2, window_seconds=60)
assert [sw.try_acquire("u1", t).allowed for t in (0, 1)] == [True, True]
assert sw.try_acquire("u1", 59).allowed is False
assert sw.try_acquire("u1", 61).allowed is True            # the hit at t=0 has aged out

# --- token bucket: a burst, then a steady rate --------------------------------
tb = TokenBucketLimiter(rate_per_second=1, burst=3)
assert [tb.try_acquire("u1", 0).allowed for _ in range(3)] == [True, True, True]
denied = tb.try_acquire("u1", 0)
assert denied.allowed is False and abs(denied.retry_after - 1.0) < 1e-9
assert tb.try_acquire("u1", 2).allowed is True             # 2 seconds later, 2 tokens refilled
assert tb.try_acquire("u1", 2, permits=5).allowed is False # too expensive a request

print("token bucket retry_after:", round(denied.retry_after, 2), "seconds")

Distributed rate limiting is the natural follow-up. Three answers, in increasing order of how good they are:

  1. Per-node limits — wrong: 10 nodes at 100/minute each means 1000/minute overall, not 100.
  2. One central counter in Redis (INCR plus EXPIRE, or a Lua script for the token bucket so refilling and taking a token happen atomically) — correct, but it adds a network round trip to every single request and makes Redis a hard dependency for your front door.
  3. Local buckets, each holding a share of the global budget, rebalanced now and then against a central counter — approximate, fast, and it gracefully falls back to per-node limits if the central store ever goes down. This is what production API gateways actually do.

Also worth one sentence: rate limit by identity where you can, not by IP address — mobile carriers and corporate networks can put thousands of real users behind one single IP.

Remember: fixed window is cheap but bursty at the boundary. Sliding window is exact but memory-hungry. Token bucket is the default, because it allows a burst without giving up on the average rate.

Sign in to try this knowledge check.

Logging framework — requirements and the two patterns

What it needs to do:

  • Levels: DEBUG, INFO, WARN, ERROR, FATAL, in that order, with a configurable threshold.
  • Several destinations at once (console, a file, the network), each with its own level and format.
  • Structured context — a request id, a user id — attached to every message automatically.
  • Thread-safe, and cheap whenever a message gets filtered out.
  • Configurable while the program is running, without needing a redeploy.

Two patterns carry this whole design, and naming both is really the entire point of the problem:

  • Chain of Responsibility for the level hierarchy: each handler checks whether a record meets its own threshold, acts if it does, and passes it on regardless — this is a broadcast chain rather than a stop-at-first-match one, so say which kind you mean.
  • Strategy for both formatting (plain text, JSON, key-value pairs) and for the destination itself (an Appender/Sink), so a brand-new output is just a new class.

There's a third, quieter decision here that matters more in production than either pattern: the level check has to happen before the message string is even built. log.debug(f"user {expensive()}") runs that f-string, and pays its full cost, even when DEBUG is turned off. The fix is either a lazy signature (log.debug("user %s", user), only formatted if it actually passes the check) or an explicit is_enabled(level) guard before doing any work.

Python
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import IntEnum
import threading


class Level(IntEnum):                      # an IntEnum, because comparing levels is the whole point
    DEBUG = 10
    INFO = 20
    WARN = 30
    ERROR = 40
    FATAL = 50


@dataclass
class LogRecord:
    level: Level
    message: str
    timestamp: float
    context: dict = field(default_factory=dict)


class Formatter(ABC):                      # strategy: how a record becomes text
    @abstractmethod
    def format(self, record: LogRecord) -> str: ...


class PlainFormatter(Formatter):
    def format(self, record):
        ctx = " ".join(f"{k}={v}" for k, v in sorted(record.context.items()))
        return f"[{record.level.name}] {record.message}" + (f" {ctx}" if ctx else "")


class JsonFormatter(Formatter):
    def format(self, record):
        fields = {"level": record.level.name, "msg": record.message, **record.context}
        body = ", ".join(f'"{k}": "{v}"' for k, v in fields.items())
        return "{" + body + "}"


class Appender(ABC):                       # strategy: where the text actually goes
    def __init__(self, min_level: Level, formatter: Formatter):
        self.min_level, self.formatter = min_level, formatter
        self._lock = threading.Lock()

    def handle(self, record: LogRecord) -> None:
        if record.level >= self.min_level:            # each destination has its own threshold
            with self._lock:                          # writes to one sink are serialised
                self.write(self.formatter.format(record))

    @abstractmethod
    def write(self, line: str) -> None: ...


class MemoryAppender(Appender):            # a stand-in for console/file/network
    def __init__(self, min_level, formatter):
        super().__init__(min_level, formatter)
        self.lines: list[str] = []
    def write(self, line): self.lines.append(line)


class Logger:
    def __init__(self, name: str, min_level: Level = Level.DEBUG):
        self.name, self.min_level = name, min_level
        self._appenders: list[Appender] = []
        self._context: dict = {}

    def add_appender(self, appender: Appender) -> "Logger":
        self._appenders.append(appender)
        return self

    def with_context(self, **kwargs) -> "Logger":
        """A child logger carrying request-scoped fields — no global mutable state involved."""
        child = Logger(self.name, self.min_level)
        child._appenders = self._appenders            # shares the same sinks, has its own context
        child._context = {**self._context, **kwargs}
        return child

    def is_enabled(self, level: Level) -> bool:
        return level >= self.min_level

    def log(self, level: Level, template: str, *args, now: float = 0.0) -> None:
        if not self.is_enabled(level):
            return                                     # a cheap exit BEFORE any formatting
        message = template % args if args else template
        record = LogRecord(level, message, now, dict(self._context))
        for appender in self._appenders:
            appender.handle(record)

    def debug(self, t, *a, now=0.0): self.log(Level.DEBUG, t, *a, now=now)
    def info(self, t, *a, now=0.0): self.log(Level.INFO, t, *a, now=now)
    def warn(self, t, *a, now=0.0): self.log(Level.WARN, t, *a, now=now)
    def error(self, t, *a, now=0.0): self.log(Level.ERROR, t, *a, now=now)


console = MemoryAppender(Level.INFO, PlainFormatter())     # for humans: INFO and above
audit = MemoryAppender(Level.ERROR, JsonFormatter())       # for alerting: ERROR only
log = Logger("app", min_level=Level.DEBUG).add_appender(console).add_appender(audit)

request_log = log.with_context(request_id="r-42", user="asha")
request_log.debug("cache lookup %s", "user:1")             # below the console's own threshold
request_log.info("order placed")
request_log.error("payment failed: %s", "timeout")

assert console.lines == [
    "[INFO] order placed request_id=r-42 user=asha",
    "[ERROR] payment failed: timeout request_id=r-42 user=asha",
]
assert len(audit.lines) == 1 and audit.lines[0].startswith('{"level": "ERROR"')
assert log.with_context().is_enabled(Level.DEBUG) is True

quiet = Logger("app", min_level=Level.WARN).add_appender(console)
quiet.info("this line is dropped before any formatting even happens")
assert len(console.lines) == 2                             # unchanged

print("\n".join(console.lines))
print(audit.lines[0])

Remember: the level check has to happen before the message string is built, never after. Otherwise, a disabled log call still pays the cost of an enabled one.

Sign in to try this knowledge check.

Concurrency, performance, and extensions

Rate limiter:

Concern How it's handled
Several try_acquire calls for one key at once A lock per key (or a striped array of locks), never one single global lock
Memory growing from keys nobody uses anymore Evict with an LRU cache or a TTL — an unbounded per-key map is really just a slow memory leak
Correctness across servers A Lua script in Redis makes refilling and taking a token one atomic step; local buckets against a shared global budget when latency matters more than being perfectly exact
Fail open, or fail closed? Decide this ahead of time, and say so out loud. If Redis goes down, do you let all traffic through (available, but unprotected) or block everything (protected, but an outage)? Usually fail-open for ordinary user traffic, fail-closed for anything expensive or dangerous

That last row is exactly the question interviewers use to separate strong candidates from weaker ones — the right answer isn't a fixed value, it's "it depends on what this endpoint actually costs, and here's my default."

Logger:

Concern How it's handled
Several threads writing to one shared sink A lock per appender (as built above), or an asynchronous appender with a bounded queue
A slow sink (the network) blocking the request An asynchronous appender: put the message on a queue, and let a background thread drain it
The queue fills up Drop the oldest entry, drop by level, or block — but as an explicit, configured policy, never as an unbounded queue that just grows forever
Losing logs if the process crashes Flush on shutdown; accept a small, known window of possible loss for async sinks, and say exactly what that window is
Request-scoped context A child logger, or a context-local variable — never a single global mutable dictionary

Extensions for both:

  • A new rate-limiting algorithm → a new RateLimiter class; the gateway itself is unchanged.
  • Per-endpoint limits → a config map from route to limiter; a composite tries each one and denies if any of them denies.
  • A new log destination (Kafka, S3, OpenTelemetry) → a new Appender.
  • Sampling ("only log 1% of DEBUG messages in production") → a filter placed before the appenders in the chain.
  • Redacting sensitive data → a formatter decorator wrapping the real formatter.

Remember: fail-open or fail-closed is a deliberate, per-endpoint decision — never a blanket default applied everywhere.

Sign in to try this knowledge check.

Quick recap

  • In a library problem, the interface is the design. Spend a full minute on the method signature: return a rich result, inject the clock, and allow weighted permits.
  • Rate limiter = a Strategy over per-key state. Fixed window is cheap but bursty at the boundary; sliding log is exact but memory-hungry; token bucket is the default because it enforces an average rate while still letting through a short burst.
  • Distributed limiting is the natural follow-up, and "local buckets against a shared budget" is the production answer — with the fail-open or fail-closed decision stated out loud.
  • Logger = Chain of Responsibility for levels, plus Strategy for the formatter and the appender, plus the one performance detail people forget: check the level before building the message.
  • Bounded, with eviction, everywhere. An unbounded per-key map and an unbounded async log queue are the two memory leaks these two designs quietly invite if you're not careful.
PreviousDesign a Vending Machine and an ATMNextDesign a Notification Service and an In-Memory Cache

Course progress

0%
0 of 177 modules complete

On this page

  • Rate limiter — requirements and the interface
  • Rate limiter — the algorithms, behind one interface
  • Logging framework — requirements and the two patterns
  • Concurrency, performance, and extensions
  • Quick recap