client service is a RESTful service. We expose 2 API
To cope big number of client requests, we have a load balancer which will evenly distribute the requests across client service instances.
To protect the system from malicious attacks (like DDoS) and control the number of received requests we have a Rate Limiter.
Since we expect that the number of read operations will be higher that the number of write operations, these operations will be divided into 2 different services - Read service and Write Service. This approach gives us possibility to scale each service independently.
The client will talk to client service (or one of it replicas), which will identify the type of operation and send a request to the relevant server.
The read service talks to cache service to speed up the operation.
The cache service reads the data from the database when needed.
To ensure Fault tolerance we have a multiple instances of each services. Each group of instances (write, read and client) have Auto Scaling system which guarantee that we always have the needed number of available instances and copes with spikes of user activity by providing more instances.
Read operation when requested url is not in the cache:
Check if cache contains this short url -> cache service does not contain this short url -> cache service reads the long url from the DB by short url -> cache service saves the obtained long url in the cache like the key-pair (short url -> long url) -> cache service returns the long url
Read operation when requested url is in the cache:
Check if cache contains this short url -> cache service not contains this short url -> cache service gets the long url from the cache by short url -> cache service returns the long url
Read operation when requested url is removed:
Check if cache contains this short url -> cache service not contains this short url -> cache service gets the long url from the cache by short url -> cache service returns the long url -> read service tries to redirect the user to long url -> read services catches the error -> read service sends a request to cache service to invalidate the removed url -> read service notify the user that such short url has been removed.
Cache will be implemented as LRU structure. As for TTL:
Since during delete operation we will remove the required value from the cache and considering the fact that we can update the short url, the TTL value can be 24 hours, which will significantly reduce the read operation time.
In case if cache does not contain the requested short url or if ttl is expired, it goes to the database to obtain the long url and it saves the url pair (short url - long url) in the cache.
we will use the following approach - Unique Short URLs for Each Request
To guarantee uniqueness, each write service will generate the unique key (long url + timestamp + key id) and use cryptographic function to encode it. The encoded value can be used as path for short url.
The key id - is the main value of the key. To guarantee that key id is unique across all of the instances we can use kafka partition ID (if the write service gets the incoming data from the kafka partition) or the name of the service.
Write service will generate a unique short url for each long url and it sends a write request to the database.
With this approach we allow to store multiple short urls which refer to one long url.
Since the generation of the short url is encapsulated in the write service and each instance can do it independently, we can evenly distribute the write request across all instances.
The delete operation is simply removes the required url from the DB