Estimate the scale of the system. Consider daily active users, read/write ratio, storage requirements, bandwidth, and any relevant QPS calculations...
Web servers (per region):
Cache (per region):
Connections:
maxclients (10K) is nowhere near stressed.Write:
POST /
Service takes long URL, generates UUID (to be covered in the high level design) and stores it before returning the short URL ID to the page
Read:
GET /
Service performs lookup for long URL associated with this URL ID and redirects user to the associated long URL.
Since the requirement was to generate a new tiny url for every request (instead of returning an existing tiny url for a website), there is no need to do a reverse lookup for a long form URL - but it does reduce the potential size of how many unique URLs will be covered by the short URL IDs.
Website:
Single URL, leverages geo DNS to route to the closest service region
Write path:
The short URL ID is the key, with the value being the longform URL. On write path, the record will be stored in a key/value store (dynamo is an option) and returned to the user. On the return path, an attempt to update the in-memory cache will be made, but the response will not fail if the write fails. Log streaming solution will be in place to replicate the entries globally.
Read path:
The short URL will hit the service GET endpoint; the cache will be checked for the mapping of tiny URL to longform, and the service will do a 302 redirect (to support end client metrics) to the long form URL. If the entry is not present in the cache, it will fallback to the data base.
Caching:
Caches will be localized by geography, and can be at the web server since the read values associated with a key never changes.
Availability:
Databases are globally replicated, with redundancy (three availability zones) best practices per region. In the event that all 3 AZs for a region are down, the geo DNS can re-route to calling the next closest region.
Storage Choice - No SQL database like DDB or Cassandra. After ID creation, look up is one way ALWAYS. Database is mostly to revive the caches, and to provide a fast lookup on cache failure.
Partition key - Short_code as the key. Short code will be randomly distributed, and there are no reasons to co-locate any of the entries
Consistency - strong on create, to prevent duplicate codes. Eventual consistency for reads is ok, since it is such a small possibility.
Schema - short_code (PK), long_url, created_at
Caching layer: Redis will serve as a CDN origin source, so the CDN layer will provide additional scaling for "hot" URLs. This should cover any excessive traffic bursts. If the CDN and cache layer miss, fetch from storage and serve the 302 as well as refresh the cache and CDN. Cache expiry can be a combination of TTL and LRU to prevent a sudden expiry and the thundering herd scenario.
The redis caching will have shards keyed on the short code hash, and shards will have replication.
ID Generation: Use a securely random number generator to create a 7 digit ID [0, 62^7). Base 62 encode, and zero-pad the output to 7. On write, if the write fails due to an existing entry, regen the ID. Handling collision retries is cheap; the issue won't come till the IDs run out which is projected out for 10+ years.