My Solution for Designing a Simple URL Shortening Service: A TinyURL Approach with Score: 7/10

by expanse_journey108

System requirements


Functional:

A user should be able to shorten links

A user should be able to signup, login and log out

A user should be able to manage stored links, e.g delete or modify them

A user should be able to share shortened links, e.g copy to clipboard or share on social media

A user should be able to set a expiration time for the link


Non-Functional:

Low latency

Scalable

Deployed in multi region


Out of scope:

Custom branding for links



Capacity estimation

Average filesize: 0,5 MB (This is high)

Number of request / day: 20 000 (avg)

Number of request / week: 140 000 (avg)

Link expiration, in days: 7 (avg)

Total filesize added each month: ~9.7 GB


Let's make it easy and say that files have an average expiration of 7 days and then gets deleted.






API design

/link

  • POST - Create a new shortlink
    • Path params: long_url, expiration_time (optional, defaults to one week), userId (optional)

/links/$userId

  • GET - Get a users shortened URLs
    • Path params: userId
    • Authentication: JWT
  • DELETE - Delete a user shortened URLs
    • Path params: userId, short_url
    • Authentication: JWT
  • PUT - Regenerate a shortened URL
    • Path params: userId, short_url, expiration_time (optional, default to one week)
    • Authentication: JWT




Database design

AWS DynamoDB (NoSQL) will be used.

Table 1 - User:

  • pk: {email}#uuid
  • sk: N/A
  • username: string
  • created_at: string / Date
  • last_login: string / Date
  • Password will be stored in Cognito, so no need to store password unencrypted in DynamoDB

Table 2 - Links:

  • pk: uuid#{short_url}
    • If the user is logged in, use the users uuid as the prefix in the partition key. Otherwise, generate a new uuid
  • creation_time: string
  • expiration_time: string
  • long_url: string
  • expired: boolean
  • GSI on long_url for easier storage of long_url



High-level design

Used CDN is Cloudfront, which act as the client in this scenario.


The user make request to the API Gateway (API Endpoints outlined under "API Design).


API GW calls a lambda, with the purpose of doing the actual transformation from a long link to a shortlink. Another lambda is called which adds metadata, expiration date and userdata and then stores the data in DynamoDB.


Authentication is handled by Cognito, which on login calls a Lambda that sets last_login in the User table in the database. Alternative, this could also be handled by setting the last_login to when a user creates a new shortlink.





Request flows

Signup

  1. User creates an account
  2. Request to Cognito
  3. Calls Lambda
  4. Stores userdata in DynamoDB table "Users"

Login

  1. User input auth credentials
  2. Cognito handshake, calls lambda
  3. Store lastlogin in DynamnDB


Link creation

  1. User input long link
  2. API GW request
  3. Calls lambda that to transformation
  4. Store link in Link DynamoDB
  5. Lambda respond with shortlink
  6. User gets shortlink





Detailed component design

API scales automatically

Lambda scales automatically

DynamoDB scale good, especially with GSI on long link.



Trade offs/Tech choices

  • Single region deployment, it's hard to motivate a double in cost
  • No load balancers
  • No decoupling (SQS)
  • No throttling



Failure scenarios/bottlenecks

  • AWS Region goes down, the service goes down. A solution can be to deploy the infrastructure in a separate region (as active-active) and use a load balancer with a latency based policy to distribute traffic to the closest region for the user.
  • Except AWS API GW built-in throttling, no throttling is implemented
  • No use of a que system. The app can be decoupled with AWS SQS


Future improvements

  • Implement multi-region
  • Decouple using que-based handling (AWS SQS)
  • Load balancers
  • Throttling