Before you start
Rate limiters are Strategy pattern in the wild. Every production system has one; every design interviewer might ask you to design one.
The Strategy signal, unmistakable
Two algorithms (Token Bucket, Fixed Window) that answer the same question ("allow this request?") through different math. That's the Strategy signal, textbook.
Your design.md Patterns section should name Strategy
AND explain why it's clearly right (not just "seems
like a good fit").
Where each algorithm wins
- Token Bucket — smooths burst traffic. A user can save up tokens during quiet periods and burst later, up to the capacity. Good for APIs that expect chatty usage.
- Fixed Window — hard cap per time slice. Simple to understand and monitor. Downside: allows 2× the rate right at window boundaries (the last window's leftover plus the new window's full quota, back to back).
Real systems often use Sliding Window (a Fixed Window variant that averages across the boundary) or Leaky Bucket (Token Bucket's cousin). Both fit the same Strategy shape — add them as new strategies without touching RateLimiter.
The state question
Where does per-user state live?
Option A — inside the strategy:
class TokenBucket:
def __init__(...):
self._buckets = {} # user_id -> (tokens, last_time)
def allow(self, user, now):
# look up + update self._buckets[user]
Option B — RateLimiter owns the state, strategy is pure:
class RateLimiter:
def __init__(strategy):
self._state = {} # user_id -> whatever the strategy needs
def allow(user, now):
self._state[user] = strategy.step(self._state.get(user), now)
...
Option A is simpler. Option B is easier to test (strategies are pure functions). Both are defensible. Pin your call.
The rules
- Bucket never exceeds capacity. Enforced by
min(capacity, current + elapsed * rate)on every refill. - Counter never exceeds max within a window.
Enforced by the
count < maxcheck before incrementing.
Both are one-line enforcements in one place. Name them.
The extension question interviewers love
"How would you add a Sliding Window Log?" (Store timestamps of recent requests; count how many are in the last N seconds. Memory O(requests-in-window), most-accurate.)
If your Strategy interface is clean, this is a new class with zero changes to RateLimiter. That's the OCP payoff.
Real-world callout
- Nginx uses leaky-bucket by default.
- AWS API Gateway uses token-bucket per stage.
- GitHub API uses fixed-window (5000 requests/hour).
- Every SaaS's "requests per minute" tier lives in code like this.