There are two categories of data we need to store
We will ignore analytics collection for now, and focus on the functional requirements, which are creation, editing, and reading of short URLs. As such we would need the following API routes
GET /api/:short_url
Purpose: Meant to get the target URL associated with a short URL. Expects short_url as path argument, and returns a HTTP Temporary Redirect.
Response: 302 target_url
PUT /api/:short_url
Purpose: Meant to update the short URL
Request body args:
Response: 200
PUT /api/shorten
Purpose: Meant to create a short URL
Request body args:
Response: 200, along with short_url
Each record will contain the following
We don't need to aggregate, or model complex relationships in this data, so we can use a read optimized no-sql database such as DynamoDB.
Our system consists of
This provide the user interface for our visitors (people who visit a short url) and users (people who create and manage short urls). the frontend servers utilize the auth server for user authentication, and the application server for creating, editing, and reading short urls. It is essentially a CRUD interface.
The application server provides the REST API for creating, editing, invalidating, and reading short URLs. When creating a short URL, the application server checks the user details to ensure the user can create a short URL. Then it generates a short URL as discussed later, writes it to both the cache as well as the database, and finally returns a suitable response to the frontend server.
When editing a short URL (changing the target URL or invalidating it), the application server again checks user details to make sure the user is authorized to make these changes, and updates the database and cache with the new details.
When resolving a short URL, the application server first checks the cache. If the target URL is found, then it is returned to the frontend, otherwise the application server checks the database, updates the cache with the value that has been found, and returns it as part of the API response. In case a target is not found, then the server returns a 404 response.
We want to come up with a hashing function that when given a URL string, returns a hashed value. We know that 200/seconds over 20 years translates to more than 57 billion URLs that must be shortened. That means our hashed value (consisting of 9 digits, 26 lower case, and 26 upper case letters) needs to be up to 8 digits long (62^8).
A straightforward way to generate a hash is to use a common hash function like MD5, CRC32 etc. However, all of these provide strings longer than 8 letters, which means we end up with wasted space. We can try to minimize this by utilizing only a part of the output hash, but than can lead to hash collisions, which can be resolved by repeatedly checking for collisions and updating our hash. That's slightly inefficient but we can use techniques like bloom filters to speed that up.
Another approach might be to generate unique ids (I suggest emulating Twitter's Snowflake id generation approach) and encode the id using base 62 encoding. with a structured id generation like snowflake along with a random component (
The load balancer helps spread our traffic across multiple frontend server instances (and the API calls over multiple application server instances). This is also where we implement things like rate limiters to protect our system from abuse such as DDOS attacks.
For URL shortening, I have suggested Base 62 encoding a unique id, instead of a more straightforward approach such as CRC32 or MD5. I have done this to avoid repeated hash collisions which would cause repeated rehashing.
I have also suggested using COTS solutions for analytics instead of building in-house systems, as that is a non-functional requirement and there are mature, well established solutions available in the market.
The database layer can often be a source of failures, especially if only one instance is used. I propose using a distributed database system layout along with a suitable consensus approach such as PAXOS in order to promote availability and partition tolerance. With our requirements, we can tolerate eventual consistency, so this is a perfect scenario for such a set-up.
What are some future improvements you would make? How would you mitigate the failure scenario(s) you described above?