A repeatable framework for system design, breaking down client-to-backend architecture, isolated code execution, and high-concurrency leaderboards.
Someone hands you a marker and says: "Design LeetCode."
The hardest part of system design is never the technology. It is the blank whiteboard.
Most people panic and draw twenty disconnected boxes. They sketch a database, a cache, user profiles, discussion forums, and contest badges. Within ten minutes the board is an illegible mess that nobody can defend.
Good system design is an orderly way of thinking. You clarify what matters, isolate the single riskiest problem, separate high-level architecture from low-level execution logic, and stress-test what breaks under load.
This guide walks through that process using LeetCode. At the end, we run the exact same framework on a completely different system to prove that the method transfers anywhere.
Before touching the board, cut the problem down to something you can hold in your head.
LeetCode has hundreds of secondary features: company tags, upvotes, streak counters, and interview kits. If you try to design everything at once, you will design none of it well.
State the core loop:
Everything else can sit behind standard queries without changing the architecture. Say what you are cutting out loud. Nobody loses points for scoping.
Next, do thirty seconds of arithmetic:
Normal traffic:
100,000 active daily users
200,000 code submissions per day
Average write rate: 2 to 3 submissions per second
Contest burst:
30,000 users join a Saturday contest
Contest starts at 10:00:00 AM
5,000 submissions per minute during peak bursts (~85 to 100 per second)
Each submission takes 1 to 3 seconds of CPU execution time
Average load is gentle. One small server could handle two requests per second easily.
The contest burst is the real problem. Thirty thousand people submit inside the same hour, each needing seconds of CPU time. That math reveals the core constraint: the system cannot be synchronous. It must buffer bursts.
Every system has one defining challenge that dictates everything else. For a payment gateway, it is avoiding duplicate charges. For a chat app, it is ordering messages under network partitions.
For LeetCode, it is this: you are running arbitrary, untrusted code written by strangers on your servers.
A user can type anything into that editor:
while True: passa = [0] * (10**9)import os; os.system("rm -rf /")If you run this code inside your main web server process, one submission can take down your entire platform. Because this is the most dangerous problem, the entire architecture must be organized around isolating it.
High-Level Design (HLD) defines the large components and asynchronous paths. It answers: how does a request travel from browser to backend without freezing the site?
Because running code takes seconds and arrives in bursts, the server receiving the request must never execute it directly. It stores the submission, acknowledges receipt in milliseconds, and hands the heavy compute to a queue.
[ User Browser ]
│
│ 1. POST /api/submissions
▼
[ API Gateway / Web Tier ]
│
├──────────────────────────────┐
│ 2. Save submission (PENDING) │ 3. Enqueue job
▼ ▼
[(PostgreSQL Database)] [(Redis / RabbitMQ Queue)]
│
│ 4. Pull job
▼
[ Execution Worker Pool ]
┌───────────────────────┐
│ Worker 1 (Sandbox) │
│ Worker 2 (Sandbox) │
│ Worker N (Sandbox) │
└───────────────────────┘
│
│ 5. Update verdict (ACCEPTED, 18ms)
▼
[(PostgreSQL Database)]
│
│ 6. Push result (SSE or Polling)
▼
[ User Browser ]
When a user clicks Submit, the browser sends a small JSON payload:
// POST /api/submissions
{
"problem_id": "two-sum",
"language": "python",
"source_code": "class Solution:
def twoSum(self, nums, target):
..."
}
The web server responds right away with an acknowledgement:
// Immediate response (202 Accepted)
{
"submission_id": "sub_928174",
"status": "PENDING",
"created_at": "2026-09-09T10:14:02Z"
}
This is the central trick of high-throughput systems: acknowledge immediately, process asynchronously.
The web server responds in fifteen milliseconds because it only writes a row to the database and drops an ID into the queue. This is why LeetCode shows Pending, then Judging, then Accepted.
The browser polls GET /api/submissions/sub_928174 every two seconds or listens on an SSE connection. During contest spikes, the queue absorbs the shock without dropping requests.
High-level design says: "The worker runs the code." Low-level design (LLD) opens that box and writes the actual execution logic.
Each submission moves through a simple state machine:
[ PENDING ] ──▶ [ RUNNING ] ──┬──▶ [ ACCEPTED ]
├──▶ [ WRONG ANSWER ]
├──▶ [ TIME LIMIT EXCEEDED ]
├──▶ [ MEMORY LIMIT EXCEEDED ]
└──▶ [ RUNTIME ERROR ]
At its core, a code runner does what you would do by hand: feed in test inputs, watch the clock, and check the answer.
If you write the judge loop without operating system noise, it looks like this:
def run_judge(user_code, test_cases, time_limit_sec=2.0):
for test in test_cases:
output, error, elapsed = execute_in_sandbox(user_code, test.input, timeout=time_limit_sec)
if elapsed > time_limit_sec:
return "Time Limit Exceeded"
if error:
return "Runtime Error"
if output.strip() != test.expected.strip():
return "Wrong Answer"
return "Accepted"
In twelve lines, you have the entire evaluation contract. Three practical engineering choices hide inside this simple loop:
Stop on the first failure. If test case 3 fails, exit immediately. Running the remaining 97 tests burns CPU and makes other users wait longer in line.
Strip whitespace. Always normalize outputs before comparing strings. A trailing newline difference between Windows and Linux should not turn a correct solution into a Wrong Answer.
Trap the timeout cleanly. If user code gets stuck in an infinite loop, the sandbox pulls the plug after two seconds, keeping the worker healthy for the next job.
The runner loop above works when code is friendly. But untrusted code is often hostile.
What stops someone from typing import os; os.system("rm -rf /") into the editor?
If you ran that code directly on your worker machine, your worker would delete its own operating system. Or what if someone submits a fork bomb:
import os
while True:
os.fork()
That two-liner spawns child processes exponentially until the host kernel freezes.
This is why "we just run it in Docker" falls short. Default containers share the host kernel. Real judge platforms lock down execution using three Linux kernel primitives:
1. Control Groups (cgroups) for hardware limits. Cgroups enforce hard resource ceilings:
MEMORY_LIMIT_EXCEEDED.pids.max = 32. A fork bomb trying to spawn thousands of processes fails on the thirty-third fork with a permission error.2. Namespaces for visibility isolation. Namespaces control what the program is allowed to see:
169.254.169.254 to steal instance credentials.3. Seccomp for system call filtering.
Seccomp intercepts system calls before the kernel executes them. The runner installs a strict whitelist: allow read, write, exit_group, and memory allocation. If code attempts forbidden syscalls like socket, ptrace, kill, or mount, the kernel immediately sends SIGSYS and kills the process.
Modern judges wrap this in microVMs like AWS Firecracker or sandboxed runtimes like gVisor. A microVM boots a minimal kernel in twenty milliseconds, evaluates the code, and vanishes. Nothing persists between runs.
A system design is incomplete until you attack your own architecture. Where does this system break first?
During a contest with thirty thousand participants, everyone opens the leaderboard at once. Computing standings on the fly with a relational query is impossible:
-- This query scans millions of rows on every refresh. It will crash PostgreSQL.
SELECT user_id, COUNT(*) AS solved, SUM(penalty_time) AS total_time
FROM contest_submissions
WHERE contest_id = 101 AND status = 'ACCEPTED'
GROUP BY user_id
ORDER BY solved DESC, total_time ASC
LIMIT 50;
The right tool for real-time ranking is a Redis Sorted Set (ZSET). Scores are indexed in memory:
ZADD contest:101 <score> <user_id>
ZREVRANGE contest:101 0 49 WITHSCORES
Fetching the top 50 participants takes less than two milliseconds, regardless of how many users are browsing.
Now find the failure: the dual write problem.
The same fact now lives in two systems: PostgreSQL holds the permanent submission record, and Redis holds the live contest score.
If a worker writes the ACCEPTED verdict to PostgreSQL and then crashes before updating Redis, the two databases disagree. The user sees Accepted on their problem page, but their leaderboard standing never updates. The leaderboard is quietly corrupted for the remainder of the contest.
The fix is an outbox pattern:
[ Worker ] ──▶ Write submission + outbox event in ONE Postgres transaction
│
▼
[ Postgres Outbox Table ]
│
▼ (event streamer)
[ Update Redis Leaderboard ]
The worker writes both the submission and an outbox event in one database transaction. A lightweight processor streams outbox events to Redis.
If Redis restarts or drops data, you can replay the contest outbox log from PostgreSQL and reconstruct exact standings in thirty seconds.
The value of this four-question order is that it works on any system design problem you encounter tomorrow.
Test it on a real-time food delivery app:
1. What must it do? A customer places an order, a restaurant confirms it, a nearby courier delivers it, and both track the location on a map. Out of scope: user reviews, discount codes, loyalty rewards, and customer service chat.
2. What is the hard part? Geospatial dispatch. Matching a moving customer order to a courier who is nearby, currently available, and heading in the right direction, evaluated continuously as traffic shifts.
3. The high-level design: Orders enter an order queue. Courier devices send GPS heartbeats every four seconds into an in-memory spatial index (like Redis Geo or Uber H3). A dispatch engine periodically pulls pending orders and queries the spatial index for the nearest five couriers.
4. Low-level design and edge cases: When dispatch offers an order to courier A, it places a temporary fifteen-second distributed lock on that courier ID. If courier A declines or times out, the lock releases and the engine cascades to courier B. What breaks first? The GPS write storm: fifty thousand couriers updating coordinates every four seconds produces 12,500 writes per second. You buffer updates in memory and persist only final delivery milestones to disk.
Notice what happened: in three minutes, you framed an architecture with clean boundaries, justified components, and a concrete edge-case solution, without memorized buzzwords.
When you approach a system design problem in an interview or on the job, stick to the order:
A great engineer is not someone who memorized every cloud service. It is someone who breaks a problem into a clean, defensible order.
Tutorials in System Design come first when available.
Follow a tiny next-token predictor until the logic behind modern language models becomes clear.
How splitting a database across machines works, how to choose the key you cannot change later, and the four cheaper fixes almost everyone should try first.
The seven layers, which of them actually exist in a real stack, and how to use the model to find out why a request is failing.