InterviewKata
System design · Worked design D27 of 46 · Data and infrastructure · Long form

Design an online code judge

A coding-practice site lets anyone paste a program, runs it against hidden tests, and says "Accepted" or why not, in seconds. Every submission is code written by a stranger, executed on your machines. And once a week a contest makes a hundred thousand people submit at the same moment.

1. The prompt

"Design an online judge: users solve programming problems in the browser, submit code in many languages, and get a verdict from hidden test cases. Weekly contests have a live leaderboard."

What makes it hard:

2. Frame the problem

How many languages?

About 20: C++, Java, Python, Go, JavaScript and others.

Each needs its own compiler or runtime image and its own time-limit multiplier.

How many users and submissions?

10 M registered users; about 2 M submissions a day outside contests.

~25 submissions a second on average: modest. The contest peak is what sizes the system.

How big are contests?

Up to 100,000 participants, 90 minutes, four problems.

A known start time means capacity can be added before it begins.

How many tests per problem, and limits?

Up to ~100 hidden tests; 1–2 s CPU and 256 MB memory per test.

A submission is up to ~100 runs; most of the cost is executing tests, not the API.

How fast must the verdict come?

A few seconds normally; under 15 s during contest peaks.

Users wait with the page open, so a queue position and live progress matter.

Is there a quick "Run" with custom input?

Yes, against sample tests or the user's own input.

A separate fast lane, so practice runs don't wait behind full submissions.

What happens if a test case turns out to be wrong?

Fix it and rejudge affected submissions, contest ones included.

Submissions and test sets must be versioned, and scores recomputable.

3. What it must do

  1. A user can submit code and get a verdict (accepted, wrong answer, time or memory limit, runtime or compile error) with how many tests passed.
  2. A user can run code against sample or custom input and see the output quickly.
  3. Contests rank participants live by problems solved and time taken.

Not now: discussion forums and editorials (ordinary content), payments, mock interviews with video, AI hints.

4. How well it must do it

TargetWhy this number
Contained: user code can never reach the network, other submissions, credentials or the host securityOne escape could leak hidden tests, user data or the whole fleet.
Verdict time: p95 under 5 s normally, under 15 s at contest peak speed · load shapePeople wait on the page; contests compete on minutes.
Fair: the same code gets the same verdict, with runtimes within ~5% data qualityA flaky time limit is the most common complaint about judges.
Correct: never accept wrong code; every verdict is reproducible consistencyRankings and reputations depend on it.
Contest availability: 99.95% during contests, leaderboard within seconds surviving failuresAn outage mid-contest can't be undone.

5. Numbers that change the design

QuantityWorking→ so we…
Work per submissionUp to 100 tests × ~0.2 s CPU on average ≈ 20 CPU-seconds (less when it fails early)Execution, not the API, is the cost → so judging runs on a separate worker fleet
Contest peak100 K people, a surge in the last 15 minutes to ~300 submissions a second300 × 20 CPU-s ≈ 6,000 busy cores → so we pre-scale before the start; autoscaling would arrive too late
Sandbox start-upA container takes ~1 s to start; a microVM boots in under 125 msAt 300 a second, start-up overhead matters → so we keep warm pools of sandboxes
Test data3,000 problems × 100 tests × ~1 MB ≈ 300 GB→ so test sets live in blob storage, versioned, and are cached on worker disks

Skip estimating problem-page traffic: static content behind a CDN.

6. The nouns

problems     problem_id, title, time_limit_ms, memory_mb, test_set_version
test_sets    problem_id, version, object_key (blob), checker (exact | float | special program)
submissions  submission_id, user_id, problem_id, contest_id?, language, code_key (blob), test_set_version,
             status (queued | running | done), verdict, tests_passed, runtime_ms, memory_kb, created_at
scores       contest_id, user_id, solved, penalty_minutes, last_accepted_at    (source of truth; leaderboard derived)

7. The contract

POST /v1/problems/{id}/submissions  {language, code}   Idempotency-Key: …
     → 202 {submission_id, queue_position}
GET  /v1/submissions/{id}           → {status, verdict, tests_passed, runtime_ms, memory_kb}
SSE  /v1/submissions/{id}/events    → queued … running test 37/100 … done
POST /v1/problems/{id}/run  {language, code, input}   → {stdout, stderr, runtime_ms}   (fast lane)
GET  /v1/contests/{id}/leaderboard?page_token=…       → [{rank, user, solved, penalty}]

Data flow: submit → save code and a queued row → queue by lane → a judge worker takes it → sandbox: compile, run each test with limits, compare output → verdict → update the row → notify the browser → contest scorer updates the leaderboard.

8. First working version

An API, a database and blob storage, one queue, and a pool of judge workers that run code inside containers.

8.1 Submitting and judging

The API stores the code, creates the row, and enqueues only the submission ID (Module 23). A worker loads the code and the test set, compiles, runs each test with time and memory limits, compares the output with the expected answer (or a special checker for problems with many correct answers), and stops at the first failure.

8.2 Getting the verdict

The browser listens on a server-sent events stream (Module 19) for progress ("running test 37 of 100"), falling back to polling. Verdicts are written once per submission and test-set version, so a retry never produces a second, different verdict.

8.3 Contest scores

When a contest submission is accepted, the scorer updates the participant's row (problems solved, penalty minutes) in one transaction, then the leaderboard.

Parked for latercontainers alone aren't a strong boundary (9.1) · noisy neighbours make timing unfair (9.2) · the contest surge (9.3) · test data and runtimes on every worker (9.4) · leaderboard at 100 K entries (9.5) · rejudges and cheating (9.6).

9. Make it hold up

9.1 Running untrusted code safely contained

ApproachWhy
✗ BadRun the program as a process on the workerIt can read the worker's files and credentials, open network connections, fork until the machine dies.
~ GoodA container per run: no network, read-only file system, an unprivileged user, a seccomp filter on system calls, cgroup limits on CPU, memory and process countStops ordinary abuse. But containers share the host kernel, so a kernel bug can be an escape.
✓ GreatAll of that inside a stronger boundary: a user-space kernel (gVisor) or a lightweight microVM (Firecracker), destroyed after each submission; judge workers in an isolated network with no credentials beyond reading test dataDefence in depth: an escape from the program still lands in a throwaway VM on a machine that holds nothing valuable.

Also: never show the output of hidden tests, cap stdout size, and limit error messages so failing submissions can't print the test data out.

9.2 Fair, repeatable timing fair

9.3 Surviving the contest surge verdict under 15 s

ApproachWhy
✗ BadOne queue, autoscaling on CPUNew machines arrive minutes after the surge; practice submissions compete with contest ones; queues explode in the final minutes.
~ GoodAutoscale on queue depth, with a warm pool of sandboxesReacts faster and avoids start-up overhead, but still behind a step change.
✓ GreatPre-scale the contest fleet before the start (the schedule is known), give contests their own queue and workers, keep warm sandboxes, allow one pending submission per user, and show queue positionCapacity is there before demand; practice can't slow the contest; users see progress instead of retrying. Try it in section 13.

9.4 Test data and runtimes on every worker verdict time

9.5 A live leaderboard for 100,000 people leaderboard within seconds

9.6 Rejudges and cheating correct

Rejudge after fixing a test: create a new test-set version, re-queue affected submissions on a low-priority lane, recompute scores from verdicts, rebuild the leaderboard. Plagiarism is checked after the contest by fingerprinting normalised code (stripping names and whitespace) and comparing submissions per problem; matches go to human review.

9.7 The whole design after the deep dives

10. Interviewer follow-ups

Someone escapes the sandbox.

Defence in depth limits it: the microVM is thrown away, the worker holds no credentials and can't reach other networks, and workers are rebuilt from images regularly. Detect it with system-call auditing and alerts on unexpected network attempts; patch and rotate the fleet.

Add an interactive problem (the program talks to a judge program).

Run the interactor in a second sandbox, connected to the user's program by pipes only, with a time limit on the whole exchange. The checker becomes a program too, versioned with the test set.

A test case was wrong during a contest.

Publish a new test-set version, rejudge every contest submission for that problem on a dedicated lane, and recompute scores from the database. Announce it; freeze the board until it's done.

Ten times the contest size.

The fleet is the only part that scales with it: pre-scale more cores (or several contest pools by region), stagger the start by region if needed, and keep one pending submission per user to stop retry floods.

Cut the bill.

Practice judging on cheaper interruptible capacity (spot instances: a lost run is just re-queued), contests on reserved capacity, early exit on the first failing test, and caching of compiled standard libraries in images.

Show "your runtime beats 87% of submissions".

Keep a per-problem, per-language histogram of accepted runtimes, updated in the background; look up the percentile when a verdict arrives. It's approximate by design.

Users in Asia see slow verdicts.

Run judge fleets in several regions, routing submissions to the nearest; test data and images are replicated. Contest scores still go to one primary database (or one per contest).

Stop people retrying the same submission 20 times.

One pending submission per user per problem, an idempotency key on submit, and identical-code detection that returns the earlier verdict.

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
Worker dies mid-judgingA slightly slower verdictVisibility timeout expiresThe submission is re-queued; verdicts are idempotent per version
Queue backlog in a contestLonger wait, with queue positionQueue depth and oldest-message agePre-scaled fleet; contest lane isolated; one pending per user
Bad language imageWrong compile errorsCanary rejudge of known submissionsStaged rollout of images; instant rollback
Wrong test caseCorrect code rejectedReports; unusual failure rates on one testVersioned test sets and rejudges
Leaderboard cache lostBoard briefly staleBoard ageRebuild from the scores table
Abuse attemptNothingBlocked system calls, network attemptsSandbox layers; ban the account

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch: verdict latency by lane · queue depth and oldest age · sandbox start time · runtime variance on reference solutions · blocked-syscall alerts · leaderboard age.

At 10×: more pre-scaled capacity and regional fleets; the API, database and leaderboard barely change.

A 60-minute interview

0–5Frame: languages, limits, contest size, verdict time
5–10Must do, how well, CPU-seconds per submission
10–15Nouns, contract, data flow
15–25First version: submit → queue → worker → verdict
25–50Deep dives: the sandbox (9.1), fairness (9.2), the surge (9.3)
50–60Leaderboard or rejudges, follow-ups, wrap-up

A 90-minute interview

0–8Frame, including rejudges and fairness
8–15Must do, how well, numbers
15–22Nouns, contract, data flow
22–35First working version
35–75Deep dives 9.1–9.6 and the final picture
75–85Follow-ups: escape, interactive problems, cost
85–90Trade-offs, metrics, 10×
LevelWhat a strong answer shows
Mid-levelAsync judging through a queue and workers, containers with limits, a job status the browser polls or streams, a sorted-set leaderboard.
SeniorLayered sandboxing (seccomp, cgroups, no network, microVMs), CPU-time fairness, pre-scaling and separate lanes for contests, versioned test sets and reproducible verdicts.
Staff+Threat model and defence in depth, warm-pool economics, rejudge and recompute strategy, capacity planning for known peaks, operational readiness for contest day.

Common mistakes:

13. Try it, check yourself, read more

Contest-day simulator. A 90-minute contest: 60 submissions a second, surging to 300 a second in the last 15 minutes, on top of 25 a second of practice. Each submission needs 20 CPU-seconds, spread over parallel test runs (about 3 s of wall time when there's no queue). Pick how the fleet behaves and watch the wait for a verdict (red) and the fleet size (dashed).

Model: autoscaling adds up to 15% capacity a minute after a 3-minute delay; a cold sandbox adds 3 CPU-seconds and 2 s of start-up to each submission; with separate lanes, contest submissions go first and practice uses the spare capacity. Illustrative numbers.

Why isn't a container with seccomp and no network enough on its own?

A microVM or a user-space kernel adds a second boundary; the throwaway, credential-free worker adds a third.

What makes time limits fair?

Wall time includes waiting for neighbours; CPU time on an isolated core is repeatable.

The contest starts at 15:00 sharp. How do you avoid a verdict backlog?

The start time is known; autoscaling reacts minutes too late, and practice traffic shouldn't compete.

How can every verdict be reproduced months later?

Immutable, versioned test sets plus the stored code make any verdict replayable.
ResourceWhy
Firecracker: Lightweight Virtualization for Serverless Applications (NSDI 2020)How microVMs give VM isolation with container-like start-up.
Firecracker and gVisorThe two common stronger boundaries for untrusted code.
nsjailA process jail combining namespaces, cgroups and seccomp, used for exactly this kind of workload.
seccomp(2) and cgroup v2The Linux building blocks for system-call filtering and resource limits.
Judge0An open-source code execution system to read and run.