Module 3: Data Model and Short Code Generation

7. Choosing the strategy

Description

You have the three strategies implemented and measured: hash (lesson 4), counter + base62 and random + verification (lesson 5), with the base62 conversion mastered (lesson 6). This lesson is where you decide. Not with an opinion, but with a table: each strategy against the criteria that really matter to Enlace, with the trade-off of each cell backed by a number you already measured. By the end you'll be able to make the decision and —what separates an engineer from a recipe-repeater— defend it by saying what would change it.

The good news that already appeared: at Enlace's scale, none of the three is a catastrophe; the 62⁷ space is so large against the demand that all of them work. So "choosing" isn't discarding two broken options, but ordering three viable options according to which criterion weighs most for this product. That's the kind of decision that appears in system-design interviews and real design docs, and the muscle this lesson trains.

Connection to the module: this lesson brings the previous three together into a decision, and builds two bridges. Backward, it revisits the store choice (lesson 3): the strategy and the store talk to each other —random + verification needs the conditional "only if it doesn't exist" write that both SQL and KV offer cheaply—. Forward, it marks the boundary with module 5: the counter's coordination cost is resolved with sharding (splitting the counter), which is that module's topic. And it prepares lesson 8, where you implement the chosen strategy in a generator that runs.

Choosing a camera, not finding "the best camera"

No sensible person asks "what's the best camera in the world?" and expects a single answer. The good question is "the best camera for what?": for traveling light, the phone's wins; for photographing birds at 200 meters, an enormous telephoto lens; for a studio, something else. The "best" depends on which criterion weighs —portability, reach, control—, and choosing well is making those criteria explicit and ordering them, not looking for an absolute winner.

Choosing the generation strategy is identical. There's no "best ID strategy"; there's the best for Enlace's criteria, ordered by importance. So the first step isn't to look at the strategies, but to name the criteria. For a shortener, there are four:

  1. Guaranteed no collisions. Two links must never share a code; if it happens, one user steps on another's link. It's the non-negotiable criterion.
  2. Not trivially guessable. That from one code you can't deduce the next, so a competitor can't enumerate the database or infer the business volume.
  3. Write-scalable. That generating codes doesn't depend on a single component that serializes all the writes and is a single point of failure.
  4. Short code. It's what a shortener sells; 7 characters, not 15.

With the criteria on the table, the table fills almost on its own, because each strategy already showed you its face and its cost.

The decision table

Here are the three strategies against the four criteria. Each cell summarizes what you measured or reasoned in lessons 4 to 6:

CriterionHash of the URLCounter + base62Random + verification
No collisions❌ Collides (29.8% at 16 bits; at Enlace's scale it clashes for sure) → needs verificationImpossible by construction (each number is handed out once)⚠️ Can clash, but 0.17% with 6 billion taken → cheap verification (~1 retry/587)
Not guessable⚠️ Predictable from the URL (anyone hashes it)Sequential (4c92,4c93,4c94) → enumerable; mitigated with permutationRandom: from one code you can't deduce another
Write-scalable✅ No central counter (but + 1 verification read)Global counter = single coordination point → sharding (M5)✅ No central counter (each server throws on its own; + 1 conditional write)
Short code✅ 7 chars (truncating)✅ 7 chars (with offset)✅ 7 chars by design
Extra property"same URL → same code" (sometimes useful, sometimes a privacy leak)Insertion order recoverable from the codeNo code↔data correlation

Read the table by columns, which is how a decision is read. The hash has a serious problem in criterion 1 (it collides at scale, it has to verify) and another in criterion 2 (predictable from the URL); its only clean advantage —"same URL, same code"— is often a leak, not a virtue (lesson 4). The counter is perfect in criterion 1 (zero collisions, free) but fails criteria 2 and 3 (guessable and central coordination), both mitigable but with extra work (permutation, sharding). The random + verification isn't perfect in any criterion but is good in all four: it barely collides (0.17%, and the verification covers it), it isn't guessable, it doesn't need a central counter, and it gives short codes. It's the option with no sharp corners.

Notice a revealing pattern: the collision-free strategies (counter and random) pay for their guarantee in opposite ways. The counter pays for it with coordination (a single dispenser everyone consults) and gets it free in computation (it verifies nothing). The random one pays for it with verification (a check per write) and gets it free in coordination (nobody consults anybody). It's a classic system-design trade: central coordination vs distributed verification. Which you prefer depends on whether a single point of failure hurts you more (choose random) or a check per write (choose counter).

In practice, they combine

The table presents the pure strategies, but real systems almost always combine to cover the holes. The two most common combinations for a shortener:

Permuted counter. You take the counter (guaranteed zero collisions, criterion 1 perfect) and pass it through a bijective permutation before encoding in base62 (covers criterion 2, it stops being guessable). You get a collision-free generator with random-looking codes. Criterion 3 (coordination) is still pending and is resolved, on scaling, with the counter's sharding (M5). Many serious shorteners use exactly this: the counter's uniqueness is so valuable that it's worth dragging its coordination cost.

Random + unique index. You generate 7 random characters (criteria 2, 3, and 4 good from the start) and let the store do the verification for you: you insert with the conditional "only if it doesn't exist" write (SQL's UNIQUE/PRIMARY KEY constraint or a KV's SET NX, lesson 3), and if it clashes —0.17% of the time— you retry with another random one. The store gives you criterion 1 atomically and without a race condition. This combination is the simplest to operate because it drags no coordination component: any server generates and writes, and the unique index is the arbiter.

flowchart TD
    subgraph A["Permuted counter"]
        A1["counter++ (unique)"] --> A2["permute (scramble)"]
        A2 --> A3["base62_encode → 7 chars"]
        A3 --> A4["store (no verification: doesn't collide)"]
    end
    subgraph B["Random + unique index"]
        B1["7 random chars"] --> B2["INSERT ... if it doesn't exist"]
        B2 -->|"ok (99.83%)"| B3["done"]
        B2 -->|"clashes (0.17%)"| B1
    end

Which to choose for Enlace? Here's a defensible recommendation, and again the important thing is the reasoning, not the verdict:

For Enlace at today's scale, random + unique index is the cleanest choice. Reasons, in order: (1) it drags no global counter, so it avoids the single coordination point from day one —any write server generates codes without asking permission—; (2) the codes aren't guessable, an important criterion for a public shortener; (3) the verification cost that in theory penalizes it turned out to be 0.17% retry (you measured it), that is, practically nonexistent; and (4) the store does the uniqueness verification atomically with a constraint you already have for free (lesson 3). The only criterion where the counter beats it —zero verification computation— doesn't matter at 40 writes per second.

The permuted counter is just as valid, and would be my choice if Enlace needed recoverable IDs or a strict insertion order. Its cost (coordination) is real but deferred: at 40 writes/sec an atomic counter is enough, and when scaling is needed, the counter's sharding (M5) is a known solution. It's not a wrong decision; it's a decision with a different future cost.

The pure hash I wouldn't choose for a public shortener's short_code: it collides at scale (forcing verification anyway, so it loses its supposed simplicity) and makes the code predictable from the URL. Its niche —"same URL, same code"— is better resolved with an explicit deduplication table (lesson 4) than as a side effect of the algorithm.

Notice the shape of the three sentences: each names the choice and its condition. "Random, because it avoids coordination and the verification cost is 0.17%." "Counter, if it needed recoverable IDs." "Not hash, because it collides and is predictable." A system-design decision without its conditions is a dogma; with them, it's engineering.

What would change the decision

An adult decision includes the switches that would flip it. These are the main ones for Enlace's generation strategy:

  • If Enlace needed to deduplicate identical URLs (that the same long URL always give the same short code), the hash comes back to the table —or, better, an explicit long_url → short_code table consulted before generating—. Deduplication is a product decision, not an algorithm one.
  • If the write volume grew a lot (from 40 to tens of thousands per second), the single global counter would become a bottleneck and it would have to be sharded (M5) or migrated to random + verification (which already scales without a counter). Random + verification is more "growth-proof" in writing.
  • If the code space started to fill up (which Enlace doesn't come close to: it uses 0.17% in 5 years), the random one's retry rate would shoot up (lesson 5, exercise 2) and the code would have to be lengthened or migrated to the counter (which doesn't depend on finding free holes). The counter is more robust when the space is almost full.
  • If predictability stopped mattering (an internal shortener, not public), the counter without permuting would be the simplest of all: zero collisions, zero verification, and you don't care that it's sequential.

That last point is important: the "correct" choice for public-Enlace isn't the correct one for every shortener. An internal service that shortens catalog URLs can use a pure counter and be perfect. Criterion 2 (not guessable) is what separates the two cases, and that's why it matters to name it.

Common mistakes

Looking for "the best strategy" in the abstract. What happens: someone asks or answers "what's the best way to generate IDs?" as if there were one, and adopts the one that sounded most sophisticated in the last blog they read. Why it happens: they skip the step of naming the criteria and ordering them by the concrete product. How to detect it: if your answer to "what strategy would you use?" doesn't start by naming the criteria (no collisions, not guessable, scalable, short) and which weighs most, you're giving a dogma. How to fix it: name the criteria first, put them in a table, and let the table —not the fashion— point to the strategy. The same question has different answers for a public shortener and an internal one, and that's fine.

Choosing the counter and forgetting its two mitigations. What happens: someone chooses counter + base62 for its zero-collisions and launches it as-is: sequential codes 1, 2, 3 (guessable and amateur-looking) and a single counter that becomes a bottleneck on growth. Why it happens: they see the advantage (uniqueness) and not the two costs (predictability, coordination) that have to be actively mitigated. How to detect it: if your counter design doesn't mention either permutation/offset or a sharding plan, it's incomplete. How to fix it: the pure counter is almost never used in public production; you use a permuted counter (for the predictability) with a counter-scaling plan (for the coordination). Choosing the counter is accepting those two tasks, not just its advantage.

Discarding random + verification out of fear of retries. What happens: someone rejects it thinking "doing a verification per write and risking retries is expensive", without having computed the real rate. Why it happens: the intuition about collisions in a large space is bad (lesson 5); "can clash" feels like "clashes often". How to detect it: if you reject the strategy without citing its retry rate against the real space, you're guessing. How to fix it: remember the number you measured —0.17% with 6 billion taken, ~1 retry every 587 insertions— and that the verification is the same UNIQUE constraint you already have from having short_code as the key. For a shortener with a space 587× larger than its demand, random + verification is usually the cleanest option, not the most expensive.

Exercises

Exercise 1 — Fill the table for a different case. A bank needs to generate transaction identifiers. Its criteria, in order: (1) guaranteed no collisions, (2) auditable/orderable in time (being able to know which transaction came before), (3) it doesn't matter that it's guessable (they're internal), (4) the length doesn't matter much. Which strategy would you choose and why? Is it the same as for Enlace?

See solution

For the bank, counter + base62 (or a pure counter, even without permuting) is the best choice, and it's not the same as for Enlace. The reasoning follows the table, but with reordered criteria:

  • Criterion 1 (no collisions): the counter gives it free and guaranteed —critical for a transaction identifier, where a collision is an accounting disaster—.
  • Criterion 2 (orderable in time): the counter is naturally monotonic, so a larger ID came later —exactly what the audit needs—. The random one does not give order (two random IDs don't say which came before), so here the random one loses.
  • Criterion 3 (guessable doesn't matter): since they're internal, the counter's sequentiality —which for Enlace was a problem— is irrelevant here or even useful. No need to permute.
  • Criterion 4 (length): doesn't weigh.

The lesson: the counter, which for public-Enlace came in second because of its predictability, is the winner for the bank because its criteria are different (the temporal order matters, the predictability doesn't). Same table, different criteria, different decision. That's choosing by criteria and not by dogma.

Exercise 2 — Justify the verification cost. Someone objects: "Random + verification does an extra query per write; that's inefficient". Answer with concrete Enlace numbers: how many verifications per second does it imply, how many retries does it add, and why is that cost acceptable given the rest of the system?

See solution

Enlace does ~40 writes per second (module 2). Random + verification implies one conditional write ("insert if it doesn't exist") per each one, that is ~40 verifications per second —a trivial load for any store, which handles thousands of operations by key per second (lesson 3)—. The retries: with 6 billion taken in the worst case, the clash rate is 0.17%, so of those ~40 writes/sec, on average one every ~15 seconds (40 × 0.0017 ≈ 0.068 per second) needs a second attempt; the rest hit on the first try.

Why it's acceptable: (a) the verification isn't a separate extra query —it's the same insertion operation, which already has to pass through the UNIQUE constraint on short_code anyway (lesson 3), so the "extra cost" is almost zero—; (b) 40 op/sec is negligible against the ~4,000 reads/sec the system already handles (with a cache, module 4); and (c) in exchange for that tiny cost, you avoid the counter's single coordination point. The trade —a very cheap verification per write in exchange for having no central counter— is clearly favorable at Enlace's scale. The objection confuses "does a verification" with "is expensive"; the numbers say it isn't.

Exercise 3 — The switch that changes the decision. For each hypothetical change in Enlace, say whether it would change the recommended strategy (random + unique index) and to which one, with a reason. (a) Enlace becomes an internal tool for a single company, with no external users. (b) Enlace needs "shortening the same URL twice to return the same short code". (c) Enlace grows to 50,000 writes per second.

See solution
  • (a) Internal, no external users → could change to a pure counter. If no external party sees the codes, the "not guessable" criterion stops mattering, and the counter without permuting becomes the simplest option: zero collisions, zero verification, zero permutation. The reason we discarded the counter for public-Enlace (predictability) disappears. Justified change.
  • (b) Deduplicate identical URLs → add a deduplication table, don't change the generator. The clean way isn't to go back to the hash (which collides), but to consult a long_url → short_code table before generating: if the URL already has a code, it's reused; if not, it's generated with random + unique index as always. It's a layer on top, not a replacement of the strategy. Deduplication is a product decision.
  • (c) 50,000 writes/sec → reinforces random, discards the single counter. At that scale, a single global counter would be a severe bottleneck and single point of failure. Random + verification already scales without a counter (any server generates on its own), so the recommendation doesn't change —in fact, it's confirmed—. If we were on the counter, this would be the moment to shard it (M5) or migrate to random. The write growth favors exactly the strategy we already chose.

The lesson: identifying in advance which change would flip the decision —and which only adds a layer— is what makes the choice robust. The recommendation isn't "random always"; it's "random for public-Enlace at this scale, and this is what would make me reconsider".

Summary and next step

You decided, with method. You named Enlace's four criteria —no collisions, not guessable, write-scalable, short code— and put the three strategies in a table, each cell backed by a measured number: the hash collides (29.8% at 16 bits) and is predictable; the counter is zero-collisions but sequential and with central coordination; the random + verification is good in all four (0.17% retry, no counter, not guessable, short). You saw that in practice they combine —permuted counter or random + unique index— and that the cleanest choice for public-Enlace today is random + unique index, because it avoids the single coordination point from day one and its verification cost turned out to be insignificant. And, above all, you expressed the decision with its conditions: what would change it (internal → pure counter; deduplication → separate table; more writes → confirms random or shard the counter).

Before moving on you should be able to: recite the four criteria; fill the decision table from memory with the trade-off of each cell; recommend a strategy for Enlace with its reason and its condition; and show how the decision changes for a different case (bank, internal shortener).

What comes next is building. Lesson 8, the module's project, puts you to implement the complete id_generator with the chosen strategy —random + verification against the db, with the base62 from lesson 6 and the expires_at handling from lesson 2's record— and run it. You deliver the executable code, the real output (round-trip and a batch of unique codes), and the written justification of why you chose that strategy. It's where the module's seven lessons become a component that works.

Resources