Rate Limiter

Level: Advanced 60–90 min

Concepts: AlgorithmsMockingBoundariesStateDesign Patterns

Solutions: C# | TypeScript | Python


Implement a rate limiter that restricts the number of requests a client can make within a time window.

Requirements

  1. Create a RateLimiter with a configurable max requests and time window (in seconds)
  2. allowRequest(clientId) — returns true if the request is allowed, false if rate limited
  3. Each client is tracked independently
  4. Use a sliding window — requests older than the window are no longer counted
  5. The limiter should accept an injectable clock/time source for testability

Test Cases

Given a rate limiter configured for 3 requests per 10 seconds:

Time (s)ClientActionResult
0”alice”allowRequesttrue (1/3)
1”alice”allowRequesttrue (2/3)
2”alice”allowRequesttrue (3/3)
3”alice”allowRequestfalse (limit reached)
3”bob”allowRequesttrue (bob is separate, 1/3)
11”alice”allowRequesttrue (first request expired)
11”alice”allowRequesttrue (second request expired)
11”alice”allowRequestfalse (third request still in window)

Bonus

  • Implement token bucket algorithm as an alternative strategy
  • Add remainingRequests(clientId) — returns how many requests the client has left
  • Add retryAfter(clientId) — returns seconds until the next request will be allowed
  • Add burst allowance — permit short bursts above the rate limit with a separate burst quota
  • Support tiered limits — different rate limits for different client tiers (free, pro, enterprise)

Reference Walkthrough

Full C#, TypeScript, and Python implementations live at tddbuddy-reference-katas/rate-limiter. Nineteen scenarios across rule construction, allow-under-limit, reject-at-limit with retryAfter, window-expiry reset, per-key isolation, and an end-to-end worked example — shared across all three languages — with a Limiter aggregate, Rule value type, Decision union (Allowed | Rejected(retryAfter)), Clock collaborator, LimiterBuilder + FixedClock test doubles, and LimiterRuleInvalidException for invalid rule config.

  • C# (.NET 8, xUnit, FluentAssertions) — walkthrough
  • TypeScript (Node 20, Vitest, strict types) — walkthrough
  • Python (3.11, pytest, frozen dataclasses, Protocol clock) — walkthrough

This kata ships in Agent Full-Bake (F3) mode: one commit per language with the full domain design landing together. The walkthroughs read as design rationalewhy Decision is a union, why Rule is a value type, why Clock is injected (same pattern as memory-cache and circuit-breaker).

The reference uses the fixed-window algorithm rather than the sliding window described in the brief above. Fixed window collapses to a two-variable state machine (windowStart, count) per key and is the simplest algorithm to teach — every behavior in the scenario list (allow, reject, reset, retryAfter, isolation) is exercisable against it, and the end-to-end worked example in WorkedExampleTests walks the full cycle for alice and bob at 3 requests per 10 seconds. The brief’s sliding-window variant is a natural next step: swap WindowState for a Deque<DateTime> of request timestamps and prune on entry. The bonus items (token bucket, remainingRequests, burst allowance, tiered limits) are deliberately out of scope — the Rule and Decision types are ready to extend without the reference hiding seams under premature abstraction. See the repo’s Gears section for when middle gear is the right call.