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 each API gateway instance to remove load in Redis. Let's say that user has 100 requests/min. Each gateway instance will lease tokens from redis cache. Formula would be (total tokens / num of servers) = number of leased tokens in the gateway. This means that the gateway can handle spikes of requests instead of hitting redis directly. Assume we have 4 instances for the api gateway. That means that each gateway can process 25 requests. If we get 25 requests in one go (burst of traffic), it is not 25 calls to redis. It would be only one, the syncing phase.
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.