We assume that since our system is globally available, it's a medium sized system with around 100,000 daily active users. With each user creating 10 new pastes per day.
Thus, our writes per second becomes 10 writes per second.
Now lets consider a short url gets clicked around 1000 times. Thus, per second our read requests are 10,000 reads per second.
As we can see, our system is gonna be read heavy.
Now, per day 1 million new pastes are created, so in 1 year we have 360million new URLs. Over 10 years we have ~3.6 billion new pastes.
We consider each paste shall generate a short url of 6 characters. Each url is a base 62 encoded string of 6 characters, thats gives us ~56 billion combinations.This is plenty to go for our system for its 10 year lifetime.
Now, for each paste created, we have metadata:
Thats gives us ~145 bytes, including metadata, lets round up each record to 300 bytes to include DB metadata/overhead space.
Over the period of 10 years, we shall be creating 3.6 billion such records. So being very generous, that's a total space of ~2 TB for metadata including any overhead.
For the actual paste, let us assume 10 KB per paste. In the whole life time of the app, there will be 3.6 billion such pastes, so that gives us 36TB of storage.
We shall store the metadata in a DB and the actual pastes in an object storage. For both, the storage estimation is easily manageable.
There will be 3 APIs
/create POST API
/paste/url_key GET API
/delete/url_key DELETE API
In DynamoDB we shall use Global Tables. This guarantees global availability and eventual consistency (sub 1 second, acceptable for our system). Tables shall have the following columns
Whenever a user creates a paste, their request is routes to our write service. Here a unique 6 character string i.e. key is generated, this shall be our url_key. This is a unique key which shall be used to access the created paste.
The paste is then stored into AWS S3 object storage using signed url, in response we get a reference id. If the user has provided us with time to live i.e. expiration time, we shall provide that to S3 as TTL, that way we do not need to cleanup S3 manually. We then store the reference ID, url_key, create_at, user_id, incremental id, and expires_at (if provided by user) into DynamoDB.
If the user has provided us with time to live i.e. expiration time, we shall store it as TTL in DynamoDB for this created record. That way DynamoDB takes care of the deleting the record, again eliminating the need for manual cleanup service.
Once the whole process is done, we shall return the response to the user. We shall not cache the new paste i.e. we shall not use write-through cache, since this arises a danger that unused pastes flood our redis memory even though they aren't accessed. Since, we are storing complete pastes to our redis, this is very important.
For key generation we could potentially think of maintaining a separate service for key generation. But this introduces race conditions, since multiple instances may get the same key, moreover keeping a single service will make it complex to horizontally scale the system in case of traffic spikes. Instead we shall use Snowflake algorithm (like twitter uses) to generate globally unique keys.
We shall generate 64 bit ID, which shall be ID = region_code + machine_id + sequencer + time_stamp. This gives us truly unique id for each instance of each region. We shall then encode this 64bit id to a Base62 encoded string. Since this is encoding and not hashing, it maintains the uniqueness of the key. This generated key shall be our url_key that we use in the write service.
Whenever the short url for the paste is clicked, first our CDN is hit, and the cached pasted is returned. If there is cache miss, the API gateway redirects the request to the read service.
Since our reads are high, we have separated read and write concerns how high availability and low latency. Here we do not hit the DynamoDB, instead we hit the cache and return the complete paste. In case of cache miss, then the read service requests the Dynamo DB for the for the object_reference_id and retrieves the paste from S3.
The whole response is then cached (since we are using cache aside policy) and then returned to the user. We include Cache-control headers, that way CDN caches this paste as well, hence next time this short url is hit, CDN can directly serve the paste.
CDN caching and internal Caching are the most important components of the read flow, since they reduce the DB hits and service hits by 90%.
We have put global index on the url_key, thus when eventually we do need to query the DB, we have fast data retrievals.
Whenever a user manually deletes the pastes, the request is routed to the write service again by the API gateway. The write service queries DynamoDB and gets the object_reference_id. The write service uses this object_reference_id to then delete the record from AWS S3, once done, the write service deletes the record from DynamoDB as well and returns 200 OK to the user.
We do not invalidate any caches since we have set TTL of 5 minutes, so eventually the caches will auto delete the records, thus eliminating the need for operational overhead.
We have put global index on the url_key, thus when eventually we do need to query the DB, we have fast data retrievals thus faster deletions.