What the interviewer just asked
“What are the interfaces? What’s the entry point?”
Define contracts before implementations. Bad interfaces force rewrites mid-coding.
RateLimiterStrategy — the algorithm contract
public interface RateLimiterStrategy {
/** Consume one request slot for this client. */
RateLimitResult tryAcquire(String clientId);
/** Read quota without consuming (for X-RateLimit-* headers). */
RateLimitResult peekStatus(String clientId);
/** Wipe state for a client (account reset / tests). */
void reset(String clientId);
}
One strategy instance manages all clients for a given config, keyed by clientId. Implementations must be thread-safe.
ClockProvider — testability before algorithms
@FunctionalInterface
public interface ClockProvider {
long currentTimeMs();
static ClockProvider system() {
return System::currentTimeMillis;
}
}
Inject System::currentTimeMillis in production and a fake clock in tests. Every time-dependent algorithm becomes deterministic — no Thread.sleep.
RateLimiterFactory
public final class RateLimiterFactory {
private RateLimiterFactory() {}
public static RateLimiterStrategy create(RateLimiterConfig config) {
return create(config, ClockProvider.system());
}
public static RateLimiterStrategy create(RateLimiterConfig config, ClockProvider clock) {
return switch (config.getType()) {
case TOKEN_BUCKET -> new TokenBucketRateLimiter(config, clock);
case FIXED_WINDOW -> new FixedWindowRateLimiter(config, clock);
case SLIDING_WINDOW_LOG -> new SlidingWindowLogRateLimiter(config, clock);
case SLIDING_WINDOW_COUNTER -> new SlidingWindowCounterRateLimiter(config, clock);
case LEAKY_BUCKET -> new LeakyBucketRateLimiter(config, clock);
};
}
}
Adding an algorithm = new class + one switch case (Open/Closed).
RateLimiterManager — API surface
class RateLimiterManager {
RateLimitResult hit(String api, String clientId);
void hitOrThrow(String api, String clientId);
RateLimitResult peekQuota(String api, String clientId);
void registerApi(String api, RateLimiterConfig config);
void registerClientOverride(String api, String clientId, RateLimiterConfig config);
}
Priority (first match wins): client override → API config → global default.
Board sketch
Caller → RateLimiterManager.hit(api, client)
│
├─ resolve Strategy (override > api > default)
└─ strategy.tryAcquire(clientId) → RateLimitResult