Two ways to mint a code
Decide how you mint the code, the core decision here, and there are two defensible answers.
Encode a unique number, the first of them. Take an identifier from a sequence and encode it in an alphabet of letters and digits. You get a code needing no collision checking at all, because the numbers underneath never repeat.
Do the capacity arithmetic: seven characters from 62 options is about 3.5 trillion codes, which covers 3,500 years at a hundred million links a month.
Watch the drawback: leakage. Sequential numbers mean one code followed by the next tells your competitors how many links you create a day, and it makes codes guessable. Randomising within a pre-allocated range fixes both cheaply.
Hash the address instead, the second answer. Take a hash of the long address and keep the first seven characters. Identical addresses now deduplicate to one code for free, which numbering cannot do.
Pay for that in collisions, because seven characters of a hash will eventually match two different addresses. Every insert has to check and retry with something appended on conflict, and at 40 writes a second that check is cheap.
The data model
Keep the data model deliberately dull. One table with the code as the key, the long address, a creation time, an expiry and an owner. No joins anywhere on the hot path.
Notice the shape you ended up with: a pure key-value lookup. That is why the wide-column stores fit at very large scale. It is also why an ordinary relational database with the code as its key serves tens of thousands of reads a second without complaining.
Build two endpoints. One takes a long address and an optional custom alias, returns the short one, and rate limits per user so nobody scripts a billion links. The other does the redirect. Custom aliases live in the same table behind a uniqueness constraint, first come first served.
Worked example
Marcus builds the encoder for an internal shortener at a fintech and picks MD5-prefix codes because retailers submit the same promotional URL hundreds of times and dedup saves rows. Six months in, at 80 million stored links, the on-call channel lights up: a merchant's link resolves to a different merchant's page. It is the first real collision, two URLs sharing the prefix 'x9Kd21Q'. The insert path had a check-then-write race: two app servers checked for the code in the same 3 ms window, both saw nothing, both inserted, and the second overwrote the first. His fix is an INSERT with ON CONFLICT that compares the stored long_url; on mismatch it appends a counter to the input and rehashes. He backfills a scan for other silent overwrites and finds 4 more. Collision handling was in his design doc; the atomicity of it was not.