1) Scalable
2) Availability
3) Consistency
4) Durability
1 million daily active users
Traffic:
For throughput:
redirect transaction involves retrieving or processing two pieces of data:
Storage:
generate_url
(Typically, the client uses an API key that was previously issued by the server. In many systems, during a registration or onboarding process, the server generates and provides the API key to the client. The client then includes this key in subsequent requests to authenticate itself.)
redirect_url
user_table
url_table
analytics_table
The analytics table is designed to store detailed information about each redirect event. It logs various attributes of every request, which can be used for monitoring, analysis, and optimization. Here’s what it typically tracks and why:
Components
Redirect flow:
1) user hits the load balancer first to determine where to redirect the user
2) we are pointed to the redirect service
3) now we have the request in the redirect service
4) we fetch the short url key from the cache
7) update the cache with key- value of shorturl: longurl
Creation flow:
1) user hits load balancer
2) we are sent to the creation service
3) here we generate a short url for the long url
4) lets say we do a hash for the long url ~ and then enode it to base64 so that the url is alphanumeric then truncate to 8 characters if its more than that
Base64 is shorter than Hex, less character
Hexadecimal:
Uses 16 symbols (0–9 and A–F). Each hex digit represents 4 bits.
→ 2 hex digits per byte (8 bits).
Base64:
Uses 64 symbols (typically A–Z, a–z, 0–9, and two additional characters, like '+' and '/'). Each Base64 character represents 6 bits.
5) we will try to insert this in the database, if it fails, because there is a unique constraint on the short url, we retry
Bottlenecks will come from the read path of our system. Meaning the cache and the database will be hit with a ton of traffic.
Both services are stateless, so it is easy to scale them by adding more servers.
Stateless: The service doesn't store client-specific state between requests. Each request must contain all the information needed to be processed.
1) Database consideration
2) Cache consideration
Refresh-Ahead:
Freshness & Consistency:
Even if a cache entry isn’t evicted, it might become stale if the underlying data changes. Refresh-ahead updates the data before it expires, ensuring that every cache hit returns current information.
Avoiding Expiry Bursts:
Without refresh-ahead, if many entries expire at once, a sudden surge of cache misses could overwhelm your database. Refresh-ahead smooths out these peaks by updating entries in the background.
Improved Performance:
By proactively refreshing the cache, you reduce the likelihood that a user request will have to wait for a database query, thus maintaining low latency and a good user experience.
Write-Through:
3) Load balancing
1) We use a cassandra( WAL and a LSM tree ) as our main analytics database since it can handle a lot of traffic for writes. Since it is write optimized, it is a lot better than a relational database since relational databases use b-trees and will have slower writes
2) Since we do sharding - we have to keep in mind that we cannot join easily. Since we dont really need to join any data, it is a good choice for us.
Why sharding makes joins difficult:
Why joins aren't really needed in a URL shortener design:
3) Since we do read replicas - we need to keep in mind the delay in updating the replicas. Since we have a cache that we update and since the first write is returned optimistically. it should be covered.
4) Since we have a few nodes/shards/replicas we need to know how to manage this system. A tool like Kubernetes, can be used to make sure our nodes are up and running.
In many modern system designs, especially those involving microservices, sharded data, and multiple replicas, Kubernetes is often mentioned because it provides a consistent and efficient way to manage the complexity of distributed systems.
Docker Swarm:
A native clustering tool for Docker containers. It's simpler to set up and manage compared to Kubernetes, though it may not offer as many advanced features.
Apache Mesos with Marathon:
Mesos is a cluster manager that abstracts resources from entire data centers, and Marathon serves as a container orchestration framework on top of Mesos. It’s used in large-scale environments that need to manage both containerized and non-containerized workloads.
HashiCorp Nomad:
A lightweight and flexible orchestrator that supports not only containers but also non-containerized applications. It's known for its simplicity and ease of integration with other HashiCorp tools.
Managed Services:
Cloud providers offer their own orchestration solutions, such as:
1) The load balancer is a bottleneck and could fail. We will have a backup loads balancer who is passive. Traffic will be redirected to it in case the main load balancer fails
2) Nodes in the database and caches could fail. Since we are using Kubernetes, we rely on it to bring them back up.
3) Servers running the services could fail. Again, we rely on Kubernetes/or other orchestration software to revive the services.
3) We rely on Kubernetes and it could fail.
4) When a node in the Redis cluster fails, we could get a lot of cache misses. Since we use consistent hashing, we only get a minimal amount of keys that get affected since they are split in between the nodes and virtual nodes
5) Some database nodes could get too much traffic/data if the sharding is not done properly - a hot spot. We have to monitor this so that we can improve our key distribution. Sharding the large node should be enough to fix the issue temporarily.
1) Adding availability zones like east and west coast. Adding data centres in multiple continents to get closer to users. This can reduce the load of the system since we balance the load among regions. Doing this also improves availability since now even if datacenters or availability zones are down, we can redirect to other regions.
2) Improved algorithms when generating shortened URLS. Since currently we hash it our selves. There is no causality, it is hard to order the shortened URLs. For this we can use a technique like SnowFlake which integrates some of the bits in the returned value so we are able to have some order.
3) Using Terraform to manage the whole system. Since now there are a lot of moving parts, we need to use something that can make managing our system easier.
4) Using Redis Sentinel to manage our cache clusters can help us handle failure events.
Redis Sentinel is designed to handle cache failures. It continuously monitors Redis instances and, if it detects that the master has failed, it automatically promotes one of the replicas to be the new master
5) We also need to discuss how our databases handle partition events. Like in primary/secondary where have promote a secondary node to a primary. Voting mechanisms to who should be the new primary. How data is replicated among nodes. So we can gracefully recover from the backups/replicas.