A short link looks like a tiny database lookup until one shared post sends a flood of visitors to it. Adding servers helps only if they can find the same mappings and survive dependency failures. Meanwhile, abusive link creation can exhaust capacity, and a confirmation email can disappear after a successful save. We’ll follow s.example/aB7 through these problems and run its caching and request-limiting logic locally.

The core idea: separate responsibilities

A URL is an address for a resource, such as a web page. Our shortener needs an authoritative record—the destination mapping we trust—and helpers for specific bottlenecks.

Think of a library. A reception desk distributes visitors among librarians; a frequently consulted reference card avoids repeated catalog searches; a borrowing limit prevents one visitor from monopolizing resources. The catalog remains authoritative even when a reference card disappears.

In our service, a load balancer distributes requests among application servers. A cache stores reusable copies of data; Redis provides an in-memory store addressed by keys. A rate limiter restricts how many requests a caller may make over time.

These mechanisms solve different problems. Faster reads do not make writes durable, and more servers do not guarantee agreement.

Follow a short link through the system

Start with one database and interchangeable application servers:

Client -> Load balancer -> Application servers -> Redis
                                  |
                                  v
                         Database: links + outbox
                                  |
                                  v
                           Worker -> Email provider

The outbox is a database table holding notifications awaiting delivery.

  1. Distribute requests with load balancing

A round-robin policy sends successive requests to successive servers. Least-connections favors servers with fewer active connections, although connection counts do not perfectly measure work.

Keep servers stateless: no request should depend on information stored only in one server’s memory. Any server can resolve aB7 because mappings live in shared storage.

Use health checks—periodic probes that identify unhealthy servers—and allow active requests to finish before removing a server. The balancing layer also needs redundancy; otherwise, its failure disconnects every healthy application server.

  1. Limit creation and store the mapping

Alice submits https://example.com/tech-insights to create a short link. Authenticate her, then check her account’s creation allowance before writing.

A counter in shared Redis lets all application servers enforce the same allowance. A counter inside each server would multiply Alice’s effective limit as servers increase.

Generate a random code, enforce a uniqueness constraint—a database rule rejecting duplicate codes—and retry collisions. Codes must be long enough for the expected population; aB7 is illustrative, not a recommended production length.

Store the code, destination, owner, and creation time. Return the short URL after committing the database transaction—a group of changes that succeeds or fails together.

A short code is an identifier, not permission to access private content.

  1. Resolve links with Redis cache-aside

When someone visits s.example/aB7, use cache-aside: application code manages loading the cache.

  1. Read Redis key link:aB7.
  2. If present, use its destination.
  3. Otherwise, query the database.
  4. Store the result in Redis with a time to live (TTL), an expiration duration.
  5. Return a redirect through HTTP, the web’s request-and-response protocol, telling the browser which URL to visit.

A 60-second TTL reduces repeat database reads while limiting how long an ordinary cached value remains stored. Redis losing this entry affects performance, not the authoritative mapping.

Treat mappings as immutable initially. If destinations can change, commit the database update before deleting the cached entry. That ordering still permits a concurrent reader to refill Redis with an older value; immediate revocation needs authoritative checks or a protocol that prevents stale versions from being served.

Use temporary redirects for changeable links. Browsers may retain permanent redirects and bypass later service decisions.

  1. Deliver notifications without losing work

Alice should receive a confirmation email. Sending it before committing risks announcing a link that never exists; sending it afterward risks losing the email if the application crashes between those operations.

Write the link and its outbox row in the same transaction. A background worker, a process handling deferred work, reads pending rows, contacts the email provider, then records success.

The worker can crash after the provider accepts an email but before recording success. Retrying may send it twice.

Give each notification a stable identifier and use the provider’s idempotency feature, if available: repeating the same identified request produces one effect. Without provider support, local duplicate tracking cannot eliminate that crash window. Claiming “exactly once” delivery would overstate the guarantee.

  1. Scale the database: replication versus sharding

Replication copies the same records across database servers. The primary normally accepts writes; replicas maintain copies. Sharding divides records across separate databases, each called a shard.

Question Replication Sharding Where does aB7 live? In the primary and its replicas On the shard chosen for that code Main benefit More read capacity and recovery options Distributes storage and write load Main complication Copies may lag; replacing a failed primary needs coordination Routing, moving data, cross-shard operations What remains difficult? A single primary’s write capacity A viral link can overload one shard

With asynchronous replication, the primary acknowledges without waiting for replicas to apply the change. A replica may therefore report Alice’s newly created link as missing. Depending on durability settings, promoting a replica after failure can also lose acknowledged writes.

Send reads that must immediately observe creation to an authoritative source.

If sharding becomes necessary, route by the short code so identical codes reach the same shard. A uniqueness constraint there can reject duplicates. Independent constraints cannot guarantee global uniqueness when duplicate codes can land on different shards.

  1. Apply CAP to specific operations

The CAP theorem concerns consistency, availability, and network partitions.

Consistency here means linearizability: operations behave as if there were one current, ordered copy of the data. It does not mean ordinary database validation rules. Availability means every request reaching a non-failing node receives a successful response. A network partition prevents some nodes from communicating.

During a partition, a distributed service cannot guarantee both that consistency and that availability. “Always pick any two” is misleading: the difficult choice arises when communication breaks.

Suppose one region loses contact with the authoritative database. It can continue redirecting previously cached links, accepting that a recently revoked destination may still work. Or it can require a current permission check and reject redirects when that check is impossible.

Returning an unavailable response sacrifices CAP availability. Serving stale state sacrifices the consistency guarantee. Make the decision per operation: public redirects, security-sensitive revocations, and link creation can need different policies. A cache hit alone does not establish a system-wide CAP guarantee.

Hands-on: run the cache and rate limiter

Use Python 3.11 or later and Docker, which runs Redis in an isolated container. These commands assume a Linux or macOS shell, Python’s venv module for isolating dependencies, and Docker running.

docker run --rm -d --name techinsights-redis \
  -p 127.0.0.1:6379:6379 redis:7-alpine

python3 -m venv .venv
. .venv/bin/activate
python -m pip install redis

The port binding limits access to the local machine. Keep production Redis on private networks with authentication and encrypted connections. Pin tested container and dependency versions for deployments; these installation references can receive updates.

Save this as demo.py. SQLite is a database organized into tables that runs inside the application process. Its in-memory database here disappears when the program exits. The demonstration tests permission to create links, rather than implementing a web server or sending emails.

import sqlite3
import uuid

import redis

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE links (code TEXT PRIMARY KEY, url TEXT NOT NULL)")
db.execute("INSERT INTO links VALUES (?, ?)",
           ("aB7", "https://example.com/tech-insights"))
db.commit()

r = redis.Redis(host="127.0.0.1", port=6379,
                decode_responses=True,
                socket_connect_timeout=1, socket_timeout=1)
r.ping()
prefix = "ti-demo:" + uuid.uuid4().hex


def resolve(code):
    key = f"{prefix}:link:{code}"
    url = r.get(key)
    if url is not None:
        return "cache", url
    row = db.execute("SELECT url FROM links WHERE code = ?",
                     (code,)).fetchone()
    if row is None:
        return "database", None
    r.set(key, row[0], ex=60)
    return "database", row[0]


LIMIT = """
local count = redis.call('INCR', KEYS[1])
if count == 1 then
    redis.call('PEXPIRE', KEYS[1], ARGV[1])
end
return count
"""

for _ in range(2):
    source, url = resolve("aB7")
    print(f"{source}: {url}")

for attempt in range(1, 5):
    count = r.eval(LIMIT, 1, f"{prefix}:limit:alice", 60000)
    result = "allowed" if count <= 3 else "blocked"
    print(f"create {attempt}: {result}")

r.delete(f"{prefix}:link:aB7", f"{prefix}:limit:alice")
r.close()
db.close()

Run:

python demo.py

Expected output:

database: https://example.com/tech-insights
cache: https://example.com/tech-insights
create 1: allowed
create 2: allowed
create 3: allowed
create 4: blocked

The Lua script, a small program executed inside Redis, increments the counter and sets its expiration without other commands interleaving. Separate increment and expiration requests could leave a counter without an expiry if the client crashes between them.

This fixed-window limiter allows three attempts in a 60-second window starting with the first attempt. Rejected attempts do not extend the window. Requests clustered around its boundary can still produce a burst.

In a web service, return HTTP 429 Too Many Requests for rejection, with a Retry-After header indicating the wait. Redis failures raise errors in this demonstration; production needs an explicit policy.

Stop the temporary Redis instance afterward:

docker stop techinsights-redis

Choose the complexity you need

Add mechanisms when measurements identify a bottleneck or a reliability requirement.

An index is a database structure that accelerates selected lookups while adding storage and write work.

Mechanism Use when Avoid or reconsider when Alternative Load balancing Several servers must share traffic or survive failures A small service has no measured need for a server fleet One application server Redis cache-aside Repeated reads dominate and bounded staleness is acceptable Reads are rarely repeated or must always reflect current state Indexed database reads Fixed-window limiting Simple request allowances are sufficient Boundary bursts exceed safe capacity A token bucket: capacity refills over time, permitting bounded bursts Replication Read capacity or recovery requirements justify copies You expect it alone to split primary writes Tune writes or consider sharding Sharding One database cannot meet measured storage or write needs One database remains sufficient Improve queries, indexes, and capacity

Cache-aside is not the only caching strategy. Write-through updates the database and cache before acknowledging Alice’s destination change, adding coordination and response time. Write-behind acknowledges the change before persisting it to the database; without durable safeguards, a crash can lose accepted changes. Keeping URL creation authoritative in the database is simpler.

Common mistakes and fixes

  1. Treating Redis as disposable everywhere. Losing cached destinations differs from losing limit counters. Define failure behavior separately; allowing creation during limiter failure permits abuse. Asynchronous Redis replication may also lose recent counters when replacing a failed primary.
  2. Overwhelming the database after cache loss. Limit concurrent fallback reads and combine simultaneous requests for the same missing key into one database fetch.
  3. Counting only by client address. Shared networks can group unrelated users. Prefer authenticated account limits, and trust forwarded address headers only from known proxies.
  4. Ignoring missing-link traffic. Briefly cache confirmed absent codes to reduce repeated lookups; never turn database errors into “link not found” responses.
  5. Assuming valid URLs are safe. Allow intended schemes such as HTTPS. Address phishing separately; if fetching previews, block access to internal services and private addresses.
  6. Logging entire destinations. Query parameters can contain secrets. Remove sensitive values from logs and restrict access to stored destinations.

Quick recap

· Load balancing spreads application requests; it does not remove database bottlenecks. · Redis cache-aside accelerates reads while the database remains authoritative. · Atomic counters enforce shared allowances, with explicit boundary and failure tradeoffs. · Replication copies data; sharding divides it and adds routing complexity. · CAP choices depend on operations; notification retries need duplicate-safe effects.

Test yourself

  1. Why can a replica miss a newly created link? With asynchronous replication, the primary can acknowledge before the replica applies the write. Read from an authoritative source when immediate visibility matters.
  2. Why combine increment and expiration in one Redis script? A client crash between separate commands could leave a counter without expiration. The script prevents other commands from interleaving between those operations.
  3. Can an isolated region guarantee current revocations and always redirect? No. If it cannot confirm current state, it must either risk serving a revoked mapping or reject some requests, sacrificing availability.

Where to go next

· Consistent hashing: routing keys across servers while reducing movement when server membership changes. · Backpressure: slowing incoming work when downstream capacity is exhausted. · Observability: using measurements, event logs, and request traces to investigate response time, cache behavior, replication lag, and notification retries.