Let's start simple, we will need two API endpoints:
At a glance we will have an API gateway which directs the requests to our application servers. It is obvious that our database is going to be the bottleneck in our application, so we will use a cache to reduce the read burden on our database. Since our data is static, i.e. no edits, we will also keep a CDN cache. A cron job periodically removes expired pastes from our database.
To scale our system horizontally, we will deploy multiple application servers, and the API gateway will give use load balancing and rate limiting capabilities to smooth out traffic and ban abusers.
The database will be the backbone of our application. While we can use a simple relational database for the application, they are not suitable for storing long blobs of text. So we will use two databases, one for metadata storage and another for the actual paste text. For the metadata store we will use Postgres and for the object store we will use S3. For durability and retention SLAs we will keep a replicas of our metadata store, S3 guarantees durability and retention internally.
For incoming read requests, the application servers query the metadata, fetch the associated paste text from the object store and return it. For big pastes, ones >50 MB in size, we will chunk them and stream the chunks. On the write side we will take the paste store the metadata and then upload the text to S3. For big pastes, again >50 MB text, we will send a pre-signed URL to the client so that they can upload directly to S3, reducing load on the servers.
Our CRON job runs every 10 minutes, and even though the cache entry will expire due to TTL, the paste can still sit in the database and be served through a cache miss. Before returning any response the application servers will check if the paste is active, if not the servers will return a 410 expired status code.
A better design here would be microservices, with a service for read and a service for write. This way we can scale reads and writes independently and according to traffic. The core logic per service remains the same.
On thing I overlooked was pasteId creation. Some approaches are:
Since the hashing technique may lead to collisions, let us go with uuid.
Deletion path: when a user makes a delete request first we will check if this is the user that created the post using user_id. If it is the user then we will mark the corresponding paste as expired in the metadata store and update our cache and CDN. The CRON job can then delete the row and paste when it runs next.
We will use a Redis cache to store requested pastes. On cache miss, we query the metadata store and save only the metadata in Redis. On subsequent requests we read from Redis, get the paste from S3 and return. Since our data is static we can also use a CDN to cache full pastes at edge, this will improve global latency.
As our data is ephemeral, we need to take care to invalidate cached pastes. For this at cache entry time the read service will calculate remaining live time for the paste using expires at and current time and set TTL to this. This way we will not serve expired pastes.
On availability and durability front, cache and CDN continue to serve during database or microservice outage.
We will run a cron job every 10 minutes to clean up any expired pastes from both the metadata store and the object store. Caches don't need to be cleaned here as we have set the TTL appropriately.