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.


  • Katas Are Rehearsal, Not Performance
    You don't practice TDD on production code. You practice on katas and bring the muscle memory to production. The gap between knowing TDD and doing TDD is reps.
  • The Test Pyramid Was an Economic Argument
    The test pyramid was not a quality law. It was a cost structure: unit tests were cheap, integration tests were expensive, so you wrote many of the first and few of the second. Agents collapsed the cost of writing tests at every level, and the cheapest test that still tells the truth is the one that pins a seam the agent cannot fake.
  • Agents Amplify Whatever Vocabulary They Find
    Agents do not bring vocabulary to your codebase. They mirror what's already there. Strong vocabulary gets agent contributions that read like the team. Weak vocabulary gets faster mediocrity. The work you did for craft reasons just became leverage.