System design interview

Design an online code judge

Sandboxed runs, a submission queue, contest spikes and repeatable verdicts.

At a glance

Level
Intermediate
Topics
Queues and streams, Storage
Main parts
Submit API · Submission DB · Judge queue · Autoscaler · Judge worker · Test data · Sandbox
What you'll learn
Practise this problem
Overview
ClientSubmit APISubmissionDBJudge queueAutoscalerJudge workerTest dataSandboxno networkClientSubmit APIchecks, limitsSubmission DBsource, verdictsJudge queueabsorbs spikesAutoscalerwatches queueJudge workerTest datacached on workerSandboxno network

Press Play to follow a request through the design, or step through it.

  • Client and edge
  • Service
  • Data store
  • Cache and queue
  1. The user submits a C++ solution for a problem. The API checks the source size and the user's limit.
  2. The API saves the source with the status 'queued' and a new submission ID.
  3. It puts a small job, holding only the ID, on the queue and returns the ID to the user at once.
  4. An idle judge worker takes the job. If the worker dies, the job returns to the queue after a timeout.
  5. The worker compiles the code and runs each test in a fresh sandbox, with CPU, memory and time limits.
  6. It compares each output with the expected answer, decides the verdict and saves it.
  7. The API reads the verdict when the client asks for the status of the submission.
  8. The user sees 'Accepted' or the first failing test number.
  • Client
  • Submit API: checks, limits
  • Submission DB: source, verdicts
  • Judge queue: absorbs spikes
  • Autoscaler: watches queue
  • Judge worker: one job at a time
  • Test data: cached on worker
  • Sandbox: no network
On this page

In 2 minutes

Key ideas
  • The numbers. The contest peak is about 24 times a normal day, and it is known in advance.
  • Isolation. Hardened containers inside disposable worker VMs.
  • Timing. CPU time on a dedicated core, with a wall-clock backstop.
  • Scaling for contests. Scheduled scale-up plus a queue-age rule.
  • Test data. Versioned and cached on each worker.
Read the 2-minute answer

A judge has to run untrusted code safely, and it must survive a contest spike that is about 24 times a normal day. The Submit API validates the request, saves the source with a queued status, puts a small job holding only the submission ID on a durable queue, and returns at once. Judge workers, one job per dedicated core, take jobs, use test data cached locally by problem and version, and run the compile and every test in a fresh sandbox.

The sandbox has no network, a seccomp filter, cgroup limits for memory, processes and output, and both a CPU limit and a wall-clock limit. The worker compares output outside the sandbox and writes the verdict, keyed by submission ID, so redelivery is harmless. Sizing is the arrival rate times the time per job: 83 per second times 8 core-seconds is about 667 cores, so with headroom I would plan 55 machines of 16 cores, started on schedule before the contest, with a queue-age rule as backup.

A per-user cap and separate lanes keep it fair. Pinned toolchain images and re-runs of borderline timings keep verdicts repeatable. For a bad test case, I would publish a new data version and rejudge in a low-priority lane.

Step 1 of 6: Clarify the scope

Questions to ask first

Ask these before you draw anything. Guess why each answer changes the design, then open it to check.

Which languages do we support, and does each need a compiler?
Why it matters: Each language is a toolchain we must install, pin and sandbox. Compiled languages also add a compile step that is itself untrusted.
How many people take part in a contest, and how spiky is it?
Why it matters: The spike, not the daily average, decides how many workers we need and how the queue must behave.
Must the same code get the same verdict every time, even under load?
Why it matters: Fairness in a contest needs repeatable timing, which pushes us toward CPU time measured on a dedicated core.
How large is the test data for one problem, and who can see it?
Why it matters: Big test files must be cached on workers. Hidden tests must never leak to the code being judged.
Do users see a single verdict, or a result for each test?
Why it matters: Showing every test gives away the hidden data, so most judges show only the first failing test's number.

Requirements

Functional

  • Accept source code, a language and a problem, and return a submission ID at once.
  • Compile and run the code against every test case, within time and memory limits.
  • Return one verdict: Accepted, Wrong Answer, Time Limit Exceeded, Memory Limit Exceeded, Runtime Error or Compile Error.
  • Show progress and the final verdict live, and keep a history of every submission.
  • Rejudge past submissions after a faulty test case is fixed.

Non-functional

  • On a normal day, 95% of submissions get a verdict within 10 seconds.
  • During a contest, 95% wait under 30 seconds in the queue.
  • Untrusted code has no network, no shared files and no way to reach the host or other runs.
  • Running the same submission again gives the same verdict at least 99.9% of the time.

Step 2 of 6: Size it

Show the maths

These numbers are assumptions for the exercise, not facts about a real system:

  • A normal day brings 300,000 submissions.
  • A contest has 20,000 participants who send 120,000 submissions in 2 hours. The busiest moments run at 5 times that average.
  • Judging one submission takes 2 seconds to compile and 20 tests of 0.3 seconds each, on one dedicated CPU core.
  • There are 2,000 problems, and the test data for one problem averages 20 MB.

The arithmetic:

Submissions per second, normal day
300,000 ÷ 86,400 s ≈ 3.5
≈ 3.5/s
Submissions per second, contest
120,000 ÷ 7,200 s ≈ 16.7; × 5 for the peak
≈ 17/s on average, ≈ 83/s at the peak
Core-seconds per submission
2 s compile + 20 × 0.3 s
8 core-seconds
Cores busy at the contest peak
83/s × 8 core-seconds
≈ 667 cores; with 30% headroom ≈ 870, or 55 machines of 16 cores
Cores busy on a normal day
3.5/s × 8 core-seconds
≈ 28 cores, so the contest needs about 24 times more
All test data
2,000 problems × 20 MB
40 GB, which fits on a worker's local disk

Step 3 of 6: Sketch the path of one request

Step through the diagram at the top of the page to watch a request move through these parts.

The Client posts source code to the Submit API, which checks the size, the language and a per-user limit. It saves the source and a status of 'queued' in the Submission DB, puts a small job, holding just the submission ID, on the Judge queue, and returns the ID straight away. The user never waits for the judging.

A Judge worker takes a job from the queue. It fetches the problem's cases from Test data, or uses its local cache, and runs the code inside a Sandbox: first the compile, then each test with fixed limits. It compares the output outside the sandbox, decides the verdict and writes it to the submission DB.

An Autoscaler watches the queue's depth and the age of its oldest job, and adds or removes workers. For a known contest it adds them in advance. The client polls the API, or listens for updates, to see the status change from queued to judging to a final verdict.

Other paths through the system

Run the tests

  1. The worker asks for the problem's test cases, naming the problem and the test data version.
  2. Test data returns the files. The worker keeps them on local disk for the next submission to this problem.
  3. The worker starts the sandbox with the compiled program and feeds it one test input.
  4. The sandbox returns the program's output, the CPU time used and the peak memory, or a kill reason.
  5. After the last test, or the first failure, the worker writes the verdict.

Add workers before a contest

  1. The autoscaler asks the queue for its depth and the age of its oldest job.
  2. The queue answers. A contest starts in ten minutes, and the schedule already calls for 55 machines.
  3. The autoscaler starts the extra workers early, so they are warm and have test data cached.
  4. The new workers start taking jobs from the queue, and the wait stays short.

Step 4 of 6: Go deep on the hard part

The sandbox

InterviewerHow do you run code you do not trust, without letting it harm the host or other users?Assume every submission is hostile.

Assume every submission is hostile. It can fork until the machine is full, fill the disk, read other files, open the network, or look for a kernel bug. The compiler is also at risk, since a tricky source file can make it use huge memory or read files it should not. Layer the defences.

  • Isolation. Run each job in a container built from Linux namespaces: its own process list, mounts and an empty network, so there is no network at all. Add a seccomp filter that allows only the system calls a normal program needs, drop all privileges, and use an unprivileged user and a read-only root file system. A small writable scratch area is thrown away afterwards.
  • Resource limits. Use cgroups. Cap memory at, say, 256 MB with no swap, so an overrun is killed and reported as Memory Limit Exceeded. Pin the run to one core. Cap the number of processes at 64 to stop fork bombs, and cap output size so a loop that prints forever cannot fill the disk.
  • Two clocks. Enforce a CPU time limit, say 1 second, measured by the cgroup. Enforce a wall-clock limit of about 3 times that, to catch programs that sleep or wait for input. Either one gives Time Limit Exceeded.
  • A fresh sandbox every time. Never reuse one between users, so nothing is left behind to read.

Keep the hidden answers out of the sandbox. The program receives each input on standard input and nothing else. The expected output stays outside, where the worker compares it.

Containers share the host's kernel, so a kernel bug could let code escape. If the contest is public and the stakes are high, run each job in a micro-VM, which has its own kernel and a stronger wall, at the price of a slower start. A cheaper middle path: run the containers inside disposable worker VMs that hold no secrets and no credentials. Then an escape reaches nothing valuable.

The queue, the worker pool and fairness

InterviewerA contest starts and 80 submissions a second arrive. How do you keep up and stay fair?The Submit API does as little as possible.

The Submit API does as little as possible. It checks the source size and the language, applies a per-user limit, saves the source and puts a job on a durable queue that holds only the submission ID. It replies in milliseconds with the ID. The queue turns a sharp spike into a line the workers drain at their own pace.

Delivery is at least once. A worker hides a job while it works, and if it does not finish within a timeout, the job reappears for another worker. Judging takes tens of seconds, so the worker sends a heartbeat that extends the timeout. Because the same code and tests always give the same verdict, a duplicate run is harmless: the verdict is written once, keyed by the submission ID. A job that crashes its worker three times goes to a dead-letter queue for a person to review, and the user sees 'System error' rather than a hang.

Sizing. By Little's law, the cores busy are the arrival rate times the time per job. At the contest peak that is 83 per second × 8 core-seconds, about 667 cores. Add 30% headroom and you need about 870 cores, which is 55 machines of 16 cores. Judge one job per core, so tests do not compete for CPU and distort the timings.

Autoscaling that reacts to queue depth is too slow on its own, because a new machine takes minutes to start. For a known contest, scale on the schedule: bring machines up 15 minutes early, and warm their test-data caches. Keep the reactive rule as a backup, based on the age of the oldest job. If the queue's oldest job passes 30 seconds, add capacity.

Fairness. Cap how many of one user's jobs run at once, say 3, so a script that submits 200 times cannot starve everyone else. Use separate lanes for contest, practice and rejudge jobs, with the contest lane first. Show the user their place in line, and the progress, such as 'judging test 7 of 20'.

Repeatable verdicts, test data and rejudging

InterviewerHow do you make sure the same code always gets the same verdict, and fix a wrong test case later?Most verdicts are stable by nature.

Most verdicts are stable by nature. The trouble is borderline timing and moving tools, so remove the sources of drift.

  • Pin the toolchain. Each language runs from an immutable image, identified by a digest, with fixed compiler flags. Record the image on the submission. An upgrade is a new image, never an in-place change.
  • Measure CPU time, on a dedicated core. One job per core, no neighbours. Compare the time to a limit that was set from a reference solution, say 2 to 5 times its time, with a larger factor for slower languages.
  • Re-run the borderline. If a run lands within about 10% of the limit, repeat it up to three times and keep the best. A noisy machine then cannot turn a pass into a fail.
  • Special checkers. When a problem has many valid answers, run a checker program in its own sandbox. It reads the input, the jury's answer and the user's output.

Test data. Keep it in object storage with a version number per problem. A worker caches it on local disk by problem and version, and checks a checksum. The whole 40 GB fits on one disk, and a least-recently-used rule drops the oldest problems if space runs short. A new version means a new cache key, so there is no stale-data bug.

Rejudging. A test case is found to be wrong. Publish the corrected data as the next version, such as 5 in place of 4. Then a rejudge job finds the affected submissions, perhaps 30,000 of them, and puts them in the low-priority lane so live users are not delayed. Each new verdict is stored as a new row that names the test-data version. The old one stays in the history. Where the result changes in a contest, recompute the standings and announce it.

Step 5 of 6: Name the trade-offs

Isolation
Chose
Hardened containers inside disposable worker VMs
Over
A micro-VM for every run

Containers start fast and pack many jobs per machine, and the VM wall limits any escape. A micro-VM is stronger but slower to start and heavier to run.

Timing
Chose
CPU time on a dedicated core, with a wall-clock backstop
Over
Wall-clock time only

A busy neighbour cannot slow a run and cause a false Time Limit Exceeded. The cost is one job per core and more machines.

Scaling for contests
Chose
Scheduled scale-up plus a queue-age rule
Over
Reactive scaling on queue depth only

Capacity is warm when the first burst lands. The cost is paying for machines for a few idle minutes, and a calendar that must be correct.

Test data
Chose
Versioned and cached on each worker
Over
Fetched from storage for every run

Runs start fast and storage is spared a flood of requests. The cost is cache management and a version in every cache key.

Step 6 of 6: Say what breaks next

Follow-up questions an interviewer asks once the design is on the board. Answer each one out loud, then open it to compare.

How do you stop a submission from learning the hidden tests?
The sandbox sees only the current input on standard input, never the answer files, and it has no network to send anything out. Show users the first failing test's number, not its data. Limit how many submissions each user can send, since probing a test one guess at a time takes many runs.
How would you support interactive problems?
Run a judge program in a second sandbox next to the user's program. Connect their standard input and output with pipes, so the judge asks a question and the program answers. Count both programs' CPU time and use a wall-clock limit to catch deadlocks.
What happens if a worker dies in the middle of a job?
The job's timeout runs out and the queue makes it visible again. Another worker judges it from the start. The verdict is written once by submission ID, so a duplicate does no harm.
How do you catch copied solutions?
Do it offline, off the judging path. Turn each accepted solution into a sequence of tokens with names and spacing removed, and compare pairs inside a contest for long shared runs. Send the matches to a human reviewer, because a score alone is not proof.

Free ATS check

See your resume the way an ATS reads it, then fix what it misses.

Drop in a PDF for a score out of 100, every fix that cost a point, and the line it belongs to. It runs in your browser, with no signup for the score.