Designing LeetCode
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.
Question one: what does it actually have to do?
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:
- A user submits code from the browser editor.
- The system executes it against hidden test cases in isolation.
- The user receives a verdict: Accepted, Wrong Answer, Time Limit Exceeded, or Runtime Error.
- During contests, a live leaderboard ranks participants by score and penalty time.
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.
Question two: what is the one genuinely hard part?
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:
- An infinite loop like
while True: pass - Memory exhaustion like
a = [0] * (10**9) - A disk attack like
import os; os.system("rm -rf /") - A port scan against your internal cloud network
- A fork bomb that spawns child processes until the operating system freezes
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.
The high-level design: connecting the pieces
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.
The low-level design: inside the code runner
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 ten percent: locking down untrusted execution
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: Cap memory at 256MB. If user code tries to allocate more, the Linux kernel Out-Of-Memory (OOM) killer halts the process instantly. The worker catches that signal and marks the submission
MEMORY_LIMIT_EXCEEDED. - CPU quota: Limit the process to one CPU core so a runaway loop cannot starve neighboring jobs.
- Process limits: Set
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:
- Network namespace: Disable network interfaces completely, leaving only loopback. User code cannot open sockets, send spam, or reach cloud metadata endpoints like
169.254.169.254to steal instance credentials. - Mount namespace: Mount the filesystem as read-only, using a temporary scratch directory for compilation that is wiped clean after the test completes.
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.
What breaks first: the contest leaderboard dual write
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.
Running the method on another system
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.
What to remember
When you approach a system design problem in an interview or on the job, stick to the order:
- Scope the core loop. State what you are building and what you are intentionally dropping. Run rough arithmetic to identify whether the challenge is steady load, storage, or bursts.
- Find the fatal risk. Identify the single hardest problem before drawing boxes. In LeetCode, it is executing untrusted code. In banking, it is race conditions on balances. Everything else arranges around this risk.
- Separate HLD from LLD. Use high-level design to decouple components with queues so bursts do not cascade into outages. Use low-level design to open the riskiest box and specify the execution logic and state machine.
- Attack the dual writes. Identify where the same data lives in two places. Always maintain a durable source of truth that can rebuild fast caches when failures occur.
A great engineer is not someone who memorized every cloud service. It is someone who breaks a problem into a clean, defensible order.