Level Up
Ten layers of shipping something real
Cheatcodes for Building
Lay of the land
Every application that works in production — from a dashboard to a trading system to an internal tool — sits on the same set of layers. Miss one and it either doesn't run, doesn't persist, or nobody can reach it
| Layer | What you need | Example |
|---|---|---|
| Application | The scripts you wrote to solve a pain point | Python, React, Flask |
| Version Control | Track changes, collaborate, undo mistakes | Git, GitHub, GitLab |
| Database | Save data between sessions | Supabase, Postgres, SQLite |
| Search | Find things in large datasets | TF-IDF, cosine similarity, pgvector |
| Hosting | What is running your code | Vercel, Dokku, Railway |
| Routing | Domain, DNS, SSL | Cloudflare, Squarespace DNS |
| Auth | Who can access what | OAuth, session cookies, JWT |
| Scheduled Jobs | Code that runs on a schedule | Cron, Celery, Airflow |
| Workers & Scale | Background processing and load distribution | Celery, RQ, nginx, HAProxy |
This is the lay of the land. Each row below is a chapter. Once you understand these layers, you can build and ship anything — the Stack page shows what reusable modules look like on top of this foundation
Packages like requests, flask, supabase, python-pptx, and jinja2 are all tools that sit in these layers. Learning to compose them is how you go from scripts to applications
Version Control
The save system for code. Every change tracked, every mistake reversible
git add . && git commit -m "initial version"
Git — Local Version Control
- Commits — save points. Every commit is a checkpoint you can load back to. Broke something?
git revert. Want to try a risky approach? Branch, experiment, merge if it works - Branches — parallel universes.
mainis your stable build. Feature branches let you experiment without corrupting your save file - Diffs — every change is tracked line-by-line. You can see exactly what changed, when, and why.
git diffshows you the delta,git logshows the history
GitHub — Collaboration + Automation
- Remote repository — your code lives in the cloud. Push your commits, pull others' changes. Everyone works from the same source of truth
- Pull requests — propose changes, get review, merge. The entire open source ecosystem runs on this loop: fork → branch → commit → PR → review → merge
- GitHub Actions — automates testing and deployment. Push to
main, it runs your tests and deploys. No manual steps. This is CI/CD (continuous integration / continuous deployment)
If you've ever saved before a boss fight, died, and loaded back in — you already understand version control
Database
Anything that needs to survive a restart needs a database
supabase.table("contacts").insert({"name": "Jane", "email": "j@co.com"})
When to Use What
| Database | Best for | Trade-off |
|---|---|---|
| SQLite | Local apps, prototypes, single-user tools | No server needed. Lives in a single file. Doesn't scale to concurrent users |
| PostgreSQL | Production apps, multi-user, complex queries | Industry standard. Powerful but needs a server (or hosted service) |
| Supabase | Fast setup, built-in auth + REST API | Hosted Postgres with a dashboard, row-level security, and real-time. Free tier is generous |
| Firebase | Real-time apps, mobile-first | Google's NoSQL. Great for chat, live updates. Different mental model than SQL |
| Redis | Caching, sessions, rate limiting | In-memory key-value store. Blazing fast but not for permanent storage |
Key Concepts
- Decide early: what's ephemeral (cache, session) vs. what's permanent (users, content, transactions). AI-generated apps skip this and break later
- Start with hosted Postgres (Supabase, Neon, Railway). You get a REST API, a dashboard, and row-level security without managing a server
- Migrations — schema changes over time. Your database evolves as your app evolves. Tools like Alembic (Python) or Supabase's built-in migration system track these changes like version control for your data model
Data Feeds as a Layer
Beyond storage, you often need to pull data from external sources — APIs, market feeds, web scraping results. The pattern is the same: define an adapter that normalizes external data into a consistent format your app expects. Think of it as a data access layer — one interface, many backends
price = await data_feed.get_tick("AAPL") # same interface, any source
Whether you're pulling stock prices, weather data, or CRM contacts — wrap external APIs in a consistent adapter. Your app code never changes when you swap the data source
Search & Indexing
How applications find things in large datasets
Every application eventually needs search. It starts simple — finding an exact string in a file — and scales up to understanding what a user means. Each approach below adds a layer of sophistication, and knowing when to reach for which one is a system design essential
Regex (Pattern Matching)
The most basic form of search: Ctrl+F, but in code. Regular expressions (regex) let you define a pattern instead of a fixed string, so you can match things like “any email address” or “a date in YYYY-MM-DD format” without listing every possible value
import re
# Find all email addresses in a block of text
emails = re.findall(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", text)
# Check if a string matches a date pattern
if re.match(r"\d{4}-\d{2}-\d{2}", user_input):
parse_date(user_input)
- Built into every language. Python’s
remodule, JavaScript’s/pattern/literals,grepin the terminal. Zero dependencies, zero infrastructure - Great for structured patterns — phone numbers, URLs, log lines, CSV parsing. Anywhere the thing you’re looking for has a predictable shape
- Not search in the product sense. Regex scans text sequentially — it doesn’t rank results, handle typos, or scale to millions of documents. When you need those things, you reach for the approaches below
Bag of Words (Keyword Search)
The simplest approach: count which words appear in a document and match them against the query. If someone searches “python flask deploy”, find documents containing those exact words
query = "python flask deploy"
results = db.search(query) # matches documents containing these words
- TF-IDF (Term Frequency – Inverse Document Frequency) — weighs words by how often they appear in a document vs. how common they are across all documents. “Deploy” in a cooking blog is more meaningful than “deploy” in a DevOps wiki
- Fast and predictable. Postgres has built-in full-text search. No ML models, no embeddings, no GPU. Works well when users know what words they’re looking for
- Breaks on synonyms. Searching “deploy” won’t find documents about “shipping to production”. It matches tokens, not meaning
Vector Search (Semantic / Cosine Similarity)
Instead of matching words, convert text into a vector (a list of numbers that represents its meaning) and find documents whose vectors point in a similar direction. This is cosine similarity — measuring the angle between two vectors in high-dimensional space
embedding = model.encode("how do I get my app on the internet")
results = vector_db.search(embedding, top_k=5) # finds "hosting", "deploy", "Vercel"
- Embeddings — a model (like OpenAI’s
text-embedding-3-small) converts text into a vector of 1,536 numbers. Texts with similar meaning have similar vectors, even if they use completely different words - Cosine similarity — measures how aligned two vectors are. 1.0 = identical meaning, 0 = unrelated. It ignores magnitude and only cares about direction, which is why longer documents don’t automatically score higher
- Vector databases — Pinecone, Weaviate, pgvector (Postgres extension), Chroma. They store embeddings and run nearest-neighbor search efficiently using indexes like HNSW
When to Use What
| Approach | Best for | Trade-off |
|---|---|---|
| Regex (Pattern Matching) | Structured patterns — emails, dates, log parsing | Zero dependencies. No ranking, no scale, no fuzzy matching |
| Keyword (BM25 / TF-IDF) | Exact matches, structured data, known terminology | Fast, no dependencies. Misses synonyms and intent |
| Vector (Cosine Similarity) | Natural language queries, “find me something like this” | Understands meaning. Needs an embedding model and vector store |
| Hybrid | Production search systems | Run both, merge results. Best of both worlds. Most real systems do this |
This is the foundation behind RAG (Retrieval-Augmented Generation) — the pattern where you search your own data, pull the most relevant chunks, and feed them to an LLM as context. Every AI-powered search, chatbot, or knowledge base uses some version of this
Hosting & Environments
Getting code off your laptop and onto the internet
Environments
Before you deploy, you need isolated environments. Every project gets its own set of packages — break one project's dependencies and the rest don't care
python -m venv .venv && source .venv/bin/activate && pip install requests
| Dimension | venv | conda |
|---|---|---|
| What it isolates | Python packages only | Python packages + system libraries (C, CUDA, etc.) |
| Python version | Uses whichever Python created it | Can install and switch Python versions per environment |
| Speed | Lightweight, instant to create | Heavier, solves more complex dependency trees |
| Best for | Web apps, APIs, scripts, most projects | Data science, ML, anything needing compiled native libs |
Default to venv. Tools like uv make it blazing fast. Reach for conda when you need CUDA drivers or compiled scientific libraries. Never install packages globally
Deploying
git push origin main # CI/CD handles the rest
Frontend vs. Backend
- Frontend — static sites and SPAs go to Vercel or Netlify. Free tier, instant deploys, CDN-backed. Don't overthink this
- Backend — APIs and services need a server. Options range from managed platforms (Railway, Render) to self-hosted PaaS (Dokku on a $12/month droplet). CI/CD via GitHub Actions — push to main, it deploys. No manual steps
Containers
A Dockerfile packages your app with all its dependencies into a reproducible image. "Works on my machine" stops being a problem because the container is the machine. Every modern hosting platform runs containers
FROM python:3.11-slim
COPY . /app
RUN pip install -r /app/requirements.txt
CMD ["python", "/app/main.py"]
| Platform | Best for | Cost |
|---|---|---|
| Vercel | Frontend, static sites, Next.js | Free tier |
| Railway | Backend APIs, quick prototypes | Pay-per-use |
| Render | Full-stack, background workers | Free tier + paid |
| Dokku | Self-hosted PaaS, full control | ~$12/mo droplet |
| Fly.io | Edge deployment, low-latency APIs | Pay-per-use |
Domains & Routing
How the internet finds your app
- Domain — a human-readable address (
yourapp.com). You buy one from a registrar (Cloudflare, Squarespace, Namecheap). ~$10/year - DNS — the phonebook that translates your domain to your server's IP address. An A record points a domain to an IP. A CNAME points one domain to another. Update these in your registrar's dashboard
- SSL — the padlock icon. Encrypts traffic between users and your server. Free via Let's Encrypt. Most hosting platforms handle this automatically
- Subdomains —
api.yourapp.com,dashboard.yourapp.com. Each can point to a different service. The reverse proxy (nginx) reads the hostname and routes to the right container
Buy a domain, point it at your server, let the hosting platform handle SSL. That's it. The complexity is in the initial setup, not ongoing maintenance
Auth
Who can access what
@login_required
Authentication vs. Authorization
- Authentication — who are you? (login, password, OAuth)
- Authorization — what can you do? (roles, permissions, groups)
Options
| Approach | Best for | Example |
|---|---|---|
| Session-based | Traditional web apps (Flask, Django) | Server stores session, browser stores cookie |
| JWT tokens | API-first architectures, SPAs | Stateless, token in header, no server-side session |
| OAuth | "Sign in with Google/GitHub" | Delegate auth to a trusted provider |
| Managed | Don't want to build auth yourself | Auth0, Clerk, Supabase Auth |
Decorators
A decorator is a one-line wrapper you put above a function to change its behavior. In Python, it’s the @something syntax. The function still works the same way — the decorator just runs extra logic before (or after) it executes. Think of it as a bouncer at the door: the party inside doesn’t change, but now only the right people get in
@login_required— wraps any route. The auth logic runs before your code. No if-statements, no repeated checks. One line locks a page, an API endpoint, or an entire section of your app@role_required("admin")— same pattern, finer control. Only users with the “admin” role get through. Stack decorators to combine rules —@login_required+@group_required("client-x")- Start with Supabase Auth or a managed provider unless you have a specific reason to roll your own. Auth demands 100% correctness — a 95% solution still leaks access
Scheduled Jobs
Code that runs when you’re not watching is where production gets real
The gap between “it works when I run it” and “it runs reliably at 3 AM every day” is the entire discipline of data engineering
0 0 * * * /path/to/venv/bin/python /path/to/script.py
When to Use What
| Approach | Best for | Trade-off |
|---|---|---|
| Cron | Simple recurring tasks — daily reports, hourly data pulls, cleanup | Zero dependencies. No retry logic, no dependency graph, no visibility when it fails silently |
| Systemd timers | Linux-native scheduling with logging and restart policies | Better than cron for servers you control. Still no DAG, no cross-job coordination |
| Celery + Beat | Python apps needing background workers with scheduling | Retry logic, result backends, priority queues. Needs Redis/RabbitMQ. Operational overhead |
| Airflow | Complex DAGs — multi-step pipelines with dependencies, retries, backfills | Industry standard for data engineering. Heavy to run. Overkill for less than a pipeline |
| GitHub Actions | Lightweight scheduled jobs without managing infrastructure | Free tier generous, built-in logs. Limited to 6-hour runtime, not great for stateful jobs |
Decision framework
One script, one schedule → cron or systemd timer. Multiple tasks, no dependencies → Celery Beat or GitHub Actions. Multi-step pipeline where step 3 depends on steps 1 and 2 → Airflow or Prefect. Start with the simplest tool that handles your failure mode. You can always graduate to Airflow later — you can’t easily un-adopt it
Data Engineering — The ETL Pattern
Data pipelines are just scheduled jobs with dependencies and quality gates
- Extract — pull data from source systems (APIs, databases, files, scraped pages). Write adapters that normalize messy inputs into a consistent schema
- Transform — clean, deduplicate, enrich, aggregate. This is where domain knowledge lives. A good transform makes downstream consumers simple
- Load — write to the destination (data warehouse, Postgres, S3). Idempotent writes — running the same job twice shouldn’t corrupt your data
- Monitor — data quality checks, row count assertions, freshness alerts. The pipeline that fails silently is worse than the one that never runs
| Tool | Best for | Trade-off |
|---|---|---|
| Cron + Python | Simple ETL, single data source, personal projects | Fastest to build. No orchestration, no backfill, no visibility |
| Airflow | Complex DAGs, team environments, data platform teams | Industry standard. Heavy to operate. Worth it at scale |
| Prefect | Modern alternative to Airflow, better DX | Pythonic API, cloud-hosted option. Smaller community |
| dbt | SQL-based transforms inside a data warehouse | Best-in-class for analytics engineering. Only does the T in ETL |
Workers & Scale
Workers process jobs. Load balancers distribute traffic. Two different problems.
Workers
Background processes that pull jobs from a queue and execute them. For anything too slow for a request/response cycle — sending emails, processing uploads, running ML inference, generating reports
@celery.task
def generate_report(user_id):
data = fetch_data(user_id)
pdf = render_pdf(data)
send_email(user_id, attachment=pdf)
| Tool | Language | Best for |
|---|---|---|
| Celery | Python | Most Python web apps. Mature, well-documented, supports scheduling via Beat |
| Python asyncio | Python | Concurrent I/O without threads — API calls, web scraping, DB queries in parallel. Built into Python, no extra dependencies |
| RQ | Python | Simpler alternative to Celery. Less config, fewer features |
| Dramatiq | Python | Modern Celery alternative with better defaults and error handling |
| Bull | Node.js | Redis-backed job queue for Node applications |
| Sidekiq | Ruby | The standard for Rails background jobs |
import asyncio, aiohttp
async def fetch_all():
async with aiohttp.ClientSession() as session:
tasks = [session.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
return [await r.json() for r in responses]
Workers vs. async: workers handle CPU-bound or long-running jobs in separate processes. Async handles I/O-bound concurrency within a single process — waiting on APIs, databases, or file reads in parallel without blocking. Use both: async inside your app for fast I/O, workers for heavy background jobs
Load Balancing
Distributing incoming requests across multiple instances of your app. For when a single server can’t handle the traffic or you need zero-downtime deploys
- Workers scale compute — add more workers to process more jobs in parallel. Each worker is independent
- Load balancers scale traffic — add more app instances behind a balancer to handle more concurrent requests
- You need workers first — most apps hit background processing limits before they hit traffic limits. A single server with 4 workers handles more real-world load than you’d expect
| Tool | Best for |
|---|---|
| nginx | Reverse proxy + load balancer. The default for most setups |
| HAProxy | High-performance TCP/HTTP load balancing |
| AWS ALB | Managed load balancing in AWS. Auto-scaling built in |
| Cloudflare | CDN + DDoS protection + basic load balancing |
| Dokku | Built-in load balancing for self-hosted PaaS |
System Design Interviews
Approach it like a case interview — structure first, then depth
A system design interview tests whether you can decompose an ambiguous problem into layers, make defensible trade-offs, and communicate your reasoning. The format maps directly to case interviews: you’re given a vague prompt, you structure the problem, you go deep where it matters, and you present back. The domain is different. The muscle is the same
The Framework
Clarify the prompt → Scope the requirements
Structure the problem → Identify the layers
Drill into a branch → Deep-dive one component
Synthesize → Summarize trade-offs and next steps
| Step | What to do | Case parallel | Time |
|---|---|---|---|
| 1. Clarify | Who uses it? How many users? Read vs. write heavy? Do the back-of-envelope math — 10M users at 10 requests/day is ~1,200 QPS. That number determines whether you need caching, sharding, or a single Postgres instance | Clarifying questions — scope before solving | 5 min |
| 2. Map the layers | Draw high-level architecture: client → API → database → background jobs. Identify which layers the problem actually lives in | Issue tree — MECE breakdown into branches | 5 min |
| 3. Deep-dive | Pick the hardest layer and go deep. In casing the deep-dive is usually a math problem — in system design it’s usually a data modeling or scaling problem. Schema design, query patterns, indexing strategy | Drilling into the most impactful branch | 15 min |
| 4. Trade-offs | Every choice has a cost. SQL vs. NoSQL, sync vs. async, cache vs. real-time, consistency vs. availability. Name what you’re giving up and why. There are always more trade-offs than you think — keep surfacing them | “Recommendation is X because Y, trade-off is Z” | 10 min |
| 5. Synthesize | Walk through the full system end-to-end. What you’d build first (MVP) vs. what scales later | Elevator pitch — 30-second summary | 5 min |
Common prompts
| Prompt | Core layers | Where the depth lives |
|---|---|---|
| Design Twitter | Data model, fan-out, feed ranking, real-time | Fan-out on write vs. fan-out on read — how does the feed get built? |
| People search engine | Schema, NL parsing, hard/soft constraint ranking | How do you rank results when some requirements are strict and others are preferences? |
| Job scheduler | Queue, dependency DAG, retries, failure alerting | How do you handle a job that depends on two other jobs, one of which failed? |
| Rate limiter | Token bucket, sliding window, distributed state | How does the limiter work across multiple servers? |
| URL shortener | Hashing, collision handling, redirect latency, analytics | How do you generate unique short codes at scale without collisions? |
| Chat system | WebSockets, message persistence, presence, delivery guarantees | What happens when a user is offline? How do you guarantee delivery? |
Evaluation criteria
- Structured thinking — decomposed before solving
- Trade-off awareness — named costs of every decision
- Depth on the crux — went deep on the hardest part
- Communication — legible to someone not in the room
- Prioritization — MVP first, scale later
The structured problem-solving, communication, and orchestration that made you a good consultant will make you the ideal forward deployed engineer
Ready to see what modules look like in practice?
The Stack →