Recently I've been reading about system design, and I stumbled upon one of the "easiest" and "most basic" interview questions in system design: "Design A URL Shortener"
I saw the problem in Alex Xu's book, systemDesignOne, algomaster, HelloInterview, etc... And I noticed that these resources tend to "handwave" a lot of depth, depth that a real interviewer will hone in on because we know many people use buzzwords without understanding the background.
The "handwaves"
1. NoSQL vs SQL
Something I consistently saw across these guides is the disregard for explaining why SQL or NoSQL. In some cases, the authors mention it on one line, and that's it:
"A better option is to store mapping in a relational database."
Other times, articles use the same substance-less justification:
"Given these points, a NoSQL database like DynamoDB or Cassandra is a better option due to their ability to efficiently handle billions of simple key-value lookups and provide high scalability and availability."
I see these consistently:
"As the URL mappings are independent and don't have complex relationships, a NoSQL database is a good fit. MongoDB is a strong candidate because:
It uses a leader-follower protocol, enabling the use of replicas for heavy read workloads.
MongoDB ensures atomicity in concurrent write operations and returns duplicate-key errors to prevent collisions.
I know the authors can't cover every topic, and many expect readers to know these concepts already. But this is a recipe for a candidate to stumble the moment someone questions these assumptions.
Interviewee: "We'll use NoSQL because it can handle billions of simple key-value lookups"
Interviewer: "Can't SQL handle simple key-value lookups?"
Interviewee: "Uhhhh, no?"
Most databases are already quite advanced, so either SQL or NoSQL will work for 99% of projects. For example, a Primary Key lookup in PostgreSQL (SELECT long_url FROM urls WHERE short_code = '2TX';) traverses a B-Tree index in memory in $O(\log N)$ time (3 to 4 pointer hops), resolving in under 1 millisecond—matching DynamoDB single-digit millisecond latency over TCP. We will also focus on that 1% of projects at massive scale where database choice matters.
So I intend to clarify the best database choice in depth for at least 3 scenarios: local scale, national scale, and global scale. And explain what actually matters when picking a database.
2. Single Point of Failure Crashes.
This is one of my biggest gripes because so many articles say a variation of "We have more than one database so we are good" or "the provider will handle this for us"
For example:
"Implement automated failover mechanisms for the API and data store layers to switch to backup servers in case of failure."
Or:
"Our core components (databases, caches, servers) support replication, which provides high availability and fault tolerance. The unique ID generation process also relies on a replicable database, which prevents a single point of failure."
And never mention this again. Or, even worse, don't mention single points of failure at all. This matters especially on smaller-scale systems: what if a single server or database handles all our traffic? - Which begets more questions like:
-
At what size should a new database or server be added?
- CPU saturation, connection limits, and blast radius dictate a new server. In production, scale out compute nodes once sustained CPU exceeds ~70% or active socket connections saturate thread pools. At the same time, databases add read replicas when read latency spikes and scale storage volumes vertically long before horizontal partitioning is ever warranted.
- For example, on Amazon RDS, the cost of CPUs/RAM of the database server is separate from the storage (e.g., SSD); each GB costs about $0.23 USD (Multi-AZ), so for pure data growth you'll find yourself increasing storage capacity rather than server capacity.
-
Can we get away with two tiny EC2 Amazon instances (
t4g.nano)?- Yes, for low-throughput workloads (like a few hundred to thousands of requests daily), two
t4g.nanoinstances (0.5 GB RAM, 2 vCPUs) sitting across two Availability Zones behind an Application Load Balancer provide real high availability for negligible cost. The constraint on nano instances is not handling the traffic spikes—which burstable credits cover—but keeping the operating system and application memory footprint small enough to avoid Linux Out-Of-Memory (OOM) kills. - One might confuse compute and storage here and think that having two instances is unnecessary if we are using a managed DB service (e.g., Amazon RDS) for two reasons:
- 1. Managed database services already include a "multi-availability zone" option, so it's easy to assume we don't need two compute instances for resilience.
- 2. The bulk of an instance's cost is CPU/RAM, not storage—so running multiple instances sounds like wasted money compared to a single instance with more storage capacity.
- But in reality, these are two separate layers: two
t4g.nanoEC2 instances across AZs provide high availability for the stateless compute tier (so our web app doesn't go down if one VM reboots), while an RDS Multi-AZ deployment provisions a second standby database instance in another AZ to provide high availability for the database tier.
- Yes, for low-throughput workloads (like a few hundred to thousands of requests daily), two
-
Are Amazon instances already crash-resistant?
- No, individual EC2 instances are isolated virtual machines that can and do crash, terminate during underlying hardware faults, or lose transient host capacity.
-
Do Amazon EC2 instances already have memory attached?
- Every EC2 instance has virtualized RAM allocated by the hypervisor. Still, that memory is strictly volatile—the instant an instance crashes or restarts, all in-memory data is erased, which is why stateful persistence is offloaded to remote network storage (EBS) or managed database engines.
My goal is for readers to understand the nuances behind saying, "Put it all on DynamoDB and let it be."
3. How Is the Short URL Actually Made?
So far, I've found three main approaches to shorten the original URL; all are valid, but I've noticed guides skip details of each implementation.
3.1. The Hash and collision loop.
These look deceptively simple, and most articles have some variation of:
"To resolve hash collisions, we can recursively append a new predefined string until no more collisions are discovered."
Or:
"And truncated hashes collide: in 62^7 slots a collision becomes likely somewhere around a few million links, which you will pass on day one. You can check the table and retry, but now every create is a read plus a write, and the retry rate climbs as the table fills."
Or:
"If a collision occurs, we append a predefined string or salt to the long URL and hash it again until we find a unique key."
But what they omit is details about how hashing works - Imagine this:
Interviewee: "We can hash the long URL and get the first seven characters of the hash as our short URL"
Interviewer: "How would you prevent collisions between the first seven characters of the hash"
Interviewee: "I would append a predefined string or salt the long URL and hash it again"
Interviewer: "Would having a predefined string fix just one collision? What if you get more than two long URLs with the same seven characters? Would you also have to save in the database a list of salts?"
Interviewee: "Yes?"
The most important things to understand in this pattern are:
Hashes are non-reversible and are not compression. A cryptographic hash (like MD5 or SHA-256) doesn't compress a 300-character URL; So hashes in this case are just a way of generating a "random" string with alphanumeric characters - it's no different from generating a random string.
Collisions happen earlier than most people realize; for a seven-character short URL (3.5 TRILLION possible URLs), collisions start happening just after a few million generations (~2.35 million due to the Birthday Paradox).
3.2. The Over-Engineered "Key Generation Service" (KGS)
I don't think anyone in production should use something like this because it's complex and slow. It technically solves collisions, but it introduces a complex system that is hard to scale and manage.
Defenders of this implementation could argue for its generality, including things like a snowflake/Twitter ID generator. Still, those unique ID generators can't be used for short URLs if we want 7-8 characters because a 64-bit Snowflake ID is 19 digits in decimal, and converting that 64-bit integer to Base62 still requires 11 characters (If this doesn't make a lot of sense, that's okay; it will be explained soon).
A few examples of this implementation in online guides are:
"We can have a standalone Key Generation Service (KGS) that generates random six-letter strings beforehand and stores them in a database (let's call it key-DB). Whenever we want to shorten a URL, we'll take one of the pre-generated keys and use it. This approach makes things simple and fast. We also avoid encoding the URL, and we won't have to worry about duplicates or collisions. KGS will make sure all the keys inserted into key-DB are unique"
And then the same source handwaves the database overhead:
"Servers can use KGS to read/mark keys in the database. KGS can use two tables to store keys: one for unused keys, and one for all used keys. As soon as KGS gives keys to a server, it can move them to the used keys table. KGS can keep some keys in memory so it can provide them quickly whenever a server needs them. For simplicity, as soon as KGS loads some keys in memory, it can move them to the used keys table. This ensures each server gets unique keys. If KGS dies before assigning all the loaded keys, it can move them back to the used keys table."
This pattern also adds huge DB overhead by requiring synchronization between databases and continuously inserting, reading with locks, moving, and deleting rows.
KGS also handwaves the complexity of server crashes (availability) and multiple databases; for example, do we have separate databases for used and unused keys? Are they in the same database? What if one database crashes? How do we decide which database to send the data to then?
I've noticed candidates gravitate towards this solution when they want to design for large scale but lack specific knowledge of more streamlined patterns, like using Redis counters, which are single-threaded, or relying on MongoDB's internals; therefore, they can generate a count that will always be unique - we'll dive deeper into this later in the article.
3.3. The Counter + Base62: The Clean Math (And the Handwaved ID Generator)
This is my preferred solution. But I've noticed sites often leave out details that can make or break someone's mental model. Here are a couple of examples:
"Its INCR command atomically increments the Counter and returns the new value in a single operation. Because Redis is single-threaded, two simultaneous calls will always receive different values. If one gets 1000, the other gets 1001. This guarantee is what makes Redis ideal for distributed counter management. Each counter value is unique, eliminating the risk of collisions without the need for additional checks."
And:
"Challenges: In a distributed environment, maintaining a single global counter can be challenging due to synchronization issues. All instances of our Primary Server would need to agree on the counter value... Sequential counters also produce predictable short codes, making URL enumeration possible. An attacker could iterate through codes to discover all URLs."
If the candidate memorizes this information without understanding the background, they will fall into a trap like:
- Hash vs Radix: Sometimes candidates think "Base62" is like a hash function, but it is actually a Radix function. Here are the differences between them:
| Feature | Hash Function (e.g., SHA-256, MD5) | Base62 (Radix Representation) |
|---|---|---|
| What it is | A one-way mathematical function mapping variable-sized input to a fixed-size digest. | A positional base-conversion system using 62 alphanumeric characters (0-9, a-z, A-Z). |
| Primary Goal | Fingerprinting, integrity verification, or pseudo-random distribution. | High-density positional notation: representing large Base-10 integers using fewer characters. |
| Reversibility | Irreversible (one-way; original input cannot be computed back from the hash digest). | Fully Bijective (two-way; can be decoded back to the exact starting integer via modulo/quotient arithmetic). |
| Collision Risk | Present (different inputs can produce matching digests via the Birthday Paradox). | Zero (a unique input integer maps deterministically to a unique Base62 string). |
| Output Length | Fixed length (e.g., SHA-256 is always 256 bits / 64 hex characters regardless of input length). | Dynamic length (proportional to integer size: values $< 62^7$ fit within 7 characters). |
-
Using a single-threaded counter without mentioning coordination when scaling, where the Counter lives, or what happens when your "counter server" crashes.
- In production, app servers don't call an external counter on every single write. They use dynamic range leases. It's like fetching a block of 1,000 IDs at a time with
INCRBY 1000from an atomic coordinator (like Redis) and dispensing them locally in memory. If an application server crashes, the remaining IDs in that leased range are abandoned; because a 7-character Base62 namespace has over 3.5 trillion possible values, discarding a few hundred numbers during a crash costs practically nothing and avoids complex recovery locks. Meanwhile, the coordinator survives restarts by replaying its on-disk write-ahead log (such as Redis AOF) or synchronizing across Multi-AZ replicas with a forced offset jump to avoid reissuing duplicate IDs.
- In production, app servers don't call an external counter on every single write. They use dynamic range leases. It's like fetching a block of 1,000 IDs at a time with
-
Security fixes: Many articles mention how using a sequential unique counter creates security issues; yet none show how to fix this.
- The fix is to pass the integer counter through a reversible, collision-free permutation—such as a lightweight Feistel cipher or an obfuscation tool like Sqids/Hashids—before converting to Base62. This scrambles sequential numbers into deterministic, random-looking alphanumeric tokens without risking duplicate keys.
- Remember: The generated short URL doesn't need a direct relationship to the long URL. Your short URL doesn't need to include any information about the long URL; it just needs to be unique.
4. Redis Read-through/Write-through/Cache aside
When guides mention Redis, I've noticed many don't distinguish between Read-through/Write-through and cache-aside architectures. This distinction matters when discussing our system's trade-offs. Some examples I've seen:
"1. A user clicks a short URL link: https://tinyurl.com/zn9edcu
- The load balancer forwards the request to web servers.
- If a short URL is already in the cache, return the long URL directly.
- If a short URL is not in the cache, fetch the long URL from the database. If it is not in the database, the user likely entered an invalid short URL.
- The system returns the long URL to the user.
The example doesn't even mention whether we're using cache-aside or read-through/write-through architectures. It might describe cache-aside, but it leaves the reader guessing about the details—and notice how Step 4 fetches from the database and Step 5 returns it to the user without ever writing the URL back into the cache!
Or this other example:
"After receiving a request, a web server first checks if the cache has the available response. If it has, it sends data back to the client. If not, it queries the database, stores the response in cache, and sends it back to the client. This caching strategy is called a read-through cache"
This example is misleading; what it describes is actually the definition of Cache-Aside, not Read-Through. In a true Read-Through cache, the application server talks only to the cache layer. The cache engine itself owns the database driver, connects to PostgreSQL, and transparently pulls the record on a miss. In the real world, almost every production stack uses Cache-Aside because standard Redis has no database drivers or query logic to fetch data from PostgreSQL or DynamoDB on its own. Mislabeling Cache-Aside as Read-Through confuses candidates about who actually owns the database driver and SQL connection pool.
And:
"We can cache URLs that are frequently accessed. We can use some off-the-shelf solution like Memcached, which can store full URLs with their respective hashes. The application servers, before hitting backend storage, can quickly check if the cache has the desired URL."
These examples barely explain how the hardware works or how it fits into our architecture. Are these services (Redis/Memcached) added on their own instance? How are they provisioned? What happens if they crash? Does Redis sit between the server and database as a proxy, or alongside the server as an internal backing service? If the same Redis cluster handles both the read cache (with LRU eviction) and the atomic ID counter (INCR), what stops Redis from evicting your counter key when memory fills up? If Redis crashes, does your Counter reset to zero? - All of these questions will be answered properly when we write our own implementations for three different scales.
Guides also handwave what happens when you use Redis for counting and caching:
As an Ephemeral Cache (Reads): URLs are cached with an LRU (Least Recently Used) eviction policy to keep hot links in memory.
As an Atomic Counter (Writes): Redis executes
INCRorINCRBYto hand out unique IDs for Base62 encoding.
Another crash could happen if you co-locate both roles on a single Redis instance configured with the standard allkeys-lru (least recently used) maxmemory policy, because servers lease counter blocks in chunks (e.g., fetching 1,000 IDs at a time). That counter key sits idle between leases. When read traffic surges and Redis memory fills up, Redis's LRU algorithm will see the idle counter key and evict it from RAM. To prevent Redis from evicting the Counter while running, change the configuration from allkeys-lru to volatile-lru (under volatile-lru, Redis evicts only keys with an explicit expiration time, leaving our non-expiring counter key untouched) and/or physically separate the instances. To ensure the Counter doesn't reset to zero if the Redis server crashes and reboots, we need an Append-Only File (AOF) saved to disk or replicated across Availability Zones.
5. Real sizes (Servers, databases).
System design prep guides frequently create world-scale scenarios, inflate traffic numbers, and jump to massive abstract scales:
"Because of the massive scale of billions of records, we must shard our database across multiple database servers using consistent hashing on the short URL."
Or even examples like:
"Since we expect to have 500M new URLs every month and we would like to save this data for 5 years:
Total number of URLs: 500M * 5 years * 12 months = 30 billion
Total storage: 30 billion * 500 bytes = 15 TB""To scale out our DB, we need to partition it so that it can store information about billions of URLs. We need to come up with a partitioning scheme that would divide and store our data into different DB servers."
These examples usually defend themselves by saying, "The cloud will handle it." Still, it's important to know how much servers actually cost, how much capacity they have, and how they differ.
For instance, some guides calculate 15 TB for 30 billion URLs and claim we must partition across different DB servers. But in real-world infrastructure, a single standard AWS RDS PostgreSQL instance can attach up to 64 TB of storage on a single volume, and an Amazon Aurora cluster scales up to 128 TB automatically without sharding. 15 TB easily fits inside a single managed instance. At the same time, relying on a standalone, un-replicated database instance on a single server without synchronous replication or automated failover remains a catastrophic Single Point of Failure.
Or what if you need a small, internal system? A single db.t4g.micro instance (with 1 GB of RAM and a baseline 20 GB gp3 SSD) and a t4g.nano server are more than enough, yet if that DB or server crashes, your whole system has multiple single points of failure. Imagine a scenario like this:
Interviewee: "So one single instance would be enough for this small-scale operation"
Interviewer: "So how would you handle redundancy? What would happen if that instance fails?"
Interviewee: "The cloud will handle it?"
Many guides also gloss over provisioning. When we provision a managed cloud database like AWS RDS (db.t4g.micro), we're renting an isolated virtual machine. If we run it as a Single-AZ (Availability Zone) instance, we still have a Single Point of Failure, so to make it resilient, we need to enable Multi-AZ (which provisions a synchronous hot standby in a different data center that automatically takes over if the primary dies). The problem is that guides often overlook the internals of this mechanism, and candidates often treat it as a magic phrase without realizing it solves failover resiliency, not write-throughput scaling.
Candidates often have warped mental models about the physical reality of the servers. For example, Redis persistence doesn't work the same across bare metal and cloud providers. On a self-hosted Linux machine, Redis logs transactions directly to /var/lib/redis/appendonly.aof on an attached SSD.
However, on burstable managed cloud instances like AWS ElastiCache (cache.t4g.micro), AWS turns off continuous AOF disk logging because constant disk syncing (fsync) exhausts shared I/O credits and blocks the single-threaded event loop.
Note: Burstable instances and I/O credits
A burstable instance operates on an allowance: you get a modest baseline of CPU capacity (around 10%–20%) when idle, but you accumulate credits that let you burst up to 100% CPU when sudden traffic arrives.The issue is that storage operations follow a similar metering model. When you run an Append-Only File (AOF), every URL creation triggers an
INCRcommand, which forces anfsync()to physically write those bytes from the operating system's buffer to disk. On a tiny, multi-tenant cloud instance, continuousfsync()calls burn through your allocated I/O credits almost immediately. Once those credits drain to zero, disk access is throttled, causing writes that usually take under a millisecond to stall for 50–100ms. Because Redis executes all commands on a single thread, waiting on that throttled disk blocks the entire engine—freezing reads, writes, and all incoming network connections.
So instead of writing an on-disk transaction log, managed micro instances rely on network-level durability: the primary node asynchronously streams memory state to a hot replica in a different Availability Zone, while periodic binary snapshots (RDB files) are pushed to durable Amazon S3 storage for disaster recovery.
Something else I've noticed is that guides tend to handwave about scaling a system progressively. If we are going to need 15 TB or 36 TB of data in 5 years, we shouldn't architect Day 1 for Year 5; we can size this for 12-24 months and then scale upwards if needed.
I believe it's important to ground QPS/TPS/storage values in real offerings. That way, it's much easier to build a complete mental model.
6. Load Balancer vs. API Gateway vs. Reverse Proxy
I've noticed that many guides also use Load Balancers, API Gateways, and Reverse Proxies interchangeably. For example:
"The load balancer distributes incoming client requests across multiple servers to prevent any single server from becoming a bottleneck. The load balancer can be configured with routing algorithms such as round-robin, least connections, or IP hash."
While another guide ignores the load balancer completely and shows a diagram like this:
[ Client ] ──► [ API Gateway ] ──► [ URL Service ] ──► [ Database ]
Guides should be clear about this instead of glossing over it. In a real system, we might need both an API Gateway and a load balancer, or just one, depending on our architecture. Much of the confusion comes from these two being bundled together in modern managed offerings—such as cloud ingress controllers (like Azure Application Gateway Ingress Controller or GKE Ingress) that handle external Layer 7 (HTTP/HTTPS) traffic routing, TLS termination, and load balancing in a single layer.
Even when bundled, an API Gateway and a Load Balancer are not interchangeable. The confusion stems from the fact that both API Gateways and Load Balancers are reverse proxies:
-
A Load Balancer is a reverse proxy tuned for raw connection throughput and even traffic spreading. It is lightweight and consumes very few resources. Load balancers can also be confusing because they behave differently depending on which network layer they operate at:
- Layer 4 (Network Load Balancer): Looks only at IP packets and TCP/UDP ports. It has no concept of HTTP, meaning it cannot see URL paths, headers, or request lines.
-
Layer 7 (Application Load Balancer): Parses HTTP/HTTPS request lines, headers, cookies, and paths (e.g., routing
/api/*to Target Group A and/*to Target Group B). However, it stops short of evaluating application-level business policy like JWT auth verification or user-specific rate limits. Because of this path routing, an ALB feels very close to an API Gateway.
An API Gateway is a reverse proxy tuned for Layer 7 application policy (authentication, granular rate limiting, and route rewriting). It uses significantly more computational power to inspect payloads, validate cryptographic tokens, and enforce business rules.
Another confusing point is that an API Gateway can technically serve as a Load Balancer. Many API Gateways (such as Kong, Envoy, or NGINX) let you configure upstream load-balancing algorithms like Round Robin or Least Connections across your backend instances.
To make things more complex, Load Balancers can sit either "before" or "after" an API Gateway depending on how the system is set up:
- The High-Scale / Kubernetes Setup (Load Balancer BEFORE Gateway):
If you run gateway software like Envoy or Kong yourself, you need multiple gateway servers so the gateway itself doesn't crash or become a bottleneck. But how do internet users reach multiple gateway servers? You put a Layer 4 Network Load Balancer (NLB) in front to balance incoming TCP sockets across your gateway fleet.
- The Standalone Gateway (No External Load Balancer):
In smaller setups, or when using cloud-managed serverless gateways (like AWS API Gateway), you don't provision a separate load balancer in front of it. AWS API Gateway already has an internal ingress routing fleet built in. You point your custom domain name directly to API Gateway, and it routes requests to your backend microservices or serverless functions.
- Zero API Gateways (Load Balancer Only):
If you are building a simple service that doesn't need API token tiers, client rate limits, or complex route rewrites, an Application Load Balancer (ALB) can terminate TLS (decrypting incoming HTTPS traffic) and use Round Robin to distribute HTTP requests straight to your compute nodes. The backend servers handle authentication and business logic themselves.
So we can think of these three in an inheritance hierarchy:
Reverse Proxy: Any server that sits between a client and a backend server, intercepting requests and responses so the backend stays hidden from the outside world.
Load Balancer (Reverse Proxy + Distribution): A reverse proxy optimized to monitor backend health and spread traffic evenly. A Layer 4 LB inspects only raw TCP packets, while a Layer 7 LB inspects HTTP request lines and paths. Both offer ultra-low latency and consume minimal compute. It can send requests to an API Gateway fleet or directly to backend servers.
API Gateway (Reverse Proxy + Load Balancer + Governance): A specialized reverse proxy that does load distribution, but also inspects HTTP headers and payloads to enforce business rules—blocking unauthenticated callers, rejecting spam via token buckets, and rewriting URL paths before application code ever executes. Because it decrypts bearer tokens, parses request bodies, and validates schemas, it uses more compute and can introduce a slight latency penalty. It typically sends requests directly to backend servers, though in complex multi-tier setups it can route through internal load balancers.
7. Consistent hashing
Whenever system design articles discuss scaling past a single machine, they mention consistent hashing:
"Our design is horizontally scalable. The database can be sharded, and we can use consistent hashing to distribute load across both the application and database servers evenly."
"The additional data store is partitioned using consistent hashing. The partition key is the long URL, so we can quickly find the URL record. A key-value store such as DynamoDB is used as the additional data store."
Or fall back on basic modulo arithmetic:
"We should also potentially consider sharding the database. We can shard it using some mapping from the URL (for example, the URL's hash code modulo some integer), which will allow us to locate the database which contains this file quickly."
Modulo hashing (hash(key) % N) is an anti-pattern for dynamic systems (which most systems are): the moment you add or remove a single server, almost 100% of your keys map to entirely different machines, causing data thrashing or thundering herds. Consistent hashing solves this by ensuring that when a node joins or leaves, only a fraction of keys ($K/N$) get remapped.
The problem is how guides illustrate it: a cosmic 360-degree circle floating in the middle of a whiteboard diagram, with keys and servers casually "mapped onto the ring."
[ Application Servers ] ──► ( The Floating Hash Ring ) ──► [ Database Shards ]
This abstraction can lead to blind spots when an interviewer digs into the physical implementation:
1. Where Does the Ring Physically Live?
Candidates often assume the hash ring is a standalone service or a central coordinator machine. But if 50 application servers have to make a network call to a "Hash Ring Coordinator" on every single request, that coordinator becomes a latency bottleneck and a single point of failure.
In production, there is no ring server. The hash ring is an in-memory data structure (like a TreeMap<Long, Node>) living directly inside the local process RAM of the server making the routing call to the database.
When a server needs to route a short URL:
It hashes the key using a fast, non-cryptographic algorithm like MurmurHash3 on its own CPU.
It executes a local binary search in $O(\log N)$ time to find the nearest server pin.
The lookup finishes in under a microsecond with zero network round-trips.
2. How Do Servers Coordinate Changes?
If every application server keeps its own copy of the ring in local RAM, how do they stay synchronized when a database shard crashes or a new cache node boots up?
Production architectures solve this through two distinct models:
- Centralized Coordination:
In systems using standalone cache pools (like Memcached) or sharded SQL instances, the storage nodes don't talk to each other. Instead, a small, independent cluster of coordinator machines (e.g., t4g.small instances running etcd, Consul, or ZooKeeper) continuously pings the storage nodes. Application servers keep an open watch listener with this coordinator fleet. When a storage machine drops offline, the coordinator alerts the application servers, which instantly recompute and swap their local in-memory tree pointers to route around the dead node.
- Decentralized Gossip:
Distributed databases like Apache Cassandra or Amazon DynamoDB eliminate external coordinator servers. Each database server maintains the hash ring directly in process memory. The database machines continuously ping each other across an internal network port to exchange health updates ("Gossip"). If Database Machine C dies, Database Machines A and B detect the dropped heartbeat, spread the update cluster-wide, and independently adjust their local partition boundaries without needing an external manager.
3. Can Load Balancers and API Gateways Use Consistent Hashing?
Guides suggest using consistent hashing to "distribute load across both application and database servers." This creates confusion: should our Load Balancers or API Gateways use consistent hashing to distribute requests to stateless backend servers?
For a stateless URL shortener - absolutely not.
An Application Load Balancer's job is fair load distribution. Algorithms like Round Robin or Least Outstanding Requests spread traffic evenly across all web servers.
If your Load Balancer consistently hashes incoming requests based on the URL path (
GET /popular-deal), 100% of the traffic for that viral link will hammer the same backend server. That node will burn through CPU and crash while the remaining servers sit idle. Because stateless backend nodes read from a shared Redis cache or database cluster anyway, pinning specific URLs to specific app servers creates hot spots with no performance gain.
Consistent hashing at the Gateway or Load Balancer tier makes sense only when the upstream servers hold in-memory, non-shared state:
Local In-Memory Cache Tiers: If backend compute nodes maintain an LRU cache directly inside their own process memory instead of calling an external Redis instance, consistent hashing ensures repeat requests hit the host that already holds the cached data.
Stateful WebSockets: Real-time chat applications (like Discord) use consistent hashing at the gateway to route users to the exact server hosting their active room connection.
Layer 4 Packet Balancing (e.g., Google Maglev): Specialized edge load balancers use consistent hashing algorithms across router fleets so that network packet routes remain stable even if intermediate proxy boxes restart.
Unless your compute nodes hold local state, consistent hashing belongs on the data-routing clients targeting partitioned caches or database shards, not on the front door distributing traffic across stateless application instances.
Next steps
Now that we have seen the many holes that guides tend to have, the next part of this article series will show how to design a URL shortener without leaving out any details. This way, any candidate asked this question will be prepared to go in depth at any point in their interview.
FAQ
Implementation · Will the next article provide concrete code or architecture diagrams?
Yes. Part 2 provides production VPC topologies and runnable code across three distinct tiers:
Small Internal MVP (~500 writes/day): Multi-AZ PostgreSQL using native
BIGINT GENERATED ALWAYS AS IDENTITYsequences, an Application Load Balancer, and a micro Redis instance for Cache-Aside reads.National Consumer Scale (~5,000 writes/sec): Horizontally autoscaling app clusters, dedicated Redis clusters separating atomic ID range leases from LRU read caches, and read replicas for uncached lookups.
Global Scale (100M+ writes/day): Multi-region edge ingestion, decentralized token leases, and auto-partitioned storage (DynamoDB Global Tables).
Trade-offs · How do I actually defend SQL vs. NoSQL in an interview?
Do not recite the generic textbook claim that "NoSQL is faster for key-value lookups". In memory, a PostgreSQL primary key lookup traverses a B-Tree index in $O(\log N)$ time (3 to 4 pointer hops) and resolves in under 1 millisecond—matching DynamoDB latency over TCP. Base your defense on two concrete physical factors:
Write Idempotency (Deduplication): If shortening an identical long URL twice must return the same short code, SQL guarantees this natively via an atomic
UNIQUEconstraint onlong_url. In a NoSQL table partitioned byshort_url, querying bylong_urlrequires either a catastrophic full-table scan or an asynchronously replicated Global Secondary Index (GSI). Because GSIs are eventually consistent, concurrent writes for the same link create duplicate short codes.Single-Writer Ceilings: A single properly tuned relational database instance comfortably handles 10,000 to 20,000 indexed inserts per second and up to 64–128 TB of storage. Choose NoSQL (DynamoDB or Cassandra) only when sustained write throughput exceeds what a single primary can physically commit (30,000–50,000+ writes/sec) and you treat every link generation as a blind append without global deduplication.
Failure Handling · What happens during a cloud database failover?
Availability is preserved by isolating failure domains rather than assuming managed cloud services eliminate downtime:
The 30–60s Failover Window: If a Multi-AZ primary database crashes, automated DNS failover repoints traffic to the synchronous standby in 30 to 60 seconds. During this window, link creation (
POST /shorten) fails, but link redirects (GET /{code}) continue resolving uninterrupted from the Redis Cache-Aside tier.Circuit Breakers: Application servers must set aggressive socket timeouts and trip circuit breakers on database calls. If Redis misses a cold link during a database outage, the server must fail fast rather than hanging on dead database TCP connections and exhausting worker thread pools.
Counter Recovery: Application servers lease blocks of IDs (e.g.,
INCRBY 1000) and treat remaining local tokens as ephemeral. If an app server crashes, discard its leased block. Discarding a few hundred IDs out of a 3.5-trillion Base62 namespace costs nothing and avoids complex distributed recovery locks.
Security · How do I prevent sequential ID enumeration?
Base62 is a positional base conversion, not encryption—incrementing an integer counter directly leaks predictable URLs (/1, /2, /3) that scrapers can iterate over.
Reversible Obfuscation: Pass the 64-bit integer through a lightweight, collision-free Feistel cipher or permutation tool (like Sqids/Hashids) before encoding to Base62. This scrambles sequential increments into pseudo-random alphanumeric tokens without risking collisions.
Edge Rate Limiting & 404 Heuristics: Enforce sliding-window rate limits at your API Gateway or reverse proxy (e.g., 60 lookups/min per IP). Track 404-to-200 response ratios at the WAF; clients scanning non-existent short codes will hit abnormal miss ratios and can be automatically blocked at the perimeter.
HMAC Signatures for Sensitive Links: For private or single-use URLs (such as password resets or invoices), append a cryptographic signature token (
/xK9b?sig=...) validated at the gateway before redirecting.
Scaling · When should I actually shard a database?
Never shard simply because an article mentions "billions of rows". 1 billion URLs at 500 bytes per row is only 500 GB, and 30 billion URLs is 15 TB—both fit comfortably on a single standard 64 TB AWS RDS volume or a 128 TB Aurora cluster without manual partitioning. Shard only when physical single-node thresholds break:
Write IOPS & WAL Flush Limits: When sustained write volume breaches ~20,000 to 40,000 writes/sec, where single-primary disk locks and write-ahead log (WAL) syncs become the hardware bottleneck.
Multi-Region Latency & Edge Distribution: When global users face 150ms+ cross-continental redirect latencies. Resolve this by deploying edge reverse proxies (Cloudflare/CloudFront) and multi-region read replicas (like Aurora Global Database) before attempting cross-region sharding.
About the author:
I'm a full-stack (frontend-leaning) Senior / Staff engineer. I'm currently exploring new engineering opportunities. If your team is tackling distributed systems challenges or needs someone who designs for production reality instead of interview buzzwords, connect with me on LinkedIn or check out my work on GitHub.
Top comments (0)