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
- User creates an account
- Request to Cognito
- Calls Lambda
- Stores userdata in DynamoDB table "Users"
Login
- User input auth credentials
- Cognito handshake, calls lambda
- Store lastlogin in DynamnDB
Link creation
- User input long link
- API GW request
- Calls lambda that to transformation
- Store link in Link DynamoDB
- Lambda respond with shortlink
- 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