Design LeetCode
Run this for someone else. You hold the answers; they do not. Read the prompt, keep the clock, and use the probes below when an answer is thin. Do not show them this page.
Open with this
Run untrusted code from strangers against hidden tests in a dozen languages, return a fair verdict in seconds, and survive 50,000 people submitting in the last minutes of a contest. Take a couple of minutes on requirements, then we will do some numbers, then the design. I will interrupt to keep us moving.
The clock
- 4 min: functional requirements and scope
- 4 min: non-functional requirements, with numbers
- 5 min: back-of-envelope estimates
- 16 min: high-level design and one or two flows
- 16 min: deep dives and the close
Move them on out loud when a section overruns. The commonest failure is spending twenty minutes on requirements and never reaching a deep dive, and preventing that is your job as much as theirs.
Requirements · 8 min
Listen for: a scoped set of capabilities, an explicit out-of-scope list, and numeric targets rather than adjectives. Prompt with “what are you not building?” if they never scope, and “what number would make that requirement real?” if they say “fast” or “highly available”.
Functional (6)
- Browse and read problems: Statement, examples, constraints and a code template per language.
- Run code against custom input: Quick feedback while coding: sample tests or the user’s own input, output shown.
- Submit and get a verdict: Accepted, Wrong Answer, Time Limit Exceeded, Memory Limit Exceeded, Runtime Error or Compile Error, with runtime and memory. Hidden tests stay hidden.
- Contests: Timed events with thousands of participants, a live scoreboard, and penalties for wrong submissions.
- Submission history: Every past submission with its code and verdict.
- Out of scope: Discussion forums, editorial content, the code editor itself, and payments for premium problems.
Non-functional (5)
- Isolation (no escape, no network, no peeking): This is the requirement that drives the hardest trade-off: every submission is hostile code, so each must run in a sandbox strong enough that a kernel exploit, a fork bomb or a read of the test files cannot affect the host or other users, yet cheap and fast enough to start thousands a minute.
- Verdict latency (p50 < 3 s, p99 < 10 s): Compile, run 50 to 100 tests, compare. Users wait watching a spinner.
- Fairness (same code, same verdict): Time limits must not depend on a noisy neighbour. A solution accepted once must not get TLE on a rerun.
- Contest burst (10× normal load within minutes): Submissions cluster at the start and the last minutes of a contest. Capacity must be ready before the burst, not after.
- Durability (no lost submissions): A contest submission that disappears changes someone’s ranking. Every submission is stored before it is judged.
Estimates · 5 min
Ask for two or three numbers, not all of them. What matters is whether they state assumptions, round sensibly, and say what the number implies. Push once with “where did that come from?”
The numbers (6)
- Submissions per second: ~60 avg. 1 M DAU × 5 runs and submissions a day = 5 M/day ÷ 86 400 ≈ 58/s. Small by web standards; each one is seconds of CPU, which is what makes it expensive.
- CPU cores busy on average: ~180. Each submission costs ~3 core-seconds (compile plus tests): 58/s × 3 s ≈ 175 cores busy at any moment on average.
- Contest peak: ~500 submissions/s. 50 k contestants, many submitting several times in the last 10 minutes: say 300 k submissions in 600 s ≈ 500/s, about 9× the daily average, needing ~1,500 cores.
- Sandbox starts per minute at peak: ~30 k. 500 submissions/s × 60 = 30 k sandboxes a minute. A container starts in ~100 ms and a microVM in ~150 ms; a full VM in tens of seconds would be impossible.
- Test data: ~150 GB. 3,000 problems × ~50 MB of input and expected output = 150 GB. Small enough to cache the popular problems’ tests on every worker’s local disk.
- Submission storage per year: ~10 TB. 5 M/day × ~5 KB (code, verdict, per-test timings) = 25 GB/day × 365 ≈ 9 TB/year. Code is kept forever for history and plagiarism checks.
High-level design · 16 min
Let them draw. Interrupt only to ask what backs a component or what a box actually does. Then pick one flow below and ask them to walk it end to end.
Components (15)
- Browser: Code editor and problem view. Submits code, then waits for the verdict over a server-sent events stream (with polling as a fallback).
- API gateway: Authenticates, rate limits submissions per user (stops spamming the judge), and routes. Contest traffic gets its own limits and priority.
- Problem service: Serves statements, examples, templates and limits. Statements are static and cached at the CDN; hidden tests never leave the judge side.
- Problem store: Problem metadata: time and memory limits per language, checker type (exact match, float tolerance, special judge), and a version for the test set.
- Submission service: Validates the submission (size, language), stores it with status "queued", enqueues a judge job, and later records the verdict and notifies the user.
- Submissions DB (Postgres · by user): Every submission: code, language, problem and test-set version, status, verdict, runtime and memory. Partitioned by user for history pages.
- Judge queue (priority classes): Durable queue with separate classes: contest, submit, run. Workers pull from the highest-priority non-empty class, so a contest is never stuck behind casual runs.
- Judge workers (autoscaled fleet): Pull a job, fetch (or use cached) tests, start a sandbox, compile, run each test with limits, compare output, and report a verdict. Stateless; scaled by queue depth.
- Sandbox (gVisor / Firecracker microVM): Runs the untrusted program with no network, a read-only filesystem except a scratch directory, CPU and memory limits, a process limit, and a syscall filter. Destroyed after each submission.
- Test data (S3 · cached on workers): Hidden inputs and expected outputs per problem and version. Workers keep a local cache keyed by version, so popular problems never refetch.
- Runtime images (warm pool per language): Prebuilt images for each language and compiler version, kept warm on every worker so starting a sandbox does not pull gigabytes.
- Result stream (pub/sub → SSE): Publishes status changes per submission (queued, running test 37/80, verdict) to the gateway connections waiting on them.
- Contest service: Contest windows, registration, scoring rules and penalties. Consumes verdicts during a contest and updates standings.
- Live standings (Redis sorted set): Score and penalty per contestant, encoded into one sortable number. Served (cached for a few seconds) to everyone watching the scoreboard.
- Plagiarism checker: After a contest, compares accepted submissions per problem using token fingerprints that ignore renaming and formatting, and flags clusters for review before ratings change.
Flows to ask them to walk (5)
- Submit a solution and get a verdict: Store first, judge asynchronously in a sandbox, stream progress back. Each step is retryable without changing the verdict.
- The user submits code; the submission service stores it as queued.
- A judge job is put on the queue in the submit class.
- A worker pulls the job, loads the code and the cached tests, and starts a sandbox from a warm runtime image.
- The code is compiled, then run against each test with CPU-time and memory limits; output is compared by the problem’s checker.
- The worker reports the verdict; the submission row is updated and the user’s stream receives it.
- Run code against custom input: The interactive path: one or a few inputs, output shown to the user, higher priority than ordinary submissions because the user is actively waiting.
- The user clicks Run with the sample tests or their own input.
- It goes to the run class of the queue, ahead of ordinary submissions.
- A worker executes it in a sandbox and also runs the reference solution on the same input.
- Output, expected output and any error are streamed back.
- A contest with 50,000 people: The scale-breaking case: a load spike that is known in advance. Capacity is warmed before the start, contest jobs get priority, and the scoreboard is cached.
- Thirty minutes before the start, the worker fleet is scaled up and contest runtime images warmed.
- At the start, everyone opens the problems; statements come from the CDN and problem cache.
- Submissions pour in; contest jobs are pulled before everything else.
- Each verdict updates the contestant’s score and penalty in the live standings.
- The scoreboard is served from a few-second cache; the last hour can be frozen.
- A submission tries to break out: The failure and security path: fork bombs, infinite loops, giant outputs, network calls and attempts to read the tests are contained by layered limits.
- The program forks thousands of processes or loops forever.
- It tries to open a network connection or read files outside its scratch directory.
- It writes gigabytes to stdout.
- The worker destroys the sandbox and reports the verdict.
- If a sandbox or worker crashes outright, the job is retried on another worker.
- After the contest: plagiarism and ratings: The background path. Final standings are only published once copied solutions have been found and reviewed.
- The checker fetches every accepted contest submission per problem.
- It fingerprints each solution after normalising names and formatting, and finds suspiciously similar groups.
- Flagged groups go to review; confirmed cases are removed from the standings.
Deep dives · 16 min
Pick two. Ask the headline question, let them answer, then use the follow-ups. The follow-ups are where the level gets decided, so leave time for at least three of them.
Running hostile code safely
Ask: Every submission is code from a stranger. How do you run it without letting it escape, and still start 30,000 of them a minute?
Good answers name: MicroVM (Firecracker) or user-space kernel (gVisor) per submission, plus cgroups limits, seccomp, no network, read-only filesystem, Plain containers (namespaces + cgroups + seccomp), One full VM per submission, Language-level sandboxes (restricted interpreters).
Our pick: Each submission runs in a fresh gVisor sandbox or Firecracker microVM started from a warm per-language image, destroyed afterwards. Inside: no network interface; a read-only root filesystem and a small tmpfs scratch directory; tests fed through stdin, never mounted where the program can see them; cgroup limits on CPU time, memory and process count; a seccomp profile allowing only the syscalls compiled programs need; and a cap on output size. Worker hosts run nothing else sensitive and hold no credentials beyond pulling jobs and test data. AWS Lambda uses Firecracker for exactly this multi-tenant problem.
- How does the program get its input if the test files are not visible?
The worker, outside the sandbox, streams each test’s input to the program’s stdin and reads its stdout through a pipe. The expected output never enters the sandbox, so even a program that escapes its scratch directory has nothing to read. - What if someone finds an escape anyway?
Defence in depth limits the damage: the worker host has no secrets worth stealing, no access to the database except through narrow APIs, and is replaced regularly. Watch for submissions that crash workers or trigger unusual syscalls, and quarantine them. - Why not reuse a sandbox for the next submission to save start-up time?
Anything one program writes, leaves running or modifies could affect the next one, including its timing. Warm pools of fresh, never-used sandboxes give most of the speed without that risk. - How do you support 15 languages?
One image per language and compiler version, with the exact flags documented on the site, built and tested in CI against a set of reference solutions. Workers keep all images cached. Adding a language is adding an image and its time multiplier, not changing the judge. - How many sandboxes run on one host?
One per dedicated core pair, so a 64-core host runs about 30 at once. That is lower density than packing them tightly, and it is the price of fair timing; the fleet size estimate already assumes it.
The judge pipeline
Ask: Why a queue and workers instead of judging inside the request?
Good answers name: Store, enqueue with priority classes, pull-based workers, idempotent verdict writes, stream results, Judge synchronously in the API request, Push jobs to workers from a scheduler.
Our pick: The submission service writes the submission with status queued and enqueues a small job (ids only) into a durable queue with classes: contest, run, submit. Workers pull from the highest non-empty class, take a visibility lease on the job, judge, and write the verdict with a conditional update on the job attempt number, so a worker that was presumed dead cannot overwrite a later result. Unacknowledged jobs reappear after the lease, with a retry cap. The client follows progress over server-sent events, with polling as a fallback. Rejudging (after a test-set fix) is just enqueuing the same submission ids again with the new version.
- A problem’s tests were wrong and 20,000 submissions need rejudging. How?
Bump the test-set version, then enqueue those submissions in a low-priority rejudge class so live traffic is unaffected. Each rejudge writes a new verdict record linked to the version, and users are notified only if their verdict changed. - Why SSE rather than WebSockets for results?
Results flow one way, server to client, and SSE is plain HTTP that works through proxies and reconnects automatically. WebSockets would also work but add a bidirectional channel nobody needs here. - How do you show "you are number 340 in the queue"?
Per class, track a monotonically increasing enqueue counter and the counter of the last job started. The difference approximates the position. It does not need to be exact; it needs to show progress. - How would you know the judge is healthy?
Queue age per class (the oldest waiting job), verdict latency at p50 and p99, sandbox start time, worker crash and system-error rates, and how many verdicts change on rejudge of a control set of known solutions, which catches flaky timing.
Fair time limits
Ask: The same solution gets Accepted once and Time Limit Exceeded on a rerun. Why, and how do you stop it?
Good answers name: Measure CPU time via cgroups, pin each sandbox to dedicated cores, fixed-frequency hosts, per-language multipliers, rerun near-limit TLEs, Wall-clock time on shared cores, Count instructions instead of time.
Our pick: Limits are in CPU time measured by the cgroup, with a separate generous wall-clock cap to kill programs that sleep or block. Each sandbox gets dedicated cores (no hyper-threading siblings shared with another sandbox) on hosts with frequency scaling disabled. Each language has a calibrated multiplier, set by timing reference solutions. A TLE within 10 % of the limit is rerun once on a fresh sandbox before it is reported, and the runtime shown to users is the minimum across runs.
- Why have a wall-clock cap at all if you measure CPU time?
Because a program that sleeps, waits on a lock or blocks on input uses almost no CPU and would otherwise run forever. The wall cap (a few times the CPU limit) guarantees every sandbox ends. - How do you set Python’s time limit for a problem?
Time the official solution and a few accepted reference solutions in each language on the judge hardware, then set each language’s limit from those measurements with a margin, rather than one global multiplier. Problems where Python cannot pass with the intended algorithm should be flagged at authoring time. - Isn’t "beats 81 % of submissions" also noisy?
Yes, so compare against a distribution measured on the same hardware class and recalculated periodically, and show runtimes rounded. It is a motivational number, not a benchmark; never let it affect verdicts. - Memory limits: how are they measured fairly?
Peak resident memory from the cgroup, with the language runtime's baseline (a JVM or Python interpreter) subtracted or included in a per-language allowance. Without that, an empty Java program would use more memory than a full C++ solution.
Surviving a contest spike
Ask: Contest submissions jump to 9× normal in minutes. How do you have the capacity ready?
Good answers name: Scheduled pre-scaling from registrations + priority classes + visible queue position, Reactive autoscaling only, Throttle contest submissions per user.
Our pick: Thirty minutes before a contest, scale the worker fleet to the forecast (registrations × historical submissions per contestant, with margin) and warm the contest’s language images and test data on every worker. Contest jobs have the highest queue priority, so any shortfall delays practice traffic, not contestants. Statements and the scoreboard are served from caches. A per-user cooldown on contest submissions acts as a safety valve. After the contest, the fleet scales down on queue depth.
- The forecast was wrong and the queue grows anyway. What do contestants see?
Their queue position and an estimated wait, updated live, so a 30-second delay is understandable. Penalty time uses the submission time, not the verdict time, so waiting in the queue does not cost them ranking. - How do you release problems at exactly the start time?
Statements are stored in advance but served only when the contest is open, checked at the edge with a short-lived flag. Pre-warming the CDN is not possible for secret content, so origin capacity for statements is sized for the first minute. - What if a contest problem's tests turn out to be wrong mid-contest?
Fix the tests as a new version, rejudge every submission for that problem in the contest class, and recompute standings. Announce it, and extend the contest if many people were affected. Versioned tests make this a mechanical operation instead of a crisis. - How do you protect the judge from a bot submitting every second?
A per-user cooldown on submissions per problem at the gateway, plus a cap on queued jobs per user, so one account can never occupy more than a sliver of the fleet. Bots that rotate accounts are caught by registration checks and anomaly detection.
The live scoreboard
Ask: How do you rank 50,000 contestants by problems solved and penalty time, live?
Good answers name: Encode (solved, penalty) into one number in a sorted set; serve cached pages; optionally freeze the last hour, Recompute from the submissions table on each request, Recompute every minute in a batch.
Our pick: The contest service consumes verdicts and keeps per-contestant state (solved problems, attempts per problem, penalty). The rank key is solved × 10^7 − penalty_seconds, stored in a Redis sorted set, so ZREVRANGE gives the board and ZREVRANK gives anyone’s position. The public board is rendered and cached every few seconds. During the final hour the public board can be frozen while contestants still see their own verdicts; at the end, the hidden updates are revealed. Final standings are recomputed from the submissions table after plagiarism review, so the cache is never the source of truth.
- A rejudge changes a verdict after the contest. What happens to the standings?
Standings are derived from verdicts, so the contest service recomputes the affected contestants and the sorted set is rebuilt from the submissions table. Ratings are only computed after the final standings are confirmed. - Why does the encoding put solved count first?
Because one more solved problem must beat any amount of penalty. Multiplying solved by a number larger than the maximum possible penalty guarantees that, while still letting lower penalty win among equal solved counts. - How do you handle a contestant who submits the same accepted problem twice?
Only the first accepted submission per problem counts toward solved and penalty; later ones are recorded but ignored for scoring. The contest service keeps a per-problem state (unsolved, attempts, solved_at) so the rule is a simple check. - How do you reveal a frozen scoreboard?
Keep two views: the frozen public board (state at the freeze time) and the live internal board. After the contest, reveal the hidden updates problem by problem, or all at once, from the live state. Both are derived from the same verdict stream, so they cannot disagree.
Close · 5 min
Ask what breaks first at ten times the load, and what they would build next. Then give them your read: one thing that was strong, one thing that was missing, one thing to practise. Be specific; “good job” helps nobody.