Company questionsGroundworkMeta

System design · Platforms and search

Design a LeetCode-style coding platform

  • Reported once
  • 2 diagrams
  • 2 sources

One candidate write-up. Treat it as a single data point.

The prompt
Design an online platform where users read a problem, submit code in several languages, and get a verdict from hidden test cases. Assume 1 million daily active users and a weekly contest with 100,000 participants over two hours.

A representative prompt in the style reported, not a quote from Meta. The answer below is written for this site.

Try it first.The answer is covered until you pick. Talk it through against a 45 minute clock, or just read it.

Ask first

Clarify the problem before you draw anything.

Questions I would ask first: which languages and how many? Is the verdict synchronous or can the user wait a few seconds? Do contests need a live leaderboard and a fair, repeatable time limit? Are custom checkers needed (problems with many valid outputs)? Is plagiarism detection in scope? Who writes problems and test data?

Assumed scope: submit, judge against hidden tests, return a verdict (accepted, wrong answer, time limit, memory limit, runtime error, compile error), submission history, and a contest leaderboard. Out of scope: the problem editor, plagiarism detection, discussion forums, and the code editor in the browser.

Numbers

A back-of-envelope that decides the shape.

These are planning figures for the exercise, not facts about any real service.

  • Everyday load: 1 million daily active users × 3 submissions = 3 million a day. 3,000,000 ÷ 86,400 is about 35 a second; a 5× peak is about 175 a second.
  • Contest load: 100,000 people × 6 submissions = 600,000 over 7,200 s, about 83 a second on average. Submissions cluster in the first minutes and the last ten, so plan a 10× peak: about 830 a second.
  • Judge cost: assume 2 s of one core per submission (compile plus about 10 test runs). 830 × 2 = 1,660 busy cores. With 2× headroom that is 3,320 cores, about 210 workers of 16 cores each.
  • Database writes: three per submission (queued row, outbox row, final verdict) = 3 × 830 = about 2,500 a second, under the 5,000 write line even if outbox cleanup adds a fourth (3,320).
  • Database reads: a submission waits about 5 s and its client polls once a second, so 830 × 5 = 4,150 polls a second. A leaderboard seen by 100,000 viewers every 10 s is 10,000 reads a second, but it is served from a cached snapshot. Total about 14,000, under the 20,000 line.
  • Storage: 3 million source files × 5 KB = 15 GB a day in object storage; 3 million metadata rows × 300 B = 0.9 GB a day, about 330 GB a year.

The conclusion: one relational primary is enough. The scarce resource is sandboxed CPU, so the design is about the judge fleet, its queue and its isolation, not the database.

The design

Diagrams first, then components, data and the hard parts.

Submit to verdict, at a glanceThe API only stores and queues. The scarce resource is the judge fleet, so the queue has lanes and workers take jobs with a lease.
Browserpolls by submission idAPI + rate limiteridempotency keyObject storagesource + test bundlesSubmissions tableQUEUED row + outbox rowJudge queuecontest/practice/rejudgeJudge workerslease, sandboxResults consumerdedupe by submission idLeaderboardsorted setcode + keystore sourceone transactionrelay publishestake with leaseconditional verdictverdict eventfirst accepted only12345678
  1. The client posts the code with an idempotency key; the limiter allows a few submissions a minute per user.
  2. The API stores the source in object storage and keeps only a code_ref in the row.
  3. In one transaction the API inserts the QUEUED row and an outbox row, then returns 202 with the submission id.
  4. A relay publishes the outbox row to the judge queue, which has contest, practice and rejudge lanes.
  5. A worker pooled for that language takes the job with a lease.
  6. The worker writes the verdict where status != DONE, so the first verdict wins.
  7. The verdict becomes an event for the results consumer.
  8. On an accepted verdict the consumer updates the leaderboard, counting a user's first accept per problem once.

Open on the whiteboardAll 2 diagrams on one tidy board, with a step-by-step walkthrough.

Components. API service, submissions table (relational), object storage for source code and test bundles, a judge queue, a worker fleet, a results consumer, a leaderboard store, and a rate limiter in front of the API.

Data model. submissions(id, user_id, problem_id, language, status, verdict, runtime_ms, memory_kb, code_ref, created_at, contest_id). The primary key is a generated id; index (user_id, created_at desc) for history and (contest_id, created_at) for contest queries. Source code lives in object storage, referenced by code_ref, so rows stay small.

Write path.

  1. The client posts the code with an idempotency key, so a double click returns the same submission. The limiter allows a few submissions a minute per user (a token bucket keyed by user id).
  2. The API stores the source, inserts a row with status QUEUED, and writes a job to an outbox in the same transaction. A relay publishes it to the judge queue. The API returns 202 with the submission id.
  3. The queue has separate lanes: contest, practice, and rejudge. Lanes keep a practice backlog from delaying a contest. Workers are pooled per language runtime, because compilers and images differ.
  4. A worker takes a job with a lease, runs it in the sandbox, and writes the verdict with a conditional update (where status != 'DONE'). If the worker dies, the lease expires and the job is retried. Judging is repeatable apart from borderline time limits, and the conditional update keeps the first verdict, so a second run changes nothing.
  5. A results consumer updates the leaderboard on accepted verdicts, de-duplicated by submission id and counting only a user's first accepted submission per problem, so a replayed event changes nothing.

Read path. The client polls the submission by id with backoff, or listens on a server-sent stream. The leaderboard is a sorted set keyed by (problems solved, penalty), rebuilt from the table if lost.

Hard problem 1: running untrusted code. Assume every submission is hostile. Run it in a container hardened with a seccomp syscall filter, no network, a read-only root, a small writable tmpfs, an unprivileged user, and cgroup limits on CPU, memory, process count and output size. Containers share the host kernel, so for a stronger boundary use a user-space kernel sandbox or a microVM, and accept the startup cost and syscall overhead. Never run code in the API process, and never give the sandbox credentials. Wipe the sandbox after every run, so one submission cannot leave anything for the next.

Hard problem 2: fair verdicts under burst. Wall-clock limits are noisy on shared machines. Pin each run to a core, enforce CPU-time limits as well as wall time, and re-run a borderline time-limit failure once before reporting it. The contest start time is known, so scale the fleet and pre-pull test bundles (about 10 problems × 10 MB each) before it begins, instead of reacting to queue depth. Stop a test run at the first failure for practice submissions, and run all tests in a contest only if scoring needs it.

Trade-offs

What you chose, and when you would flip.

  • Container vs microVM sandbox. Containers start in milliseconds but share the kernel, so one kernel bug can escape. A microVM or user-space kernel isolates better and costs more per run. Pick the stronger boundary for a public platform running arbitrary code; flip to plain hardened containers only for a closed internal tool.
  • Polling vs push for verdicts. Polling is stateless and survives restarts; push saves requests but needs long-lived connections. With about 4,000 polls a second, polling is fine; flip to a stream when waiting clients reach the hundreds of thousands.
  • Queue autoscale vs scheduled scale. Reactive scaling lags a burst by minutes, which is exactly how long a contest start lasts. Pre-scale from the contest schedule, and keep reactive scaling for the unscheduled practice load.
  • Stop at first failure vs run every test. Stopping saves CPU; running all gives partial-credit scoring and better feedback. Stop early for practice, run all where scoring demands it.

Follow-ups to expect

Where the interviewer pushes next.

  1. How do you stop a submission that forks processes, allocates memory in a loop, or prints gigabytes of output?
  2. A worker crashes halfway through a contest submission. What does the user see and what is the retry story?
  3. How would you rejudge 200,000 old submissions after a test case is fixed without hurting live contests?
  4. How do you keep the leaderboard consistent if the results consumer processes an accepted verdict twice?
  5. Where would you add support for a language with a slow compiler, and how do you avoid unfair time limits across languages?

Where candidates slip

The mistakes this question punishes.

  • Running submitted code on the API servers or in a shared process, and treating sandboxing as a detail.
  • Sending everything through one queue, so a practice backlog delays a contest.
  • Sizing the database carefully and never sizing the judge fleet, which is the real bottleneck.
  • Using wall-clock time only, then getting flaky time-limit verdicts from noisy neighbours.
  • Writing every state change straight to the database, or storing source code as large blobs in the primary table.
  • Skipping idempotency and rate limits, so a double click or a script floods the queue.
  • Leaking hidden test inputs or expected outputs in error messages.

Have a go before you read.

Interviews reward the sketch you make cold. Clarify out loud, draw it, then compare with the answer.

Now make it stick

Where this was reported

Public pages only. Companies do not confirm these, and they rotate their questions, so treat each link as one person's account or one list's tag.

Read these chapters first