Let's shard on longURL, allowing horizontal DB scaling without incurring cross-partition writes / accesses.
A hash index for checking if a LongURL has been shortened before would be a good choice. Also we could batchly dump the key-value pairs to SSTables, which allow for O(logn) finds instead of O(1) but uses less memory than the hash index.
Sharding is all we should need to bring the system within our requirements. Thus we can get away with single leader replication for durability's sake and potentially to increase throughput on hot shards, etc.
High-level design
The client's request will hit a load balancer, where consistent hashing (on the longURL) is used to determine a shard ID (this will be used to determine which stateless web server to direct the request to as well as the DB node where the shortURL would be found if we already have it.
The web server will make a request to the appropriate DB shard and if it exists already, the stored short link will be returned to the user within a 3xx redirect if they were requesting getPage(). If it doesn't exist and they were calling getPage, return a 4xx error. If they were calling getShortLink() and the link doesn't exist, we must create it. The web server will delegate creation of the hash to a has worker, perhaps buffered by a durable message queue like kafka. Similar to a resource manager, worker nodes could be scaled to deal with increasing load.
Once the hash of the website content has been calculated, for uniqueness we should use a good hash function with a bit range of at least 2^32, we will persist the short:long element in the key value store and return the short link to the user
Request flows
Above
Detailed component design
Above
Trade offs/Tech choices
Above
Failure scenarios/bottlenecks
SPOF: Load balancer, could scale horizontally but increases complexity beyond what is necessary for the question.
Future improvements
What are some future improvements you would make? How would you mitigate the failure scenario(s) you described above?