Design LeetCode — problems, sandboxed runs, and live leaderboards

Design LeetCode

Coding-platform walkthrough: list/view problems, sandboxed multi-language execution, competition leaderboards with Redis ZSETs, queues for 100k contestants, and language-agnostic test harnesses — with interview bars by level.

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.

Students write code; sealed exam booths grade with no phones and a timer; results update a live scoreboard.
Exam hall: sandboxed booths grade papers; the scoreboard updates live.
01List / view

Problems + stubs.

02Submit

Sandbox judge.

03Leaderboard

Redis ZSET + poll.

04Scale

Queue + workers.

Pair with Delivery framework, Long-running tasks, Message queues, Redis, and Real-time updates (when to skip WebSockets).

Functional requirements

Whiteboard split into functional requirements and non-functional requirements for LeetCode.
Put this on the board first — above/below the line keeps the hour focused.

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.

  1. Entities + APIs — one endpoint per FR.
  2. HLD per endpoint — where state lives, how data flows.
  3. Deep dives — sandbox, Redis leaderboard, queue scale, test harness.
  4. Level check — mid owns breadth; senior owns isolation + harness; staff cuts overkill.

Core entities

Broad entities first — columns later when the HLD needs them.

Problem, Submission, and Leaderboard entities with key fields.
Talk through these three with the interviewer — User is usually implied.

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.

Client calls API server which reads a paginated problem list from DynamoDB.
API Server + Problems DB — paginate; return partial fields.

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.

Client Monaco IDE to API Server to DynamoDB Problem schema for viewing a specific problem.
Viewing a specific problem — GET /problems and GET /problem/:id off DynamoDB.

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 Server runs code in Java Python Javascript Docker runtime services and stores submissions in DynamoDB.
Running user code in containers — language runtime services inside a Docker boundary.
  1. API receives code + language + problem id (userId from JWT).
  2. Route to a warm container for that language; run harness + user code under limits.
  3. 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.

Leaderboard HLD with Submissions including competitionId and Docker runtime services.
Leaderboard MVP — competitionId on Submissions; client polls every ~5s; DB aggregate is the weak spot.

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.
Docker runtime services with security checklist: isolation, read-only FS, CPU bounds, timeout, VPC, seccomp.
Security checklist beside the runtime box — isolation, /tmp writes, CPU/mem, timeout, VPC, seccomp.

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 ZADD the user's score. Reads are ZRANGE … 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
Primary server with Redis leaderboard sorted set, DynamoDB, Docker runtimes, and Redis Polling caption.
Redis Polling — ZADD on pass; GET /leaderboard reads the sorted set every ~5s.

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).
Final-style design with AWS SQS, Worker, AWS Fargate language runtimes, Write Submission Data, and security notes.
AWS SQS → Worker → Fargate runtimes; worker writes submission data back; client polls GET /check/:id.

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.

One JSON test case flows through a per-language harness into user code for comparison.
One suite · N harnesses — never rewrite tests per language.

Final design

Final design: Monaco IDE, Primary Server, Redis ZSET, DynamoDB, AWS SQS, Worker, AWS Fargate runtimes, security and on-the-box notes.
Final — SQS + Fargate workers, Redis leaderboard, DynamoDB schemas, sandbox security on the box.

Cost and performance levers

What interviewers expect by level

Mid, senior, and staff expectations for a LeetCode system design interview.
Security + execution correctness beat inventing Kafka for 4k problems.
Interview takeaway

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.

← Lattice