September 2, 2026

When designing IDs that a person may need to read, copy, type, or pass to someone else, usability matters as much as uniqueness. A lookup ID may have to be dictated over the phone, written down, or entered by hand. For arbitrary machine-generated lookup IDs, numbers are a particularly practical format for human communication. People already handle long numbers: credit card numbers are routinely read, copied, and dictated despite containing sixteen digits.

Numbers are also easier to speak than arbitrary letters and symbols. There is no need to distinguish “B” from “D,” explain capitalization, or say whether a character is the letter “O” or the digit “0.” A number can be read aloud.

For this reason we use a twenty-digit numeric lookup ID, written in four groups of five digits. The grouping makes the number easier to scan and check, following the same general principle as a credit card:

12505-89847-63568-88524

A lookup ID such as this looks like nothing more than a random string of digits. But the number itself can carry a small amount of hidden information. If designed correctly, this eliminates the need for the server to maintain a separate table mapping every ID to metadata.

The embedded information, which we will call a hint, can be recovered directly from the number using simple arithmetic. The hint might identify the server that stores the data associated with the ID, or it might indicate which version of a protocol the client should use when processing it. In either case, the ID remains self-contained: it is generated as a number and later interpreted mathematically, without consulting a directory of special cases.

The ID is a 20-digit integer, which we will call N. The hint is represented by an integer H, and the total number of possible hints determines a modulus M. The ID is constructed as:

N = K × M + H

where K is chosen randomly and N is kept within the 20-digit range [1019, 1020).

Recovering the hint requires only one operation:

H = N mod M

Consider a hint of up to two alphanumeric characters, such as “s1,” “ny,” or “v2,” with letter case ignored. Using a–z and 0–9 gives 36 symbols. Including the empty value, the number of possible hints is:

360 + 361 + 362 = 1,333

The modulus is therefore M = 1,333. The hint is encoded as a base-36 integer using the character ordering defined by the system.

A 20-digit number in the range [1019, 1020) contains roughly 66 bits of information. The 1,333 possible hints require about 10 bits, leaving approximately 56 bits of leftover randomness—enough to make accidental collisions extremely unlikely in ordinary use.

Consider the hint “s1,” encoded as 712. One ID in that residue class is:

12505-89847-63568-88524

Removing the dashes and applying the modulus gives:

12505898476356888524 % 1333 = 712

Nothing in the visible ID reveals this information. There is no prefix, suffix, or other marker identifying a server or protocol version; the mapping between remainders and their meanings remains part of the company’s implementation. This provides a useful separation between the public interface and the internal system. The user needs only the ID required to retrieve the data, while routing, infrastructure, and processing details remain encapsulated behind it. The company can change those internal details without changing the format of the ID or exposing its operations through it.

If two characters are not enough, the hint can be lengthened. A larger hint space leaves fewer random IDs per residue, so collisions become more likely; at generation time the generator should check that the ID is unique. That check is acceptable. The generator already has full context. The client has none, and that is why the ID must carry a hint: on lookup, the client needs to know how to proceed. A five-character hint gives much more freedom than two. Two characters is the conservative case shown here, and it is immediately suitable for production.

Sample implementation

The following is an implementation in Python. The constants define the alphabet, the two-character hint space, and the 20-digit ID range:

ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"
ALPHABET_SIZE = len(ALPHABET)
MAX_HINT_LENGTH = 2
MODULUS = sum(ALPHABET_SIZE**n for n in range(MAX_HINT_LENGTH + 1))  # 1,333
MIN_ID = 10**19
MAX_ID = 10**20 - 1

A hint of at most two characters becomes an integer in 0 … MODULUS−1. Empty is 0; each character contributes its 1-based index. decode_hint converts that integer back to a string. extract_hint_from_id takes the ID as a string, strips dashes, reduces it modulo MODULUS, and calls decode_hint:

def encode_hint(hint):
    hint = hint.lower()
    if not hint:
        return 0
    value = 0
    for char in hint:
        value = value * ALPHABET_SIZE + (ALPHABET.index(char) + 1)
    return value


def decode_hint(value):
    if value == 0:
        return ""
    chars = []
    while value > 0:
        value, remainder = divmod(value - 1, ALPHABET_SIZE)
        chars.append(ALPHABET[remainder])
    return "".join(reversed(chars))


def extract_hint_from_id(id):
    return decode_hint(int(id.replace("-", "")) % MODULUS)

An ID is a random 20-digit N with that residue:

import secrets

def generate_numeric_id_with_hint(hint):
    h = encode_hint(hint)
    k_min = (MIN_ID - h + MODULUS - 1) // MODULUS
    k_max = (MAX_ID - h) // MODULUS
    k = secrets.randbelow(k_max - k_min + 1) + k_min
    return k * MODULUS + h
extract_hint_from_id("12505-89847-63568-88524")  # 's1'

Keep reading

  1. Shoulder Surfing QR Codes: A Defense

    August 19, 2026

    QR codes are a convenient way to pair a mobile device with a desktop application or a web application running on the desktop: the desktop displays a code, and the phone scans it. …

    Continue reading

Entelechy, (from Greek entelecheia), in philosophy, that which realizes or makes actual what is otherwise merely potential. — Encyclopedia Britannica