Short answers first: use atomic increments (or DB sequences) to avoid duplicate codes, hand out ID blocks to avoid a single counter bottleneck, and store an expiry timestamp to support link expiration. Read the details below for examples and trade-offs.
Follow-up one — concurrent create requests
Question: Two create requests arrive at the same instant. Can they get the same short code? Short answer: No — they should not. Use an atomic mechanism so only one request reads/increments/persists the counter at a time. What to do:- Prefer storage-level atomic operations: Redis
INCR, databaseSERIAL/SEQUENCE, or other built-in atomic counters. - If your store lacks atomic increments, use distributed locks (e.g., based on etcd/Zookeeper or a Redis-based lock) — note this often performs worse than using native atomic ops.
- Ensure the read+increment+persist is a single atomic operation or transaction so no two requests observe the same value.
- Atomically read-and-increment the counter (or acquire lock and increment).
- Persist the new counter value (if needed by your allocator).
- Convert the numeric ID to a short code (base62-like encoding) and return it.
- Redis INCR: https://redis.io/commands/incr
- PostgreSQL sequences: https://www.postgresql.org/docs/current/sql-createsequence.html
Follow-up two — counter as a single point of failure
Question: What happens when the counter service goes down? Short answer: Making the counter a single, constantly-hit service is risky. Let each application server obtain a block of IDs (a range) from the counter once, and then allocate IDs locally from that block. This reduces contention and the dependency on the central counter. How block allocation works:- The central allocator grants non-overlapping numeric ranges to each server (for example, blocks of 10,000 IDs).
- Each server uses its local range to generate many short codes without contacting the allocator until the block is exhausted.
- If the central allocator is temporarily unavailable, servers continue issuing IDs from their local ranges.

Operational tips and trade-offs:
- Block size selection:
- Larger blocks → fewer allocator calls, better throughput, but higher potential ID wastage if a server fails before using its block.
- Smaller blocks → less wasted ID space, but more frequent central allocator calls.
- Persist each server’s current position inside its block (e.g., local durable offset) so it can resume allocation after restarts.
- Consider periodic heartbeats and rebalancing if servers die with unused ID ranges (to avoid wasted ranges when that matters).
- Use a consensus-backed allocator (e.g., leader-managed increment with replication) if you need strong durability for the allocator itself.
If you use blocks, plan for server crashes: decide whether unused IDs in a server’s block are acceptable wastage or if you need reclamation. Reclaiming ranges increases allocator complexity and coordination.
Follow-up three — link expiry
Question: We want links to expire after a certain time. How would you implement that? Short answer: Store anexpires_at timestamp on each short-code row and check it at redirect time. Optionally use TTL indexes or background cleanup to reclaim storage.
Implementation options:
- Synchronous check on redirect:
- Add an
expires_atcolumn to the URL mapping table. - On each redirect request, compare current time to
expires_at. If now >expires_at, return 404 (or a custom expired response).
- Add an
- Asynchronous cleanup:
- Use a background job to delete expired rows.
- If your datastore supports TTL/expire indexes (e.g., MongoDB TTL, some cloud stores), rely on that to automatically remove expired entries.
- Caching considerations:
- For high-read paths, cache resolved redirects but ensure you include expiry in cache metadata.
- Cache entries must be invalidated or set to expire at the same time as
expires_atto avoid serving stale redirects.

Additional considerations:
- If you allow custom expiry per user, validate inputs and enforce maximum/minimum expiration windows.
- For cache-heavy systems, include expiry metadata in cache keys or set TTLs that align with the mapping row’s
expires_at. - Use background pruning to reclaim storage if retention matters for cost or performance.
Final notes — availability, consistency, operational complexity
When applying these changes, consider trade-offs:- Using an atomic central counter gives strictly sequential IDs (simple), but can be a performance bottleneck.
- ID block allocation improves availability and throughput at the cost of potential ID wastage and more complex allocator logic.
- Expiry checks are cheap (single timestamp comparison) but must be integrated into caching and background cleanup strategies.
- Redis INCR: https://redis.io/commands/incr
- PostgreSQL sequences: https://www.postgresql.org/docs/current/sql-createsequence.html
- Distributed locks (overview): https://redis.io/topics/distlock
- TTL indexes and automatic expiry (MongoDB example): https://www.mongodb.com/docs/manual/core/index-ttl/