Building a Scalable Short‑URL Service with Redis and Java

Short URLs are popular in SMS and social media because they save characters, look tidy, enable analytics, and hide parameters; this article explains their benefits, the basic redirect workflow, and provides a detailed backend design—including storage choices, high‑concurrency strategies, distributed ID generation, and a Java‑Redis implementation.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Building a Scalable Short‑URL Service with Redis and Java

Why Short URLs?

Short URLs are used in SMS and other character‑limited platforms because they reduce length, look cleaner, enable traffic statistics and hide query parameters, improving user experience and security.

Basic Principle

The workflow consists of four steps: (1) a service maps a long URL to a short token, (2) the short URL is embedded in the message, (3) the user clicks the short URL, the browser follows a 301/302 redirect to the original long URL, and (4) the target content is displayed.

Service Design

Since a deterministic compression algorithm that guarantees a one‑to‑one mapping is impossible, the common approach is to use an incremental identifier generator (a “ticket‑issuer”). Each new long URL receives the next numeric ID, which is then encoded into a short string (e.g., www.x.cn/0, www.x.cn/1).

Mapping Storage

The ID‑to‑URL mapping must be persisted. For modest traffic a relational database such as MySQL can store the mapping with an auto‑increment primary key. The numeric ID can be stored either as a string (for exact‑match lookups) or as a decimal number (to save space and enable range queries).

One‑to‑One Mapping

Using only an auto‑increment key does not guarantee that the same long URL always receives the same short token; repeated requests generate different IDs. To achieve true one‑to‑one mapping you need an additional lookup table (e.g., a key‑value store) that maps the long URL to its existing ID, at the cost of extra memory.

Short URL Representation

Numeric IDs are usually converted to a higher base (commonly base‑32 or base‑64) to shorten the string. The conversion uses a custom digit set that includes 0‑9, A‑Z, a‑z, giving up to 62 characters per digit.

High Concurrency

Directly incrementing a MySQL counter for every request becomes a bottleneck under high QPS. Two common mitigations are caching and batch allocation.

Cache

Popular or recently accessed long URLs can be cached in an in‑memory store such as Redis. When a request hits the cache, the short URL is returned without a database round‑trip.

Batch Allocation

Instead of fetching a single ID each time, the service can request a block of IDs (e.g., 10 000) from the database, serve them from memory, and replenish the block when the remaining count falls below a threshold. Unused IDs are written back asynchronously, reducing constant DB load.

Distributed Architecture

The single ticket‑issuer is a single point of failure. A distributed solution can partition the ID space among multiple generators. For example, 1 000 instances could each handle a distinct suffix range (0‑999) and increment by 1 000, eliminating inter‑service coordination.

Implementation Example (Java + Redis)

The following Java utility demonstrates a practical implementation that uses Redis (via Jedis) for ID generation, mapping storage, and caching. It includes base‑conversion logic, cache lookup, and batch‑friendly ID handling.

package util;

import redis.clients.jedis.Jedis;

public class ShortURLUtil {
    private static final String SHORT_URL_KEY = "SHORT_URL_KEY";
    private static final String LOCALHOST = "http://localhost:4444/";
    private static final String SHORT_LONG_PREFIX = "short_long_prefix_";
    private static final String CACHE_KEY_PREFIX = "cache_key_prefix_";
    private static final int CACHE_SECONDS = 1 * 60 * 60;

    private final Jedis jedis;

    public ShortURLUtil(String redisConfig) {
        this.jedis = new Jedis(redisConfig);
    }

    /** Return a short URL for the given long URL using the specified base. */
    public String getShortURL(String longURL, Decimal decimal) {
        // 1. Check cache
        String cache = jedis.get(CACHE_KEY_PREFIX + longURL);
        if (cache != null) {
            return LOCALHOST + toOtherBaseString(Long.valueOf(cache), decimal.base);
        }

        // 2. Generate next ID
        long id = jedis.incr(SHORT_URL_KEY);

        // 3. Persist mapping (could be MySQL; here we store in Redis for demo)
        jedis.set(SHORT_LONG_PREFIX + id, longURL);

        // 4. Write cache entry
        jedis.setex(CACHE_KEY_PREFIX + longURL, CACHE_SECONDS, String.valueOf(id));

        // 5. Convert ID to chosen base and return short URL
        return LOCALHOST + toOtherBaseString(id, decimal.base);
    }

    /** Characters used for base conversion (0‑9, A‑Z, a‑z). */
    private static final char[] DIGITS = {
        '0','1','2','3','4','5','6','7','8','9',
        'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
        'Q','R','S','T','U','V','W','X','Y','Z',
        'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p',
        'q','r','s','t','u','v','w','x','y','z'
    };

    /** Convert a decimal number to a string in the given base (e.g., 32 or 64). */
    private String toOtherBaseString(long n, int base) {
        long num = n < 0 ? (2L * 0x7fffffff) + n + 2 : n;
        char[] buf = new char[32];
        int charPos = 32;
        while (num / base > 0) {
            buf[--charPos] = DIGITS[(int) (num % base)];
            num /= base;
        }
        buf[--charPos] = DIGITS[(int) (num % base)];
        return new String(buf, charPos, 32 - charPos);
    }

    /** Supported bases. */
    enum Decimal {
        D32(32), D64(64);
        final int base;
        Decimal(int base) { this.base = base; }
    }

    public static void main(String[] args) {
        ShortURLUtil util = new ShortURLUtil("localhost");
        System.out.println(util.getShortURL("www.baidudu.com", Decimal.D32));
        System.out.println(util.getShortURL("www.baidu.com", Decimal.D64));
    }
}

This example can be extended to use a relational database for persistent storage while keeping Redis for fast cache and ID generation. Batch allocation can be added by reserving a range of IDs with a single INCRBY command.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaredishigh concurrencydistributed systemshort URL
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.