Build a URL Shortener
Build a URL shortener with Flask and Redis to learn system design.
Introduction
30 Second Summary
Every time you click a short link in a social media post or text message, a URL shortener is doing the work behind the scenes. It takes a long, ugly web address and swaps it for a compact code that redirects you to the right place.
In this project, you will build your own URL shortener service from scratch. You'll design a REST API, implement an encoding algorithm to generate short codes, and use Redis to store and cache URL mappings.
What You'll Build
A fully functional URL shortener API running locally with Flask and Redis that demonstrates core system design concepts.
By the end of this project, you'll have:
- A REST API with endpoints for creating short URLs and redirecting visitors to the original address.
- A base62 encoder that converts numeric IDs into compact alphanumeric codes.
- A caching strategy optimised for the read-heavy nature of URL shorteners, backed by Redis.
- A system design document summarising your architecture decisions and scaling strategies.
- Secret Mission: Add click analytics tracking to count how many times each short URL is accessed.
Are there any prerequisites?
No prior experience with Flask or Redis is needed. This guide includes full setup instructions for all required tools. Basic familiarity with running commands in a terminal is helpful but not required.
Set Up Your Development Environment
Before you can build a URL shortener, you need two core pieces of infrastructure running on your machine.
Python handles the application logic and API layer. Redis provides the lightning-fast key-value storage that makes URL lookups near-instant. In this step, you will install both tools, create a project folder, and confirm everything runs.
In this step, get ready to:
- Install Python 3 and Redis on your machine.
- Create a project folder with a virtual environment and install Flask and the Redis Python client.
- Run a basic Flask app skeleton and confirm it responds on localhost.
Install Python 3 and Redis
Your URL shortener needs Python 3.10 or higher (the redis Python client requires it) and a running Redis server.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal (macOS) or PowerShell (Windows), and pressing Enter.
- Check your Python version by running:
python3 --version
✔️ I see version 3.10 or higher
Python is ready to go. Move on to installing Redis below.
ⓧ I see a version lower than 3.10
You need Python 3.10 or higher for this project. Visit the official Python downloads page and download the latest Python 3 installer for your operating system.
After installing, close and reopen your terminal, then run python3 --version again to confirm the new version appears.
macOS users with an older default Python
macOS may ship with an older Python that shadows your new install. If python3 --version still shows the old version after installing, try running python3.12 --version (or whichever version you installed) to confirm the new binary is available.
ⓧ Command not found
Python is not installed yet. Visit the official Python downloads page and download the latest Python 3 installer for your operating system.
Run through the installer, then close and reopen your terminal. Run python3 --version to confirm it shows 3.10 or higher.
Now install Redis. The steps differ by operating system.
macOS
- Install Redis using Homebrew by running:
brew install redis
- Start the Redis server as a background service by running:
brew services start redis
What does this do?
This starts Redis in the background and sets it to restart automatically when you log in. Redis will listen on localhost:6379 by default.
Don't have Homebrew?
If brew is not recognized, install Homebrew first by visiting brew.sh and following the one-line install command. Then retry the commands above.
Help me install Redis on macOS with Homebrew
Windows
Redis does not run natively on Windows. The recommended approach is to use WSL (Windows Subsystem for Linux).
- Open PowerShell as Administrator and install WSL by running:
wsl --install
- Restart your computer when prompted.
- After restarting, open the Ubuntu terminal (search for Ubuntu in your Start menu).
- Install Redis inside WSL by running these commands:
sudo apt-get update
sudo apt-get install redis -y
- Start the Redis server by running:
redis-server --daemonize yes
Why WSL?
WSL lets you run a real Linux environment inside Windows. Redis is designed for Linux, and WSL gives you the full Redis experience without Docker overhead.
WSL install failed?
Make sure you are running Windows 10 version 2004 or higher, or Windows 11. Also confirm you opened PowerShell as Administrator (right-click, Run as administrator).
Help me troubleshoot WSL or Redis on Windows
- Verify Redis is running by testing the connection:
redis-cli ping
You should see PONG in the output. This confirms Redis is running and accepting connections on localhost:6379.
Not seeing PONG?
Make sure you started the Redis server in the previous step. On macOS, run brew services start redis again. On Windows (WSL), run redis-server --daemonize yes from the Ubuntu terminal.
Help me fix Redis connection issues
Create your project folder and install dependencies
With Python and Redis confirmed, you will create a dedicated folder for this project and set up a virtual environment to keep your dependencies isolated.
- Open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Visual Studio Code, and pressing Enter.
- Open the built-in terminal in VS Code by pressing Ctrl+` (both macOS and Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir url-shortener
cd url-shortener
- Create a Python virtual environment by running:
python3 -m venv venv
- Activate the virtual environment by running:
macOS
source venv/bin/activate
Windows
source venv/bin/activate
Using WSL?
Since you are running inside the WSL Ubuntu terminal, use the same Linux command above. If you are in PowerShell instead, run venv\Scripts\activate.
You should see (venv) at the start of your terminal prompt. This means the virtual environment is active.
- Install Flask and the Redis Python client by running:
pip install Flask==3.1.3 redis==7.4.0
Why pin versions?
Pinning exact versions (Flask==3.1.3 and redis==7.4.0) ensures your project behaves the same way regardless of when you install. This prevents subtle bugs caused by library updates.
- Create the requirements.txt file to record your dependencies by running:
echo "Flask==3.1.3
redis==7.4.0" > requirements.txt
- Confirm the file was created by running:
cat requirements.txt
You should see both Flask==3.1.3 and redis==7.4.0 listed.
pip command not found?
Make sure your virtual environment is activated. You should see (venv) at the start of your prompt. If not, re-run the activate command from the previous step.
Help me fix the pip not found error
Create the Flask app skeleton and test it
Now you will create a minimal Flask application to confirm your setup works end-to-end before building the URL shortener logic.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Select the url-shortener folder on your Desktop, then click Open.
- Create a new file called app.py by clicking the new file icon in the Explorer sidebar (or press Cmd+N on macOS / Ctrl+N on Windows, then save as app.py).
- Copy the following code into app.py:
from flask import Flask
app = Flask(__name__)
# Simple index route to confirm the app is running
@app.route('/')
def index():
return 'URL Shortener is running!'
# Start the Flask development server
if __name__ == '__main__':
app.run(debug=True)
What does this code do?
This creates a minimal Flask web application with a single route at /. When you visit http://localhost:5000 in your browser, Flask returns the text "URL Shortener is running!" to confirm everything works.
The debug=True flag tells Flask to automatically reload when you change code. This saves you from restarting the server manually during development.
- Save app.py.
- Run the Flask application by opening the VS Code terminal (Ctrl+`) and running:
python3 app.py
You should see output indicating the server is running on http://127.0.0.1:5000.
- Open your browser and navigate to http://localhost:5000.
You should see the text "URL Shortener is running!" displayed in your browser.
- Go back to your terminal and press Ctrl+C to stop the server.
Not seeing the page in your browser?
Make sure the terminal still shows the server running (no error messages). Check that your virtual environment is active ((venv) in the prompt) and that you saved app.py before running the command.
If you see "Address already in use", another process is using port 5000. On macOS, AirPlay Receiver sometimes uses this port. Disable it in System Settings > General > AirDrop & Handoff.
Help me get my Flask app running
✔️ Awesome, I've got everything!
Great. Double check you have saved both app.py and requirements.txt.
ⓧ I'd like to double check the full code
Here is what your files should look like at this point:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'URL Shortener is running!'
if __name__ == '__main__':
app.run(debug=True)
Flask==3.1.3
redis==7.4.0
Your development environment is ready. Next up, you will design the API endpoints and data model for your URL shortener.
Design the API and Data Model
Your Flask app is running and Redis is ready. Now it's time to design the actual URL shortener.
Every URL shortener needs two things: a way to accept long URLs and a way to redirect short codes back to the originals. In this step, you'll define the API endpoints, connect to Redis, and set up the data model that powers your shortener.
In this step, get ready to:
- Define two REST endpoints for creating and resolving short URLs.
- Connect to Redis and plan the key-value data model.
- Write route skeletons and test them with curl.
Define your API endpoints
A URL shortener only needs two operations. One endpoint accepts a long URL and returns a short code. The other endpoint accepts a short code and redirects the user to the original URL.
- POST /shorten accepts a JSON body with a url field and returns a short code.
- GET /<short_code> looks up the short code and redirects the browser to the original URL.
Why only two endpoints?
URL shorteners are read-heavy workloads. For every URL that gets created, it may be accessed hundreds or thousands of times. Keeping the API surface small means you can optimize the redirect path without extra complexity.
Set up Redis and plan the data model
Redis is a key-value store. It's perfect for URL shorteners because the core operation is a simple lookup: given a short code, return the long URL.
Your data model uses three keys: a counter for generating unique IDs, a prefix for storing URL mappings, and a TTL value for cache expiration.
- In app.py, replace the existing import line at the top and everything down to (but not including) the @app.route('/') line with this code:
import redis
from flask import Flask, request, jsonify, redirect
app = Flask(__name__)
# Connect to the local Redis server
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
# Data model constants
COUNTER_KEY = 'url_shortener:counter'
URL_PREFIX = 'url:'
CACHE_TTL = 3600
What does this code do?
The redis.Redis() call creates a connection to your local Redis server. The decode_responses=True flag tells the client to return strings instead of raw bytes.
COUNTER_KEY is the Redis key for an atomic counter that generates unique IDs. URL_PREFIX namespaces URL mappings so they don't collide with other keys. CACHE_TTL sets how long (in seconds) a hot URL stays cached before Redis expires it.
- Save app.py.
Seeing a connection error?
If Python reports a connection refused error, your Redis server is not running. Start it with redis-server in a separate terminal window.
help me debug a Redis connection error in Flask
Write the route skeletons and test with curl
With the data model in place, you'll now replace the old placeholder route with the two real endpoints. These are skeletons that validate the structure works before you add the encoding logic.
- In app.py, find the existing route block:
@app.route('/')
def index():
return 'URL Shortener is running!'
- Delete the entire @app.route('/') block (the decorator, function definition, and return line).
- In its place, add the POST /shorten route by pasting this code:
# Endpoint to create a short URL
@app.route('/shorten', methods=['POST'])
def shorten_url():
data = request.get_json()
if not data or 'url' not in data:
return jsonify({'error': 'Missing url field'}), 400
return jsonify({'message': 'shorten endpoint placeholder'}), 201
What does this code do?
This route only accepts POST requests. It reads the JSON body with request.get_json() and checks that a url field exists. If the field is missing, it returns a 400 error. For now, it returns a placeholder response. You'll add the real encoding logic in the next step.
- Below the shorten route (and above the if __name__ block), add the redirect route by pasting this code:
# Endpoint to redirect a short code to the original URL
@app.route('/<short_code>')
def redirect_url(short_code):
return jsonify({'message': f'redirect placeholder for {short_code}'}), 200
What does this code do?
The <short_code> in the route is a Flask URL variable. Whatever the user types after the slash gets passed into the function as the short_code parameter. For now, it echoes the code back. You'll add the actual Redis lookup and redirect in Step 4.
- Save app.py.
- If your Flask server is still running, stop it with Ctrl+C and restart it by running:
python app.py
- Open a second terminal window. Test the POST /shorten endpoint by running:
curl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" -d '{"url": "https://www.example.com"}'
- You should see a JSON response containing shorten endpoint placeholder with a 201 status.
- Test the redirect endpoint by running:
curl http://localhost:5000/abc
- You should see a JSON response containing redirect placeholder for abc.
Getting a 404 or connection error?
Make sure your Flask server is running in the first terminal. You should see output showing it's serving on http://127.0.0.1:5000.
If you see a syntax error when starting the server, double-check that you removed the old @app.route('/') block completely and that the if __name__ block is still at the bottom.
help me debug my Flask routes not responding
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's what your complete app.py should look like at this point:
import redis
from flask import Flask, request, jsonify, redirect
app = Flask(__name__)
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
COUNTER_KEY = 'url_shortener:counter'
URL_PREFIX = 'url:'
CACHE_TTL = 3600
@app.route('/shorten', methods=['POST'])
def shorten_url():
data = request.get_json()
if not data or 'url' not in data:
return jsonify({'error': 'Missing url field'}), 400
return jsonify({'message': 'shorten endpoint placeholder'}), 201
@app.route('/<short_code>')
def redirect_url(short_code):
return jsonify({'message': f'redirect placeholder for {short_code}'}), 200
if __name__ == '__main__':
app.run(debug=True)
Your API skeleton is responding. Both endpoints accept requests and return structured JSON. Next up, you'll implement the base62 encoding that turns numeric IDs into short, readable codes.
Implement Base62 Encoding
Your API skeleton is responding to requests. Now it's time to give the /shorten endpoint real functionality.
The core challenge of a URL shortener is generating short, unique codes from long URLs. You'll solve this with base62 encoding, an algorithm that converts numbers into compact alphanumeric strings using digits, lowercase, and uppercase letters.
In this step, get ready to:
- Write a base62 encoder function.
- Wire it into the /shorten endpoint with Redis counter-based ID generation.
- Test by creating multiple short URLs and verifying unique codes.
Write the base62 encoder function
Base62 encoding maps a number to a string using 62 characters: 0-9, a-z, and A-Z. Each short code is just a number represented in base 62 instead of base 10.
- In app.py, find the import lines at the top of your file.
- Add string and time as new imports above the import redis line, so the top of your file looks like this:
import string
import time
import redis
from flask import Flask, request, jsonify, redirect
- Next, find the line that says CACHE_TTL = 3600.
- Add the following code below it to define the base62 character set and the encoder function:
BASE62_CHARS = string.digits + string.ascii_lowercase + string.ascii_uppercase
def encode_base62(num):
# Handle the edge case where num is 0
if num == 0:
return BASE62_CHARS[0]
# Build the base62 string by repeatedly dividing
result = []
while num > 0:
result.append(BASE62_CHARS[num % 62])
num //= 62
# Reverse because we built the string from least significant digit
return ''.join(reversed(result))
What does this code do?
BASE62_CHARS creates a 62-character alphabet by combining digits (0-9), lowercase letters (a-z), and uppercase letters (A-Z).
The encode_base62 function works like converting decimal to hexadecimal, but with 62 possible digits instead of 16. It repeatedly divides the number by 62 and maps each remainder to a character. For example, the number 1000 becomes g8 in base62.
You won't see a change yet. The next chunk wires this function into the endpoint.
Wire up the shorten endpoint
The encoder function exists, but the /shorten endpoint still returns a placeholder. You'll now connect the encoder to Redis counter-based ID generation.
- In app.py, find the shorten_url() function.
- Find this placeholder return line:
return jsonify({'message': 'shorten endpoint placeholder'}), 201
- Replace that line with the following implementation:
long_url = data['url']
# Increment the atomic counter to get a unique ID
counter = r.incr(COUNTER_KEY)
# Convert the numeric ID to a short base62 code
short_code = encode_base62(counter)
# Store the URL mapping as a Redis hash
r.hset(f'{URL_PREFIX}{short_code}', mapping={
'long_url': long_url,
'created_at': str(time.time()),
'clicks': 0
})
return jsonify({
'short_code': short_code,
'short_url': f'http://localhost:5000/{short_code}'
}), 201
What does this code do?
- r.incr(COUNTER_KEY) atomically increments a counter in Redis. Each URL gets a unique number (1, 2, 3, ...). Atomic means no two requests can get the same number, even under concurrency.
- encode_base62(counter) converts that number to a short string. Number 1 becomes 1, number 62 becomes 10, number 1000 becomes g8.
- r.hset() stores a Redis hash with the original URL, a creation timestamp, and a click counter initialised to 0.
Why a counter instead of hashing the URL?
A counter gives you collision-free codes by design. Every number is unique, so every base62 code is unique. Hash-based approaches (like truncating MD5) require collision detection and retry logic. The trade-off is that counter-based codes are sequential and predictable, but for a learning project that simplicity is worth it.
- Save app.py.
- If your Flask server is still running, it should auto-reload. If not, restart it by running:
python app.py
- Test the endpoint by running this curl command in a new terminal window:
curl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" -d '{"url": "https://www.example.com/very/long/path"}'
You should see a JSON response containing a short_code (like 1) and a short_url (like http://localhost:5000/1).
Seeing a NameError or ImportError?
- Check that import string and import time are at the top of your file. Missing imports cause NameError.
- Verify that BASE62_CHARS is defined above the encode_base62 function. Python reads top to bottom.
- Make sure your Redis server is still running. The r.incr() call will fail if Redis is down.
Help me debug my base62 encoding endpoint
Test encoding with multiple URLs
A single test confirms the endpoint works. Multiple tests confirm that each URL gets a unique code.
- Create a second short URL by running:
curl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" -d '{"url": "https://www.wikipedia.org/wiki/System_design"}'
- Create a third short URL by running:
curl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" -d '{"url": "https://redis.io/docs/"}'
Each response should return a different short_code. If your first URL got 1, the second should get 2, and the third should get 3. The codes are sequential because the counter increments by one each time.
Why are the codes so short right now?
With only a few URLs created, the counter is a small number like 3, which encodes to just one character. As the counter grows, codes get longer. With 7 characters, base62 supports over 3.5 trillion unique codes.
- Also test the error handling by sending a request without a URL field:
curl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" -d '{"wrong_field": "test"}'
You should see the error response: {"error": "Missing url field"} with a 400 status.
Getting the same code for every request?
- Make sure you're using r.incr(COUNTER_KEY) and not a hardcoded value. The counter must increment on every call.
- Check that COUNTER_KEY is set to 'url_shortener:counter'. A typo here means your counter isn't persisting between requests.
Help me figure out why my short codes aren't unique
✔️ Awesome, I've got everything!
Great. Double check you've saved app.py and your Flask server is running without errors.
ⓧ I'd like to double check the full code
Here's the complete app.py as it should look at the end of this step:
import string
import time
import redis
from flask import Flask, request, jsonify, redirect
app = Flask(__name__)
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
COUNTER_KEY = 'url_shortener:counter'
URL_PREFIX = 'url:'
CACHE_TTL = 3600
BASE62_CHARS = string.digits + string.ascii_lowercase + string.ascii_uppercase
def encode_base62(num):
if num == 0:
return BASE62_CHARS[0]
result = []
while num > 0:
result.append(BASE62_CHARS[num % 62])
num //= 62
return ''.join(reversed(result))
@app.route('/shorten', methods=['POST'])
def shorten_url():
data = request.get_json()
if not data or 'url' not in data:
return jsonify({'error': 'Missing url field'}), 400
long_url = data['url']
counter = r.incr(COUNTER_KEY)
short_code = encode_base62(counter)
r.hset(f'{URL_PREFIX}{short_code}', mapping={
'long_url': long_url,
'created_at': str(time.time()),
'clicks': 0
})
return jsonify({
'short_code': short_code,
'short_url': f'http://localhost:5000/{short_code}'
}), 201
@app.route('/<short_code>')
def redirect_url(short_code):
return jsonify({'message': f'redirect placeholder for {short_code}'}), 200
if __name__ == '__main__':
app.run(debug=True)
Your URL shortener can now generate unique short codes from any URL. Next up, you'll make those codes actually redirect visitors to the original URL.
Implement Redirect with Caching
Your shorten endpoint is creating short codes and storing them in Redis. Now you need the other half of the equation: when someone visits a short URL, your server needs to look up the original URL and send them there.
In this step, you'll implement the redirect endpoint and add a caching layer that keeps frequently accessed URLs fast to look up.
In this step, get ready to:
- Implement the redirect endpoint that looks up short codes and returns HTTP 302 redirects.
- Add a TTL-based cache layer using the cache-aside pattern.
- Test the full end-to-end flow from shortening to redirecting.
Implement the redirect endpoint
When someone visits a short URL, your server receives the short code as a URL parameter. It needs to find the matching long URL in Redis and send the visitor there.
- In app.py, find the placeholder redirect_url function:
@app.route('/<short_code>')
def redirect_url(short_code):
return jsonify({'message': f'redirect placeholder for {short_code}'}), 200
- Replace the entire function body (the return jsonify(...) line) with the following code, so your function looks like this:
@app.route('/<short_code>')
def redirect_url(short_code):
# Look up the original URL from Redis
long_url = r.hget(f'{URL_PREFIX}{short_code}', 'long_url')
# Return 404 if the short code doesn't exist
if not long_url:
return jsonify({'error': 'Short URL not found'}), 404
return redirect(long_url, code=302)
What does this code do?
r.hget(f'{URL_PREFIX}{short_code}', 'long_url') fetches just the long_url field from the Redis hash you created in the shorten endpoint. This is a single O(1) lookup.
If the code doesn't exist in Redis, you return a 404 error so the client knows the short URL is invalid. Otherwise, Flask's redirect() function sends an HTTP redirect response that tells the browser to go to the original URL.
You won't test this change yet. The next substep adds caching before you run the full flow.
Add a TTL-based cache layer for hot URLs
URL shorteners are read-heavy workloads. A popular link might be clicked thousands of times, but it's only created once. The cache-aside pattern keeps hot data readily available by refreshing its expiry time every time it's accessed.
- In app.py, inside the redirect_url function, find the return redirect(long_url, code=302) line.
- Find this line:
return redirect(long_url, code=302)
- Add the following line directly above it, so frequently accessed URLs get their TTL refreshed:
# Refresh TTL on access (cache-aside pattern)
r.expire(f'{URL_PREFIX}{short_code}', CACHE_TTL)
return redirect(long_url, code=302)
What is the cache-aside pattern?
Every time a short URL is accessed, r.expire() resets the key's time-to-live to 3600 seconds (one hour). URLs that nobody clicks will eventually expire and free up memory. Popular URLs stay alive because each access resets the clock.
This is a core system design pattern. At production scale, you'd pair it with a separate persistent database so expired keys can be reloaded on demand.
Why 302 instead of 301?
A 301 (Permanent Redirect) tells the browser to cache the redirect locally. Future visits skip your server entirely, which means you lose visibility into click analytics.
A 302 (Temporary Redirect) ensures the browser always hits your server. You can track every single click. This trade-off is a classic system design interview question.
- Save app.py.
Seeing a syntax error on save?
Make sure the r.expire() line is indented to the same level as the return statement below it (8 spaces or 2 tabs inside the function body).
help me fix an indentation error in my Flask redirect function
Test the full end-to-end flow
Time to test the complete journey: shorten a URL, then visit the short code and confirm you get redirected to the original.
- Make sure your Flask server is still running. If you stopped it, start it again by running:
python app.py
- Create a new short URL by running this command in a separate terminal:
curl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" -d '{"url": "https://www.example.com"}'
You should see a JSON response with a short_code and short_url field. Note the short_code value from the response.
- Test the redirect by requesting the short URL with headers visible. Replace the code in the URL with the short_code from your response:
curl -i http://localhost:5000/[[SHORT_CODE="your short_code from the response above"]]
- Look at the response headers. You should see a 302 FOUND status and a Location: https://www.example.com header.
What should I see?
The response will include headers like this:
HTTP/1.1 302 FOUND
Location: https://www.example.com
The 302 status tells the browser this is a temporary redirect. The Location header tells it where to go. If you paste the short URL directly into a browser, it will follow the redirect automatically.
- Test a non-existent code to confirm your 404 handling works:
curl http://localhost:5000/nonexistent
You should see a JSON response with the error message Short URL not found.
Getting a 500 error instead of a redirect?
Check that your Redis server is still running. If it stopped, restart it with redis-server in a separate terminal window.
If you see Short URL not found for a code you just created, it might be from a previous test session before you restarted Redis. Create a fresh short URL and try that code instead.
help me debug my redirect endpoint that's returning a 500 error
✔️ Awesome, I've got everything!
Great. Double check you've saved app.py and that your redirects are working with a 302 status.
ⓧ I'd like to double check the full code
import string
import time
import redis
from flask import Flask, request, jsonify, redirect
app = Flask(__name__)
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
COUNTER_KEY = 'url_shortener:counter'
URL_PREFIX = 'url:'
CACHE_TTL = 3600
BASE62_CHARS = string.digits + string.ascii_lowercase + string.ascii_uppercase
def encode_base62(num):
if num == 0:
return BASE62_CHARS[0]
result = []
while num > 0:
result.append(BASE62_CHARS[num % 62])
num //= 62
return ''.join(reversed(result))
@app.route('/shorten', methods=['POST'])
def shorten_url():
data = request.get_json()
if not data or 'url' not in data:
return jsonify({'error': 'Missing url field'}), 400
long_url = data['url']
counter = r.incr(COUNTER_KEY)
short_code = encode_base62(counter)
r.hset(f'{URL_PREFIX}{short_code}', mapping={
'long_url': long_url,
'created_at': str(time.time()),
'clicks': 0
})
return jsonify({
'short_code': short_code,
'short_url': f'http://localhost:5000/{short_code}'
}), 201
@app.route('/<short_code>')
def redirect_url(short_code):
long_url = r.hget(f'{URL_PREFIX}{short_code}', 'long_url')
if not long_url:
return jsonify({'error': 'Short URL not found'}), 404
r.expire(f'{URL_PREFIX}{short_code}', CACHE_TTL)
return redirect(long_url, code=302)
if __name__ == '__main__':
app.run(debug=True)
Your URL shortener is fully functional. You can create short URLs and get redirected to the originals. Next up, you'll document the system design decisions behind everything you've built.
Document Your System Design Decisions
Your URL shortener is fully functional. It creates short codes, stores mappings in Redis, and redirects visitors with a caching layer.
In a system design interview, building the code is only half the job. You also need to explain WHY you made each design choice. In this step, you'll create a DESIGN.md document that captures your architecture decisions, trade-offs, and scaling strategy.
In this step, get ready to:
- Document your architecture, data flow, and redirect semantics.
- Analyze scaling strategies for 100M+ URLs.
- Compare encoding approaches and their trade-offs.
Create your architecture documentation
A system design document should answer three questions: what are the components, how does data flow through them, and why did you pick each approach over alternatives.
- Create a new file called DESIGN.md in your url-shortener folder.
- Add the following content to the file:
# URL Shortener System Design
## Architecture
- **API Layer**: Flask REST API with two endpoints
- **Storage**: Redis (key-value store, ideal for simple code-to-URL mappings)
- **Encoding**: Base62 counter-based approach
## Data Flow
1. User submits long URL via POST /shorten
2. Server increments atomic counter in Redis
3. Counter value is encoded to base62 (e.g., 1000 -> "g8")
4. Mapping stored in Redis hash: url:{code} -> {long_url, created_at, clicks}
5. Short URL returned to user
## Redirect Flow
1. User visits short URL (GET /:code)
2. Server looks up code in Redis
3. Returns HTTP 302 redirect to original URL
## Why 302 Instead of 301?
- 301 (Permanent): Browser caches the redirect. Future visits bypass our server entirely.
- 302 (Temporary): Browser always hits our server. We can track every click.
We chose 302 because analytics visibility matters. If we did not need analytics,
301 would reduce server load significantly.
What does this document capture?
The Architecture section names the three core components (API, storage, encoding) so an interviewer immediately understands the system's shape.
The Data Flow and Redirect Flow sections trace how a request moves through the system for both write (POST /shorten) and read (GET /:code) paths. The Why 302 section explains a real trade-off: 301 redirects let browsers cache and skip your server, which means you lose click tracking visibility.
- Save DESIGN.md.
- Open the file in VS Code's preview by pressing Cmd+Shift+V (macOS) or Ctrl+Shift+V (Windows). You should see a nicely formatted document with headings, bullet points, and numbered lists.
File not rendering as Markdown?
Make sure the file is saved with the .md extension. VS Code only renders Markdown preview for files with this extension.
help me preview my DESIGN.md file in VS Code
Add scaling considerations
A strong system design answer addresses what changes when the system grows. Your current design works perfectly for a single machine, but at 100 million URLs the bottlenecks shift.
- In DESIGN.md, add the following section at the bottom of the file:
## Scaling Considerations (100M+ URLs)
- **Sharding**: Partition by short code across multiple Redis instances
- **Read replicas**: Add Redis replicas for redirect lookups (read-heavy workload)
- **CDN caching**: Cache 302 redirects at CDN edge for hot URLs (short TTL)
- **Separate read/write services**: Scale redirect servers independently from creation servers
- **Analytics**: Move click tracking to async event logging (e.g., Kafka) to avoid
slowing the redirect path
Why these scaling strategies?
URL shorteners are read-heavy workloads. The ratio of redirects to URL creations is approximately 10:1 or higher. Every strategy here optimises for that reality.
Sharding spreads storage across machines. Read replicas handle redirect traffic without hitting the primary. CDN caching moves hot URLs to the network edge so they never reach your servers at all. Separating read and write services lets you scale each independently based on actual demand.
- Save DESIGN.md.
Include encoding trade-offs
Interviewers love hearing you compare alternatives. The base62 counter approach you implemented is one of two main strategies for generating short codes. The other is hash-based (truncating MD5 or SHA256). Each has distinct trade-offs around collisions, predictability, and complexity.
- In DESIGN.md, find the ## Scaling Considerations (100M+ URLs) heading.
- Add the following section above the Scaling Considerations heading:
## Encoding Strategy: Base62 Counter
- Simple, collision-free, deterministic
- 7 characters = 62^7 = 3.5 trillion unique codes
- Trade-off: codes are sequential and predictable (could enumerate URLs)
- Alternative: hash-based (MD5/SHA256 truncated) adds collision handling complexity
but prevents enumeration
Base62 counter vs hash-based: which is better?
Neither is universally better. The base62 counter gives you guaranteed uniqueness with zero collision handling. You never need to check if a code already exists. The downside is that codes are sequential, so someone could guess other URLs by incrementing.
Hash-based approaches (truncating MD5 or SHA256 to 7 characters) produce unpredictable codes but introduce collision probability. You need retry logic or a bloom filter to handle duplicates. For a learning project, the counter approach teaches the core concept with less complexity.
- Save DESIGN.md.
- Review the full file in Markdown preview. You should see six distinct sections: Architecture, Data Flow, Redirect Flow, Why 302 Instead of 301, Encoding Strategy, and Scaling Considerations.
Sections appearing in the wrong order?
Make sure the Encoding Strategy section appears between the Why 302 section and the Scaling Considerations section. If it ended up at the bottom, cut and paste it above the Scaling heading.
help me reorder my DESIGN.md sections
✔️ Awesome, I've got everything!
Great. Double check you've saved your file.
ⓧ I'd like to double check the full code
Here's the complete DESIGN.md file:
# URL Shortener System Design
## Architecture
- **API Layer**: Flask REST API with two endpoints
- **Storage**: Redis (key-value store, ideal for simple code-to-URL mappings)
- **Encoding**: Base62 counter-based approach
## Data Flow
1. User submits long URL via POST /shorten
2. Server increments atomic counter in Redis
3. Counter value is encoded to base62 (e.g., 1000 -> "g8")
4. Mapping stored in Redis hash: url:{code} -> {long_url, created_at, clicks}
5. Short URL returned to user
## Redirect Flow
1. User visits short URL (GET /:code)
2. Server looks up code in Redis
3. Returns HTTP 302 redirect to original URL
## Why 302 Instead of 301?
- 301 (Permanent): Browser caches the redirect. Future visits bypass our server entirely.
- 302 (Temporary): Browser always hits our server. We can track every click.
We chose 302 because analytics visibility matters. If we did not need analytics,
301 would reduce server load significantly.
## Encoding Strategy: Base62 Counter
- Simple, collision-free, deterministic
- 7 characters = 62^7 = 3.5 trillion unique codes
- Trade-off: codes are sequential and predictable (could enumerate URLs)
- Alternative: hash-based (MD5/SHA256 truncated) adds collision handling complexity
but prevents enumeration
## Scaling Considerations (100M+ URLs)
- **Sharding**: Partition by short code across multiple Redis instances
- **Read replicas**: Add Redis replicas for redirect lookups (read-heavy workload)
- **CDN caching**: Cache 302 redirects at CDN edge for hot URLs (short TTL)
- **Separate read/write services**: Scale redirect servers independently from creation servers
- **Analytics**: Move click tracking to async event logging (e.g., Kafka) to avoid
slowing the redirect path
Secret mission
Add Click Analytics
Your URL shortener works, but you have no idea which links are popular. Real URL shorteners track every click. Can you add analytics to yours?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Redis server (running locally).
- Python virtual environment and project folder on your Desktop.
- Flask development server (running locally).
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with your URL shortener.
- Your Flask dev server will stop when you close the terminal or press Ctrl+C.
- Redis continues running in the background until you stop it or restart your computer.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your project files stay on disk so you can resume later.
- Stop the Flask dev server by pressing Ctrl+C in the terminal where it's running.
- Stop the Redis server by running:
macOS
brew services stop redis
Windows
Open your WSL terminal and run:
sudo service redis-server stop
Linux
sudo systemctl stop redis-server
- Your project folder and virtual environment remain on your Desktop. When you're ready to resume, start Redis again and activate your virtual environment.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your project files and stops Redis.
- Stop the Flask dev server by pressing Ctrl+C in the terminal where it's running.
- Stop and uninstall Redis by running:
macOS
brew services stop redis
brew uninstall redis
Windows
Open your WSL terminal and run:
sudo service redis-server stop
sudo apt remove redis-server
Linux
sudo systemctl stop redis-server
sudo apt remove redis-server
- Delete your project folder by running:
macOS
rm -rf ~/Desktop/url-shortener
Windows
Remove-Item -Path "$HOME\Desktop\url-shortener" -Recurse -Force
Linux
rm -rf ~/Desktop/url-shortener
- This removes the virtual environment, all Python files, and the DESIGN.md document.
Nice Work!
Nice Work!
You just built a fully functional URL shortener from scratch. This is the single most common system design interview question, and you can now explain it from hands-on experience.
Here's what you accomplished:
- Built a REST API with Flask that handles URL creation via POST /shorten and redirection via GET /:code.
- Implemented base62 encoding to generate compact, unique short codes from an atomic counter stored in Redis.
- Used Redis as both primary storage and a caching layer with TTL-based expiry, demonstrating the cache-aside pattern for read-heavy workloads.
- Secret Mission: Added click analytics tracking with per-URL counters and a stats endpoint.
Ready to quiz yourself?