Understanding the problem
Let's be honest: LeetCode needs little introduction if you're interviewing. For the uninitiated, it's a platform of coding problems (easy → hard), multi-language editors, instant judge feedback, and periodic contests. In a system design round, you're graded on how you run stranger code safely and how contests stay snappy — not on inventing a social network around problems.
Problems + stubs.
Sandbox judge.
Redis ZSET + poll.
Queue + workers.
Pair with Delivery framework, Long-running tasks, Message queues, Redis, and Real-time updates (when to skip WebSockets).
Functional requirements
Non-functional requirements
Planning the approach
For user-facing product questions, the plan is boring on purpose: walk functional requirements left to right, then let NFRs drive deep dives. That keeps you out of ranking-ML weeds while the judge and leaderboard are still undrawn.
- Entities + APIs — one endpoint per FR.
- HLD per endpoint — where state lives, how data flows.
- Deep dives — sandbox, Redis leaderboard, queue scale, test harness.
- Level check — mid owns breadth; senior owns isolation + harness; staff cuts overkill.
Core entities
Broad entities first — columns later when the HLD needs them.
API / system interface
One endpoint per functional requirement. Shorthand types are fine if you're clear with the interviewer.
GET /problems?page=1&limit=100 -> Partial<Problem>[]
GET /problems/:id?language=python -> Problem
POST /problems/:id/submit -> Submission
{ code: string, language: string }
GET /leaderboard/:competitionId?page=1&limit=100 -> Leaderboard
- Partial<Problem> — list cards need id, title, level, tags — not the full statement or stubs.
- language query on view — return the matching code stub (default e.g. Python).
- userId never in the body — comes from session/JWT. Timestamps are server-generated.
Later, when submit becomes async under load, add GET /check/:submissionId for client polling (~1s) — same pattern production LeetCode uses.
HLD — list problems
Most interview systems default to microservices. Here a simple client-server monolith is more honest: small codebase, little service-mesh tax. Start there; split only if a deep dive forces it.
SQL or NoSQL both work. Prefer something like DynamoDB when you don't need joins and want to nest testCases on the problem document. Index for pagination.
{
id, title, question, level, tags: [],
codeStubs: { python, javascript, typescript /* ... */ },
testCases: [{ type, input, output }]
}
HLD — view a problem and code
GET /problems/:id returns the full statement and language stub. The browser hosts an editor (e.g. Monaco). Hidden tests stay on the server — never ship the full suite to the client.
HLD — submit and get feedback
This is where the interview gets interesting. Running arbitrary user code on your API hosts is how you lose the company. Compare isolation options out loud, then pick one.
- Containers (Docker) — strong isolation, predictable warm pools, fine control of CPU/mem/net. Default pick when volume is steady and cold starts hurt.
- VMs / microVMs — heavier isolation; more ops cost. Mention when the interviewer stresses escape risk.
- Serverless (Lambda) — great burst scaling; cold starts can blow the 5s budget. Fine if you keep warm concurrency.
Proceed with containers: steady contest load, avoid cold starts. Language-specific pools (Python, JS, …) receive the submission; a worker invokes the sandbox (e.g. Docker exec), waits, reads stdout or a mounted volume — the container itself makes no outbound calls.
- API receives
code+language+ problem id (userId from JWT). - Route to a warm container for that language; run harness + user code under limits.
- Persist Submission; return pass/fail (and failed case metadata as product allows).
HLD — live competition leaderboard
Define a competition on the board: 90 minutes, 10 problems, up to 100k users. Score = distinct problems solved; ties broken by earliest time to finish (from contest start).
Naive path: on each GET /leaderboard/:competitionId, query submissions for that competition, group by user, sort. In SQL:
SELECT userId,
COUNT(DISTINCT problemId) AS numSolved,
MIN(submittedAt) AS lastSolveTime
FROM submissions
WHERE competitionId = :id AND passed = true
GROUP BY userId
ORDER BY numSolved DESC, lastSolveTime ASC;
In DynamoDB, a GSI on competitionId lets you fetch submissions, then group/sort in memory. Clients re-poll every ~5 seconds for freshness.
Deep dive — isolation and security
Containers are the start, not the finish. Name these controls; implement detail only if probed.
- Read-only filesystem — mount code read-only; write outputs to a short-lived temp dir.
- CPU and memory bounds — kill the container on exceed (no noisy-neighbor wipeouts).
- Hard timeout — e.g. 5s wall clock; stops infinite loops and meets the NFR.
- No network — deny egress/ingress except what the worker needs (VPC SGs / NACLs on AWS).
- seccomp — block dangerous syscalls so a breakout is harder.
Deep dive — efficient leaderboards
Polling a full aggregate every 5s at contest scale hammers the DB. Walk three options; land on Redis sorted sets.
- DB query per poll — simple; high load; latency grows with submissions. Reject as the steady-state design.
- Periodic cache — rebuild top-N into Redis every ~30s from the DB. Better, but not truly live; races if rebuild is slow.
- Redis ZSET on write — on each passing submission, update DB and
ZADDthe user's score. Reads areZRANGE … REV WITHSCORES— O(log N + M), no group-by.
Key: competition:leaderboard:{competitionId}
Write: ZADD competition:leaderboard:{id} {score} {userId}
Read: ZRANGE competition:leaderboard:{id} 0 99 REV WITHSCORES
# ZREVRANGE is deprecated since Redis 6.2 — prefer ZRANGE + REV
Ship the first ~1,000 rows to the client; page further on scroll. Score encoding must preserve tie-breaks (e.g. composite score from solve count + inverted finish time) — call that out so ZSET ordering matches the product rule.
Deep dive — 100k competition scale
API servers horizontally scale fine. The bottleneck is CPU-heavy judging.
Napkin: peak 10k concurrent submissions × ~100 tests × ~100ms/test ≈ 100k CPU-seconds if serialized — tens of hours on one core, ~1,600+ cores to finish in a minute. Even the largest VMs top out well below that. Vertical scaling alone is dead — and you'd pay for a huge box idle all week.
- Horizontal container pools — autoscaling groups / ECS or Fargate services per language on CPU / queue depth.
- Queue in front (SQS) — Primary Server enqueues; a Worker pulls, runs the language runtime, then writes submission data back (DB + Redis ZSET). API becomes async: return submission id, client polls
GET /check/:id(~1s).
Deep dive — running test cases
Follow-up that breaks box-drawing mode: how do you actually run one test suite against any language?
Don't author N copies of every test. Keep one serialized suite per problem. Each language image ships a harness that deserializes inputs into native structures, calls the user's function, and compares to expected output.
{
"type": "tree",
"input": [3, 9, 20, null, null, 15, 7],
"output": 3
}
Trees serialize as level-order (BFS) arrays; ints/arrays as JSON. The Python image includes a TreeNode helper beside the user's Solution class — same idea in JS/Java/Go. Register serialization strategies per type.
Final design
Cost and performance levers
What interviewers expect by level
Mid: working spine + isolation awareness. Senior: sandbox trade-offs + harness + Redis board. Staff: simplicity with a scale story — and the discipline to stop.
Wrapping up
LeetCode is a security-and-execution design wearing a product costume. Keep the problem store simple, put untrusted code in hardened containers, materialize contest ranks in Redis, and introduce a queue when the judge becomes CPU-bound — without pretending this is a planetary-scale social graph.
Related: Delivery framework · Long-running tasks · Message queues · Redis · Real-time updates · Rate limiter · Common patterns.