Define the APIs expected from the system. This is your chance to analyze and define the read and write paths so that you can come up with the high-level design...
checkRateLimit(apiKey, endpoint) --> Response(status - success/failure, waitTime[optional] - time it needs to wait if user is throttled)
getLimit(apiKey, endpoint) --> Response(maxRequests, minutes)
upgradePlan(apiKey, endpoint, payment, tier[maxRequests, minutes]) -> Response(status=success/fail)
cancelPlan(apiKey, endpoint) -> Response(status=success/fail)
Describe the overall system architecture. Identify the main components needed to solve the problem end-to-end. Use the diagramming tool to create a block diagram.
Deep dive into 2-3 key components. Explain how they work, how they scale, discuss tradeoffs, capacity, and any relevant algorithms or data structures.
b. Cache key is api key + endpoint. Values are number of tokens + last refill time.
Cons: what if we receive two requests from the same API key+ endpoint as cache key at same millisecond, different machines? Use a locking mechanism, where one acquires a lock to write, but this is very slow. Redis has support for atomic operations. We move all logic into one atomic step inside Redis and it guarantees no race conditions and that no other request can interrupt this logic.
what if machines have different clocks? Servers can differ in time. Let's use Redis time as the single source of truth of time.
Let's say we have a billion of DAU, how do we handle high QPS? We can have a local cache in the load balancer to remove overload Redis. Let's say that user has 100 requests/min. We let the load balancer handle 50 requests instead. As soon as the load balancer handle the 50 requests, we let know the redis cache (we're leasing a portion of the max tokens in a local cache). That way we make sure that max tokens never gets in overflow state. In conclusion, small local buckets absorbs bursts, backed by Redis for global enforcement.
Another way to handle high QPS is to partition distributed cache by pair (apikey and endpoint) to distribute load. We can use consistent hashing for whenever a machine gets added/removed. Even if we partition the cache, we may get huge number of requests for a particular pair (apikey and endpoint). We can make an exception for those type of keys and partition even further.