Module 8: Project — Design Enlace End to End

5. Step 4a — The write path

Description

With the blueprint drawn (lesson 4), step 4 of the framework begins: the deep dive. And we deep-dive first into the path the diagram showed short and calm —the write one, shorten— for two reasons. First, because it is the simplest, and starting with the simple keeps your muscle warm for the read path (lesson 6) and the distributed scaling (lesson 7), which are denser. Second, because in it lives one of Enlace's most beautiful decisions: how to generate the short_code —the unique, short identifier that is the heart of the problem—. You are going to go down from the diagram's "generate 7 base62 characters" to the concrete mechanism: a global counter + base62_encode, why that combination produces no collisions, and why 7 characters are more than enough for the 6,000 million records (the calculation 62⁷ vs the demand, executed).

In this lesson you go through shorten end to end over the distributed architecture: the long_url arrives, the stateless server generates the short_code, and writes it to the primary of the shard it falls into. You are going to execute base62_encode to confirm it produces the correct codes, reproduce the anchor 62⁷ = 3,521,614,606,208 with 0.17% used, and understand why Enlace's write path (~40/s) is the one that will never be the bottleneck —the luxury that lets you concentrate all the effort on the read—.

Connection to the module: this is the first of the three "deep dive" lessons of the capstone (5: write, 6: read, 7: distributed scaling). It takes the box of the diagram that says "write → router → primary" and opens it from the inside. It leans on the data model and ID generation of module 3 (the Link record, base62) and on the sharding of module 5 (which primary the write goes to). And it prepares lesson 7, where the distribution of the writes among shards with consistent hashing is closed. The border: the global uniqueness of the counter at scale brushes against distributed design; we solve it with judgment and mark where the sibling guides would deep-dive.

The numbered-ticket dispenser

Think of it this way. In a store with lots of customers, so that nobody argues about their turn, there is a ticket dispenser: each customer who arrives tears off a slip with a number, and the numbers come out in strict order —1, 2, 3, 4…—, without a single repeat. The dispenser has one rule: it keeps count of the last number it gave, and to the next customer it hands that number plus one. Since there is one dispenser and it advances one at a time, it is impossible for two customers to receive the same number. There is no need to verify, no need to compare against the tickets already given, no risk of clash: the mechanism guarantees uniqueness by construction, not by review.

Notice the elegance. Compare it with the alternative: a dispenser that gives each customer a random number between 1 and a million. That one can repeat —two customers could draw the same random—, so it would have to keep a list of the ones already given and verify each new one against it ("has this 7,342 come out yet? yes → draw another"). The sequential dispenser needs none of that: the "last + 1" is unique for free. The only thing the sequential dispenser does need is to be a single one —or coordinated, if there are several—, because if there were two independent dispensers, both could go for ticket 43 at the same time.

Enlace's short_code generator is that dispenser. A global counter keeps count of the last ID handed out, and each shorten takes "the last + 1" —a number unique by construction—. Then that number is converted to a short string with base62_encode (1,000,000 becomes '4c92'), which is the one the user sees. Since the counter advances one at a time and never goes backward, two different URLs never receive the same code: uniqueness is guaranteed by the mechanism, without verifying collisions, without comparing against what is already saved. It is the numbered-ticket strategy, and its only requirement —the counter is a single one, or coordinated— is a known problem with known solutions.

It is worth spelling it out:

Enlace's short_code is generated with a global counter (the last ID + 1) converted to base62. The counter guarantees uniqueness by construction —like the numbered tickets—, without verifying collisions. The base62 conversion makes it short: 7 characters give 62⁷ ≈ 3.5 trillion codes, more than enough for Enlace's 6,000 million in 5 years.

The path of shorten, end to end

Let us go through the write over the distributed architecture, step by step, as a sequence:

sequenceDiagram
    participant C as Client
    participant LB as Load balancer
    participant S as Server (stateless)
    participant ID as Global counter
    participant R as Shard router
    participant P as Shard primary
    C->>LB: POST /shorten { long_url }
    LB->>S: route to a free server
    S->>ID: give me the next id
    ID-->>S: id = 1000000
    S->>S: short_code = base62_encode(1000000) = "4c92"
    S->>R: where does short_code "4c92" go?
    R-->>S: shard k (consistent hashing)
    S->>P: INSERT Link(short_code, long_url, created_at, ...)
    P-->>S: OK (replicates to the replicas via the log)
    S-->>C: 201 { short_code: "4c92" }

Read it step by step: (1) the client sends the long_url; (2) the load balancer routes it to any stateless server (any one serves, there is no stuck session); (3) the server asks the global counter for the next id —say 1,000,000—; (4) it converts it to short_code with base62_encode'4c92'; (5) the shard router tells it which shard that code belongs to (consistent hashing over the short_code); (6) the server inserts the Link record into the primary of that shard; (7) the primary confirms and copies the change to its replicas via the replication log; (8) the server returns the code to the client. A single write to a single primary per shortening. With ~40 writes/s distributed among the shards, each primary receives a handful per second: a walk in the park.

Notice where each piece of the lesson 4 diagram lives in this flow: the load balancer distributes, the stateless server runs the logic, the counter gives the uniqueness, the router does the sharding, the primary persists. The write path uses the whole architecture, but stresses it little —because it is only ~40/s—.

The code generation, executed

Let us go down to the heart: base62_encode over the counter's number. It is a base change —divide by 62 and keep the remainders—, and it produces the short string the user sees. Let us run the generator and confirm it produces the correct codes, without quoting them from memory:

# id_generator.py — Enlace's counter + base62, executed.
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
BASE = len(ALPHABET)   # 62


def base62_encode(n: int) -> str:
    """Convert a non-negative integer into its base62 string."""
    if n == 0:
        return ALPHABET[0]
    chars = []
    while n > 0:
        n, rem = divmod(n, BASE)
        chars.append(ALPHABET[rem])
    return "".join(reversed(chars))


# A simple id_generator: the counter that advances one at a time.
class IdGenerator:
    def __init__(self, start=0):
        self._counter = start

    def next_code(self) -> str:
        self._counter += 1
        return base62_encode(self._counter)


# --- We confirm the codes and the space anchor ---
print("encode(1)       =", repr(base62_encode(1)))
print("encode(62)      =", repr(base62_encode(62)))
print("encode(1000000) =", repr(base62_encode(1000000)))
print("encode(62**7-1) =", repr(base62_encode(62**7 - 1)))

gen = IdGenerator(start=999_999)
print("three codes in a row:", [gen.next_code() for _ in range(3)])

supply = 62 ** 7
demand = 100_000_000 * 12 * 5      # 6,000 million at 5 years
print(f"\n62^7 (space)    = {supply:,}")
print(f"demand 5 years  = {demand:,}")
print(f"fraction used   = {demand/supply:.4%}")
for length in range(6, 9):
    print(f"  {length} chars: 62^{length} = {62**length:>18,}  "
          f"{'ENOUGH' if 62**length >= demand else 'short'}")

What to expect. Running python id_generator.py with Python 3.14.0:

encode(1)       = '1'
encode(62)      = '10'
encode(1000000) = '4c92'
encode(62**7-1) = 'ZZZZZZZ'
three codes in a row: ['4c9c', '4c9d', '4c9e']

62^7 (space)    = 3,521,614,606,208
demand 5 years  = 6,000,000,000
fraction used   = 0.1704%
  6 chars: 62^6 =         56,800,235,584  ENOUGH
  7 chars: 62^7 =      3,521,614,606,208  ENOUGH
  8 chars: 62^8 =    218,340,105,584,896  ENOUGH

Let us go part by part. encode(1) = '1', encode(62) = '10' (the "carry" to the second position, like in base 10 when going from 9 to 10), encode(1000000) = '4c92' (four characters for what in base 10 are seven digits), and encode(62⁷-1) = 'ZZZZZZZ' (the largest code that fits in 7 characters, seven Zs). The "three codes in a row" from the generator —'4c9c', '4c9d', '4c9e'— show the ticket property: consecutive, unique, without collision verification. The counter advanced from 1,000,000 to 1,000,002 and the codes came out in order.

And the anchor, executed: 62⁷ = 3,521,614,606,208, the 5-year demand is 6,000,000,000, and the fraction used is 0.1704%. Three and a half trillion possible codes, of which Enlace will use less than two thousandths in five years. That enormous slack is what justifies "7 characters": with 6 it would just barely reach (you would use >10% of the space, with no margin to grow); with 8 you would waste brevity (one character more than needed, and a shortener that lengthens its codes for free wastes what it sells); 7 is the sweet spot —a factor of ~587× over the demand for the price of one character more than the tight minimum—.

The distributed detail: one counter, many servers

There is an honest asterisk the capstone has to address, because it is born precisely from the distributed architecture of lesson 4: if there are several stateless servers generating codes, how do they share a single counter without clashing? It is the single-dispenser requirement taken to a system with many registers. Three defensible answers, from the simplest to the most scalable:

  • A centralized counter service. A component (or the database itself, with a sequence/AUTO_INCREMENT) hands out the ids, one by one, to whoever asks. Simple and correct, but it is a coordination point —and at ~40 writes/s, a central counter serves them asleep, so for Enlace it suffices—. The "bottleneck" a central counter could be only appears at write scales much larger than Enlace's.
  • Pre-assigned ranges per server. The central counter hands out blocks (server A takes ids 1–1000, B takes 1001–2000), and each server spends its local block without asking again until it runs out. It reduces the coordination to once every 1000 writes. It is the technique real systems use to take pressure off the counter.
  • Distributed IDs without a central counter (Snowflake-style): each server generates unique ids by combining timestamp + machine id + local sequence, without coordinating with anyone. It scales infinitely, but it is more complex and the ids are no longer a clean one-at-a-time counter. For Enlace it is over-engineering —we mention it because it is the path when the writes are indeed the bottleneck—.

For Enlace's capstone, the defensible choice is the first (central counter, or database sequence) for its simplicity, and the second (ranges) as an optimization ready if it were ever needed. And here is the border with the ecosystem: the in-depth design of a distributed id generator with global uniqueness guarantees under failures and network partitions is territory that brushes against event-driven-architecture-guide and the distributed systems guides. In this guide it is enough for us to know that, at ~40 writes/s, uniqueness is a problem solved with a simple counter, and that there is a growth path (ranges → Snowflake) if the scale were to change. We do not deep-dive further because the numbers do not ask for it.

Why the write is the easy path

Let us close with the reason we started here: Enlace's write path is, and always will be, the calm one. The capacity table said so: ~40 writes/s. Translated to the distributed design: those ~40/s are distributed among the shards, so each primary receives a handful of inserts per second —a ridiculous load for any modern database, which handles thousands of writes/s without breaking a sweat—. There is no need to cache the writes (it makes no sense to cache something that happens 40 times/s), no need to load-balance the write with special care, no contention over the same rows (each INSERT is a new record, not an UPDATE over an existing one).

That luxury —the calm write— is exactly what the scope decision of step 1 protected. Remember why we deferred analytics? Because incrementing clicks on every resolve would turn each read into a write, and the writes would jump from ~40/s to ~4,040/s —a hundred times more—, contaminating this calm path with contention over counters. By keeping analytics out of the v1, Enlace preserves its easy write, and all the design energy can go where it really pinches: the read path, which is the next lesson.

Common mistakes

Using a hash of the URL as the short_code without thinking about collisions. What happens: someone decides to generate the code as hash(long_url) truncated to 7 characters, because it seems elegant (the same URL gives the same code). But truncating a hash reintroduces collisions: two different URLs can give the same truncated code (the birthday problem), and then one overwrites the other or you have to verify and retry on every write. Why it happens: the hash feels deterministic and "free". How to spot it: if your generator can produce the same code for two different inputs, you have collisions. How to fix it: the counter + base62 guarantees uniqueness by construction —the "last + 1" never repeats—, without verifying anything. If you need the same URL to always give the same code (deduplication), that is another design decision (an index over long_url), not a reason to hash the code.

Forgetting that a single counter needs coordination between servers. What happens: someone draws several stateless servers, each with its own local counter starting at 0, and as soon as two servers handle a shorten at the same time, both generate the code '1' for different URLs —immediate collision—. Why it happens: the idea of the single counter is copied without noticing that "single" and "several independent servers" contradict each other. How to spot it: if each server has its local counter without coordinating, your codes clash between servers. How to fix it: the counter has to be shared (a central service or a database sequence) or partitioned without overlap (pre-assigned ranges: A spends 1–1000, B spends 1001–2000). At ~40 writes/s, a central counter suffices; the key is that it be a single one, coordinated, not one per server.

Choosing the number of characters without the calculation. What happens: someone puts "8 characters" or "6 characters" for the short_code by intuition, without comparing 62^length against the 5-year demand. With 6 you risk falling short and frequent collisions on the random side; with 8 or more you lengthen the codes needlessly. Why it happens: the number of characters seems a cosmetic detail. How to spot it: if you cannot justify "7" with a calculation (62⁷ vs 6,000 M), you chose it by eye. How to fix it: run the comparison —62⁶ = 56,800 M (just barely enough, no margin), 62⁷ = 3.52 trillion (587× slack), 62⁸ (waste)—. 7 is the minimum that gives comfortable slack. In an interview, "why 7 characters?" is answered with that calculation, not with "it seemed right".

Exercises

Exercise 1 — Encode a counter id by hand. Enlace's global counter just handed out id 500. Apply base62_encode(500) by hand (divide by 62 repeatedly, note the remainders, reverse them) to get the short_code. Then verify with the lesson's implementation.

See solution

Encoding 500:

  • divmod(500, 62) = (8, 4) → symbol ALPHABET[4] = 4
  • divmod(8, 62) = (0, 8) → symbol ALPHABET[8] = 8
  • Remainders in output order: 4, 8. Reversed: 8, 4. Code: '84'.

Verification:

print(base62_encode(500))   # '84'

Notice that id 500 gives a code of only two characters ('84'), because 500 is a small number. The codes grow to 7 characters only when the counter reaches big numbers (beyond 62⁶ ≈ 56,800 million). At the beginning of Enlace's life, the codes are very short; they lengthen as the counter advances. That is a natural (and sometimes undesired: it reveals how many URLs have been created) consequence of the sequential counter, which lesson 5 of module 3 discusses with the "guessability" tradeoff.

Exercise 2 — How many characters for an Enlace 100× larger? A rival shortener creates 10,000 million URLs a month (100× Enlace), with 5-year retention. Compute its total 5-year demand and determine how many base62 characters it needs at a minimum, comparing 62^length against the demand. Do 7 still suffice, or does it need 8?

See solution

5-year demand = 10,000,000,000 × 12 × 5 = 600,000,000,000 (600 billion).

demand = 600_000_000_000
for length in range(7, 10):
    space = 62 ** length
    print(length, f"{space:,}", "ENOUGH" if space >= demand else "short")
# 7  3,521,614,606,208       ENOUGH
# 8  218,340,105,584,896     ENOUGH
# 9  ...                     ENOUGH

With 7 characters (3.52 trillion) it still suffices: 3,521,614,606,208 / 600,000,000,000 ≈ 5.9× slack. A much tighter margin than Enlace's 587×, but above 1×, so 7 characters still suffice even at 100× Enlace's scale. Only a shortener ~1,000× larger than Enlace (which would exhaust 62⁷) would start to need 8. This illustrates why 7 is the canonical choice of so many shorteners: it absorbs enormous growth before falling short. (That said, with only 5.9× slack, the rival could prefer 8 characters out of prudence, to have plenty of margin and for the random generator to almost never clash —a defensible "one more character for peace of mind" tradeoff—.)

Exercise 3 — The write path facing a failure. The shorten path goes: server → counter → router → shard primary. For each failure, say what happens and whether Enlace can keep accepting writes: (a) one of the three stateless servers goes down; (b) a shard's primary goes down; (c) the global counter goes down.

See solution
  • (a) A stateless server goes down. Enlace keeps accepting writes without a problem: the load balancer's health check takes it out of rotation and the other two servers absorb its load. Since they are stateless, nothing is lost —any one handles any shorten—. It is exactly for this that there are several stateless servers.
  • (b) A shard's primary goes down. The writes of that shard stop until a replica is promoted to primary (failover). The writes of the other shards go on as normal —the sharding isolates the failure to one shard—. The promotion of the replica to the new primary (detection, election, avoiding "split brain") is the failover, a topic of resilience-and-reliability-patterns-guide; here it is enough to know that the replica already has the data and can take over.
  • (c) The global counter goes down. If the counter is a single central service without redundancy, all writes stop (nobody can get a new id) —it is a single point of failure of the write path—. That is why, if a central counter is used, it has to be made redundant (or use pre-assigned ranges, which leave each server with ids in reserve to carry on for a while even if the central one is down). The reads, on the other hand, do not depend on the counter, so resolve keeps working even if the counter dies.

The lesson: in a distributed design, each failure has a different blast radius —a stateless server affects nothing (redundancy), a primary affects one shard (isolation), the counter affects all the writes (a single point that has to be protected)—. Mapping these radii is part of the "deep dive", and where the failover gets serious, the resilience guide takes over.

Summary and next step

In this lesson you deep-dived into Enlace's write path, the first "deep dive" of step 4. You went through shorten end to end over the distributed architecture —load balancer → stateless server → counter → router → shard primary— and went down to the heart: the generation of the short_code with a global counter + base62_encode, the numbered-ticket strategy that guarantees uniqueness by construction, without verifying collisions. You executed it: base62_encode(1000000) = '4c92', three consecutive codes from the generator, and the anchor 62⁷ = 3,521,614,606,208 with 0.17% used —the calculation that justifies the 7 characters (587× slack: 6 just barely reaches, 8 wastes)—.

You addressed the distributed asterisk —a counter shared among several servers (central, or pre-assigned ranges, or Snowflake if it scaled)— with the defensible choice for Enlace (central counter, which at ~40/s is more than enough) and the border with the sibling guides. And you saw why the write is Enlace's easy path, and how the decision to defer analytics (step 1) is exactly what keeps it easy.

Before moving on you should be able to: go through shorten over the distributed architecture; explain why the counter gives uniqueness without verifying collisions; execute base62_encode and justify the 7 characters with the calculation; and reason about how the counter is shared among servers.

What comes next is the path that really pinches. In lesson 6 you deep-dive into the read pathresolve, ~4,000/s—: the cache-aside end to end, the target hit ratio (0.90 → 5.90 ms average latency), the working set (~333 MB) that makes the cache cheap, and the residual load (386 reads/s) that reaches the database and that the data scaling (lesson 7) will have to absorb. It is where all of Enlace's design effort concentrates.

Resources