Strategy & Clock

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