REUSABLE COMPONENT DESIGN / OBJECT DESIGN BRIEF

Rate limiter design

A rate limiter component decides whether a request from a client is allowed right now, based on a policy such as 100 requests per minute.

IntermediatePhase 08 / Topic 2 of 7ResponsibilitiesCollaborationsExtensibility
01

Overview

A rate limiter component decides whether a request from a client is allowed right now, based on a policy such as 100 requests per minute. The common algorithms are token bucket (tokens refill at a steady rate, allowing bursts up to capacity), leaky bucket (constant outflow), fixed window counters, and sliding window log or counter.

An LLD answer should define a clean interface (tryAcquire(clientId)), make algorithms pluggable (Strategy), support per-client and per-endpoint rules, use an injectable clock for tests, and be thread-safe. For distributed use, the state moves to Redis with atomic scripts, but the in-process design is the same.

Arcade tokens

You receive one token per second, up to 10 in your pocket. Each game costs a token. You can play 10 quick games in a burst, but then only as fast as tokens arrive.

02

When to use it

  • Interview prompt: 'Design a rate limiter class'.
  • Protecting APIs, logins, and expensive operations.
  • Client-side throttling of calls to third-party APIs.
03

Where it shows up in interviews

Throttling component

Recognize it when: limit N actions per time window per key.

  • Design a rate limiter
  • Logger rate limiter (LeetCode 359)
  • Design hit counter (LeetCode 362)
04

Where it is used in real software

Guava RateLimiter and Bucket4j

Token bucket implementations for Java services.

Nginx limit_req

Leaky bucket style limiting at the proxy.

API platforms

GitHub, Stripe, and OpenAI enforce per-key limits and return 429 with retry headers.

05

Key terms

Token bucket
Capacity plus refill rate; allows bursts.
Sliding window log
Timestamps of recent requests; exact but memory heavy.
Fixed window
Counter per time window; bursts at edges.
Key
What is limited: user, API key, IP, endpoint.
Retry-After
When the client may try again.
06

How it works, step by step

  1. 1
    Define the interface

    tryAcquire(key): boolean, plus retryAfter.

  2. 2
    Pick the algorithm

    Token bucket is a common default.

  3. 3
    Store state per key

    ConcurrentHashMap of buckets.

  4. 4
    Refill lazily on access

    tokens += elapsed x rate, capped.

  5. 5
    Make it thread-safe and testable

    Synchronize per bucket, inject a clock.

07

Token bucket trace

Capacity 3, refill 1 token/second

Step 1 / 5
Time (s)RequestTokens beforeAllowed?Tokens after
0.0r13Yes2
0.1r22Yes1
0.2r31Yes0
0.3r40No (retry in 0.7 s)0
1.3r51 (refilled)Yes0

NOWTime (s): 0.0 | Request: r1 | Tokens before: 3 | Allowed?: Yes | Tokens after: 2

Bursts up to capacity are allowed; sustained rate is bounded by the refill rate.

08

Implementation

import java.time.Clock;import java.util.concurrent.ConcurrentHashMap; public interface RateLimiter { boolean tryAcquire(String key); } public final class TokenBucketLimiter implements RateLimiter {    private static final class Bucket {        double tokens;        long lastRefillNanos;        Bucket(double tokens, long now) { this.tokens = tokens; this.lastRefillNanos = now; }    }     private final int capacity;    private final double refillPerSecond;    private final Clock clock;    private final ConcurrentHashMap<String, Bucket> buckets = new ConcurrentHashMap<>();     public TokenBucketLimiter(int capacity, double refillPerSecond, Clock clock) {        this.capacity = capacity;        this.refillPerSecond = refillPerSecond;        this.clock = clock;    }     @Override    public boolean tryAcquire(String key) {        long now = clock.millis() * 1_000_000L;        Bucket b = buckets.computeIfAbsent(key, k -> new Bucket(capacity, now));        synchronized (b) {                                    // per-key lock            double elapsed = (now - b.lastRefillNanos) / 1e9;            b.tokens = Math.min(capacity, b.tokens + elapsed * refillPerSecond);            b.lastRefillNanos = now;            if (b.tokens >= 1) { b.tokens -= 1; return true; }            return false;        }    }}
09

Complexity and performance

Token bucketO(1) time, O(1) per key

Two numbers per key.

Sliding logO(limit) per key

Stores timestamps.

10

Trade-offs

Accuracy vs memory

Sliding logs are exact but store every timestamp; token buckets and sliding counters approximate with constant memory.

Local vs distributed

In-process limiters are fast but per instance; global limits need shared state (Redis) or division of quota.

11

Variants and related techniques

Sliding window counter

Weighted average of current and previous window counts.

Leaky bucket queue

Smooths output to a constant rate.

12

Common mistakes

  • Fixed windows allowing 2x bursts at boundaries.

    Fix: Use sliding windows or token buckets.

  • Unbounded map of keys.

    Fix: Expire idle buckets.

  • Using System.currentTimeMillis directly.

    Fix: Inject a clock for deterministic tests.

13

Interview questions

Token bucket vs sliding window?

Token bucket allows controlled bursts and uses O(1) memory per key; sliding window log is precise over the window but stores timestamps. Sliding window counters approximate with O(1) memory.

How would you make it thread-safe?

Store buckets in a concurrent map and synchronize the refill-and-consume step per bucket, or use atomic compare-and-set on packed state.

14

Practice problems

ProblemDifficultyWhat it trains
Logger rate limiterEasyPer-message window.
Token bucket with per-endpoint rulesMediumStrategy + config.
Thread-safe sliding window counterHardConcurrency.