Design CodeSignal Online Assessment Tool: System Design & Architecture

Build an online coding assessment platform: recruiters assemble timed tests, candidates solve problems in a browser IDE, and the system compiles and runs their code - code we must treat as hostile - safely, fairly, and fast enough to feel interactive.

Scope

In Scope

  • Assessment creation: recruiters pick problems from a bank, set time limits, and get a shareable candidate link
  • The candidate experience: a browser IDE with problem statements, autosave, and a run/submit loop
  • Safe code execution: multi-language, fully isolated sandboxes with strict resource limits
  • Automated grading against test cases (visible examples plus hidden tests), producing verdicts and per-test results
  • Recruiter reports: scores, attempts, and the submitted code per problem

Out of Scope

  • Live video proctoring and behavior analysis (its own real-time media + ML system); we keep the audit-log hooks it would consume
  • Plagiarism detection internals (AST similarity, ML scoring): a specialized analysis pipeline - we store what it needs (all code, all attempts) and flag it as an integration point
  • Building the code editor itself: the browser IDE embeds an existing editor component (Monaco); we design what is around it, not the text editing
  • ATS integrations (Greenhouse, Lever), billing, and enterprise account management
  • A public practice/learning product (this design is for private, timed hiring assessments)

💡 What makes running a stranger's code dangerous?

Every other design in this practice set treats user input as data - bytes to store and serve. This system EXECUTES its user input. A submission is a program written by someone we have never met, under pressure, sometimes adversarial, and we compile and run it on our machines within seconds of receiving it. Think through what a hostile submission could try: read other candidates' code or the hidden test cases off the filesystem; open a network connection to fetch answers from a friend or an AI, or to exfiltrate our problem bank; fork-bomb the machine or allocate memory until the host dies; mine cryptocurrency on our CPUs; or plant a background process that tampers with the NEXT candidate's run. None of this is exotic - candidates have tried all of it on real platforms. So the central object in this design is the sandbox: a disposable, sealed environment where code can compute and nothing else. Almost every architectural choice - fresh container per submission, no network, read-only filesystem, hard resource caps, destroy after use - falls out of taking that threat model seriously. This is a security design that happens to grade code.

Functional Requirements

  • A recruiter can create an assessment (problems, time limit, window) and share a link with candidates
  • A candidate can open the link and solve problems in a browser IDE with syntax highlighting and autosave - a refresh never loses code
  • A candidate can RUN code against the visible example tests for quick feedback, and SUBMIT to be graded against the full suite including hidden tests
  • The system returns a verdict per submission - accepted, wrong answer, time limit exceeded, runtime error, compile error - with per-test detail
  • The assessment's time limit is enforced by the server: late submissions are rejected no matter what the client claims
  • A recruiter can view a report per candidate: scores, attempts, timing, and the actual code

Non-functional Requirements

  • Execution turnaround: p95 < 5 seconds from submit to verdict for typical problems - fast enough to keep a candidate in flow
  • Isolation is absolute: submitted code must never reach the network, the host, other submissions, or the hidden tests - a hard security requirement, not a quality bar
  • Verdict fairness: deterministic - the same code gets the same verdict regardless of how busy the platform is (limits measured in CPU time, resources pinned)
  • Scale: ~2M assessments/year, peak seasons with ~5K assessments running concurrently
  • Durability: submissions and results are never lost - they are the audit trail hiring decisions rest on
  • Availability: 99.95% during assessment windows - a candidate mid-interview cannot simply come back tomorrow

💡 Why "deterministic verdicts" is the requirement with teeth

A verdict here is not a UI detail - it can decide whether a person gets a job. That raises the stakes on a subtle failure mode: nondeterminism from load. Suppose time limits were measured in wall-clock time - "the program must finish within 2 seconds of real time." On a quiet Tuesday a correct solution finishes in 1.4s and passes. During peak hiring season the same host runs twenty containers, everything is slower, the same code takes 2.3s, and the candidate is rejected with Time Limit Exceeded. Same code, different career outcome, decided by our capacity planning. That is not a bug; it is an injustice. The fix is to measure what the candidate controls: CPU time (how much computing the program actually did) rather than wall time (how long it waited its turn), and to pin resources - a dedicated CPU allocation and fixed memory per sandbox - so the environment is identical at 2am and at peak. Add fixed language versions and container images, and "the same code gets the same verdict" becomes a property of the system rather than a hope. The p95 < 5 second turnaround matters for a softer reason: the run-tweak-run loop is how people actually solve problems, and a laggy loop measures patience instead of skill. Fast feedback is part of assessment validity, not just user experience.

Back-of-the-Envelope Estimates

  • Assume ~2M assessments/year; peak hiring seasons reach ~5K assessments running at the same time
  • Per assessment: ~3 problems over 90 minutes, with ~15 graded submissions plus ~30 quick "run" executions → ~45 executions per assessment
  • Peak execution rate: 5K concurrent assessments x 45 executions ÷ 90 minutes ≈ 2,500/min ≈ 42 executions/sec on average - but submissions cluster near deadlines, so bursts run 5-10x → design for ~400/sec
  • Each execution costs real compute: ~1 vCPU and 512MB for up to ~10 seconds → at a 400/sec burst with ~5s average runtime, that is ~2,000 sandboxes alive at once ≈ ~2,000 vCPUs of burst capacity, a few hundred at steady state - the fleet size is derived, not guessed
  • Storage is a footnote: code is ~10KB per submission → 2M x 45 x 10KB ≈ ~1TB/year of code; logs and artifacts add a few TB more. Nothing here challenges a database
  • The punchline of the numbers: 42/sec is laughably small next to Design a Newsfeed's 46K feed writes/sec - but each of these requests costs ~5 CPU-seconds and a fresh sandbox. The load is measured in CPU-seconds and isolation overhead, not requests per second

💡 What the estimates mean: the queue is the design, the database is a footnote

Most designs in this set are shaped by their data tier - sharding, caching, fan-out. This one is not. At ~1TB/year of code and double-digit QPS, a single PostgreSQL instance with a standby handles every table in this system without ceremony: assessments, sessions, submissions, results. If your instinct is to reach for NoSQL or read replicas here, the estimates say put it back. What the numbers do demand is elastic, isolated compute. The demand curve is brutally spiky - quiet weeks, then hiring season, then everyone submitting in the final five minutes of a 5,000-candidate assessment - while the supply (sandboxed CPU) is expensive to hold idle. The component that reconciles a spiky demand curve with a right-sized fleet is a queue: submissions land in it instantly (the candidate sees "queued" and then "running" - honest feedback instead of timeouts), workers pull at whatever rate the fleet sustains, and an autoscaler grows the fleet on queue depth. The queue also gives us the two control points this product needs: fairness (scheduling across assessments, so one giant client cannot starve everyone else) and reliability (a job leased to a runner that dies gets reclaimed and re-run). So the architecture inverts the usual proportions: the stateful tier is small and boring on purpose, and the engineering lives in the pipeline between "submit" and "verdict" - queue semantics, sandbox construction, resource governance, and verdict determinism. When the interviewer asks where this system scales, the answer is CPU-seconds, absorbed by a queue and an autoscaling fleet - not rows per second.

High-level Architecture

  • Web App + API Gateway: the recruiter console and candidate IDE; auth, session tokens scoped to one assessment, rate limiting on submissions
  • Assessment Service (PostgreSQL): assessments, the problem bank (with hidden tests that never leave the server), candidate sessions, and server-side deadline enforcement
  • IDE Backend: autosave (debounced to Redis, flushed to PostgreSQL), and a WebSocket channel streaming execution status back to the candidate
  • Job Queue (Redis Streams with a consumer group): every run/submit becomes a job; the consumer group delivers each job to exactly one runner, with leases so a dead runner's jobs are reclaimed
  • Runner Fleet (autoscaling on queue depth): workers that build a fresh sandbox per job - no network, read-only filesystem, pinned CPU/memory, hard time caps - run the tests, capture results, and destroy the sandbox
  • Results + Artifacts: verdicts and per-test outcomes in PostgreSQL; stdout/stderr and execution logs in object storage, access-controlled
  • Report endpoint: aggregates a candidate's submissions into the recruiter's report - small data, computed on read

Data/User Flow Diagram

CREATE
[Recruiter] --> [Assessment Service] --> [PostgreSQL: assessments,
                      |                   problems (hidden tests stay here)]
                      +--> shareable link

TAKE + EXECUTE
[Candidate browser IDE]
     | autosave (debounced)          | run / submit
     v                               v
[Session state] <-- deadline    [Submission API] --> [PostgreSQL:
 (server clock rules)                 |               submission, status=queued]
                                      v
                        [Job Queue (Redis Streams)]
                          consumer group: each job -> exactly one runner
                          leases: dead runner -> job reclaimed
                                      |
                                      v
                        [Runner Fleet (autoscales on queue depth)]
                          per job, every time:
                          1. take a FRESH sandbox (pre-warmed, used once)
                             - no network, read-only FS, pinned CPU/mem,
                               process cap, hard CPU-time limit
                          2. copy code in, compile (CE short-circuits)
                          3. run each test: stdin -> stdout, compare
                          4. capture verdict + outputs
                          5. DESTROY the sandbox
                                      |
                                      v
                 [PostgreSQL: results]   [Object storage: logs/stderr]
                                      |
                                      v
                 [WebSocket] ==queued / running test 3 of 8 / verdict==>
                                      candidate IDE

REPORT
[Recruiter] --> [Report endpoint] --> aggregate submissions + results

💡 What is a sandbox, in plain language - and how strong is a container really?

A sandbox is a sealed room where a program can compute and do nothing else. Ours is built from three kinds of walls the Linux kernel provides. Namespaces control what the program can SEE: it gets its own view of the filesystem (read-only, plus a scratch /tmp), its own process list (it cannot see or signal anything else on the machine), and - crucially - a network namespace with no interfaces, so "open a connection" fails instantly; there is nothing to connect through. Cgroups control what it can USE: at most this much memory (exceed it and the kernel kills the process - that is our memory-limit verdict), this much CPU, this many processes (a fork bomb hits the cap and dies quietly). Seccomp controls what it can ASK the kernel: system calls outside an allowlist - loading kernel modules, rebooting, raw device access - are refused before they start. Now the honest tradeoff. Containers share the host's kernel, so a kernel vulnerability is a potential escape hatch - rare, but real. The isolation ladder, in increasing strength and cost: plain containers → hardened containers (what we run: all three walls plus non-root and no network) → gVisor (a user-space kernel that intercepts syscalls) → microVMs like Firecracker (each sandbox gets its own tiny virtual machine and kernel, booting in ~100ms) → full VMs. Public cloud providers running arbitrary tenant workloads sit at the microVM rung; for assessment code with short lifetimes, no network, and nothing valuable on the host, hardened containers are a defensible choice - but say the ladder out loud in an interview, because knowing what you are NOT paying for is the point.

Data Model & Storage

  • Assessments (PostgreSQL): title, problem list, time limit, availability window, settings. Small, relational, created by the thousands - not the millions.
  • Problems (PostgreSQL): statement, starter code per language, resource limits, and test_cases as JSONB where each case carries a hidden flag. The hidden ones are the crown jewels - see the note below.
  • Sessions (PostgreSQL): one per candidate per assessment - started_at, deadline (computed server-side at start), current code per problem (the durable autosave target), and an event log (joins, saves, submissions) that doubles as the audit trail proctoring would consume.
  • Submissions (PostgreSQL): code, language, status, verdict, per-test summary, timing and memory numbers, and a pointer to full logs in object storage. Append-only in spirit: this table is the evidence hiring decisions cite.
  • Job Queue (Redis Streams): one entry per execution with a consumer group across the runner fleet. The queue is delivery, not truth - if it were lost, jobs re-enqueue from Submissions rows still marked queued.
  • Autosave buffer (Redis): the last few keystrokes' worth of code per session, debounced in, flushed to the Sessions row every few seconds - so a browser crash costs seconds of typing, not minutes.
  • Artifacts (Object Storage): stdout/stderr per test, compile output, execution traces - access-controlled, since candidate code is both their intellectual property and personally identifying.

💡 Why hidden test cases must never leave the server

Hidden tests exist to catch hardcoding: if a candidate can see every test, the degenerate strategy is to write if input == X: print Y for each one, which passes without solving anything. The hidden set is what makes "passes the tests" mean "probably solved the problem." The design consequence is a strict trust boundary: grading happens entirely server-side, and the API that serves problems to the candidate's browser must be shaped so hidden tests CANNOT appear in any response - not present-but-disabled, not behind a flag the UI hides, but absent from the payload. Anything that reaches the browser is public: candidates open devtools, inspect every response, and share what they find within hours. "The UI does not show it" is not a security property; "the endpoint never returns it" is. The same boundary governs verdicts: the runner compares outputs on the server and ships back pass/fail per hidden test - never the hidden input or expected output, even on failure. (Visible examples, by contrast, show full detail, because tight feedback on the examples is what makes the run loop useful.) This is the least glamorous note in this design and the most commonly botched in real products: the trust boundary is the API schema, not the interface.

API Design

POST /v1/assessments
Recruiter creates an assessment; returns the shareable candidate link.
POST /v1/assessments/:id/sessions
Candidate opens the link; creates their session, starts the server-side clock, returns problems (visible tests only).
PUT /v1/sessions/:id/code
Debounced autosave of the candidate's working code.
POST /v1/submissions
Run (visible tests) or submit (full suite); returns a submission id immediately with status queued.
GET /v1/submissions/:id
Poll a verdict (the WebSocket usually delivers it first).
WS /v1/sessions/:id/events
Live status stream: queued → running test N of M → verdict.
GET /v1/assessments/:id/report
Recruiter report: per-candidate scores, attempts, timing, and code.

Low-level Design

Session Service - a candidate joins (step by step):

  • 1) The link is validated against the assessment window; a session row is created with deadline = now + time_limit, computed from the SERVER clock - the timer in the IDE is a courtesy display, never the authority.
  • 2) Problems are served with visible example tests only; hidden tests never leave PostgreSQL.
  • 3) Autosave: keystrokes stay local, a debounced save lands in Redis every couple of seconds, and Redis flushes to the session row every few seconds - a browser crash costs seconds of typing.
  • 4) Every join, save, and submit appends to the session's event log: the audit trail a proctoring system (out of scope) would consume, and the record disputes are settled with.
  • 5) At the deadline, the server simply rejects further submissions - a candidate with a hacked client and a frozen timer changes nothing, because the clock that matters never left our side.

Runner - executing one submission (the genuinely tricky part, step by step):

  • 1) Submit creates the Submissions row (status=queued) and XADDs a job to the stream; the consumer group hands it to exactly one runner.
  • 2) The runner takes a FRESH sandbox from the pre-warmed pool: no network namespace, read-only root filesystem plus a scratch /tmp, pinned CPU allocation, 512MB memory cap, a process-count cap, non-root user, seccomp allowlist.
  • 3) Code is copied in and compiled; a compile error short-circuits to a CE verdict with the compiler's output.
  • 4) Each test case runs as: feed stdin, capture stdout, compare to expected - with per-test CPU-time and memory measured by the kernel's accounting, not a stopwatch.
  • 5) The first hard failure classifies the verdict (runtime error, time limit, memory); output mismatches mark wrong-answer; all-pass is accepted.
  • 6) Results land in PostgreSQL, full logs in object storage, the job is acknowledged (XACK), and the sandbox is DESTROYED.
  • 7) The WebSocket streams each step to the IDE - queued, running test 3 of 8, verdict - so even a slow grade feels alive rather than hung.

The pre-warmed pool - paying the isolation tax efficiently:

  • A fresh sandbox per submission is non-negotiable (next flow), but building one takes 100-300ms - noticeable inside a 5-second budget.
  • So a pool manager keeps N clean sandboxes per language pre-built and idling; a runner grabs one, uses it exactly once, destroys it, and the manager builds a replacement in the background.
  • The candidate pays near-zero startup; the system pays the build cost off the critical path.
  • Language images (compilers, runtimes, pinned versions) are pre-pulled onto every node - an image pull mid-job would blow the entire latency budget.
  • Pool depth scales with the same signal as the fleet: queue depth and the assessment calendar (hiring-season peaks are literally scheduled - pre-provisioning is allowed to cheat).

The queue - fairness and reliability under the deadline spike (step by step):

  • The load pattern is adversarial: submissions cluster in the final minutes of every large assessment, so bursts hit 10x average. The queue absorbs them; candidates see honest "queued (position 12)" feedback instead of timeouts.
  • Fairness: jobs are scheduled round-robin across assessments, so a 5,000-candidate corporate event cannot starve a 5-candidate startup's interview happening at the same hour.
  • Runs vs submits: quick "run" jobs get a modest priority boost - they are how candidates think, and keeping the think-loop fast matters more than shaving seconds off final grading.
  • Reliability: each delivered job carries a lease; a runner that dies mid-job stops heartbeating and the job is reclaimed (XAUTOCLAIM) and re-run elsewhere. Re-running is safe because result writes are idempotent per submission - at-least-once delivery plus idempotent receipt, the exact invariant from Design WhatsApp, here applied to compute jobs.
  • The queue is delivery, not truth: if the stream were lost entirely, every Submissions row still marked queued simply re-enqueues.

Grading - determinism as a feature (step by step):

  • Limits are enforced in CPU time, not wall time: the kernel reports how much computing the program did, so a submission never fails because the host was busy - the requirement note's "same code, same verdict" made real.
  • Memory verdicts come from the cgroup: exceed the cap and the kernel kills the process; we report memory-limit-exceeded rather than a mystery crash.
  • Every fairness-relevant input is pinned: language version, container image, CPU allocation, memory - the 2am environment and the peak-season environment are identical by construction.
  • Hidden tests report pass/fail only (never inputs or expected outputs); visible examples report full detail, because the run loop is where feedback teaches.
  • Everything is kept: every attempt, every verdict, every log - partly for recruiter reports, partly because a candidate disputing a verdict deserves an answer built on evidence, and partly as the data a plagiarism pipeline (out of scope) would consume.

Database Types and Schemas

Database Schemas (PostgreSQL throughout - the estimates say so)

- Assessments
  assessment_id UUID PRIMARY KEY,
  recruiter_id UUID NOT NULL,
  title VARCHAR(255),
  problem_ids UUID[] NOT NULL,
  time_limit_minutes INT DEFAULT 90,
  window_start TIMESTAMP, window_end TIMESTAMP,
  settings JSONB,                    -- allowed languages, attempts per problem
  created_at TIMESTAMP DEFAULT NOW()

- Problems
  problem_id UUID PRIMARY KEY,
  title VARCHAR(255), statement TEXT,
  starter_code JSONB,                -- { "python": "def solve():...", ... }
  limits JSONB,                      -- { "cpu_seconds": 2, "memory_mb": 512 }
  test_cases JSONB
    -- [{ "input": "...", "expected": "...", "hidden": true }, ...]
    -- hidden cases are NEVER serialized into any client-facing response;
    -- the trust boundary is this API shape, not the UI

- Sessions (one per candidate per assessment)
  session_id UUID PRIMARY KEY,
  assessment_id UUID NOT NULL,
  candidate_email VARCHAR(255),
  started_at TIMESTAMP NOT NULL,
  deadline TIMESTAMP NOT NULL,       -- server clock; the only timer that counts
  code JSONB,                        -- durable autosave target per problem
  event_log JSONB,                   -- joins, saves, submits: the audit trail
  UNIQUE (assessment_id, candidate_email)

- Submissions (append-only in spirit: the evidence table)
  submission_id UUID PRIMARY KEY,
  session_id UUID NOT NULL,
  problem_id UUID NOT NULL,
  mode run_mode_enum,                -- 'run' (visible tests) | 'submit' (all)
  language VARCHAR(20), code TEXT,
  status status_enum DEFAULT 'queued',  -- queued|running|done
  verdict verdict_enum,              -- accepted|wrong_answer|tle|mle|rte|ce
  tests_passed INT, tests_total INT,
  cpu_time_ms INT, memory_mb INT,
  logs_ref VARCHAR(512),             -- object-storage pointer
  submitted_at TIMESTAMP DEFAULT NOW(),
  INDEX (session_id, problem_id, submitted_at DESC)

Redis

- Job queue (Redis Streams)
  XADD exec-jobs * submission_id ... language ... limits ...
  Consumer group "runners": each job delivered to exactly one runner;
  leases + XAUTOCLAIM reclaim jobs from dead runners.
  The queue is DELIVERY, not truth - rows with status=queued re-enqueue
  if the stream is ever lost.

- Autosave buffer
  "code:{session_id}:{problem_id}" -> latest draft
  flushed to the Sessions row every few seconds

Object storage
  /artifacts/{submission_id}/compile.txt, test_{n}.stdout, test_{n}.stderr
  -- access-controlled: candidate code is their IP and their PII

API Request/Response Payloads

POST /v1/submissions
Request
{
  "session_id": "sess_8f3e2d1c",
  "problem_id": "prob_two_sum",
  "mode": "submit",
  "language": "python",
  "code": "def solve(nums, target):\n    seen = {}\n    ..."
}
Response
{
  "submission_id": "sub_7a2b8f4e",
  "status": "queued",
  "queue_position": 12
}

// Returns immediately - execution is asynchronous behind the queue.
// queue_position is honest feedback during deadline spikes: candidates
// see movement instead of a spinner. A submission after the session's
// server-side deadline is rejected here with 410, whatever the client's
// timer claimed.
WS /v1/sessions/:id/events (what the candidate sees live)
Response
{ "type": "status", "submission_id": "sub_7a2b8f4e",
  "status": "running", "current_test": 3, "total_tests": 8 }

// ...then, a few seconds later:

{ "type": "verdict", "submission_id": "sub_7a2b8f4e",
  "verdict": "wrong_answer",
  "tests_passed": 6, "tests_total": 8,
  "cpu_time_ms": 340, "memory_mb": 41 }
GET /v1/submissions/:id (per-test detail)
Response
{
  "submission_id": "sub_7a2b8f4e",
  "verdict": "wrong_answer",
  "tests": [
    { "test": 1, "hidden": false, "passed": true,
      "input": "[2,7,11,15], 9", "expected": "[0,1]", "output": "[0,1]",
      "cpu_time_ms": 12 },
    { "test": 7, "hidden": true, "passed": false,
      "cpu_time_ms": 45 }
  ],
  "logs_url": "https://artifacts.example.com/sub_7a2b8f4e/...(signed)"
}

// The trust boundary in action: visible tests return full detail
// (that is where feedback teaches); hidden tests return pass/fail and
// timing ONLY - never the input or expected output, even on failure.
GET /v1/assessments/:id/report (recruiter view, abridged)
Response
{
  "assessment_id": "a_9d4f1a2b",
  "candidates": [
    {
      "candidate_email": "dev@example.com",
      "completed_in_minutes": 74,
      "total_score": 2.5,
      "problems": [
        { "problem_id": "prob_two_sum", "best_verdict": "accepted",
          "attempts": 3, "best_cpu_time_ms": 120 },
        { "problem_id": "prob_intervals", "best_verdict": "wrong_answer",
          "attempts": 7, "tests_passed": "6/8" }
      ]
    }
  ]
}

// Computed on read - at this data size, aggregation needs an index,
// not a pipeline. Attempt counts and timing come straight from the
// Submissions evidence table.
Error responses (examples)
Response
// 410 Gone - the server-side deadline has passed
{
  "error": "session_expired",
  "message": "The assessment time limit has been reached"
}

// 429 - submission rate limit (per session)
{
  "error": "rate_limited",
  "message": "Please wait before submitting again",
  "retry_after_seconds": 5
}
// Rate limiting per session blunts both abuse and accidental
// submit-spamming during deadline panic.

💡 Three terms from the flows: consumer group, cgroup, CPU time vs wall time

Consumer group: the queue feature that turns "many runners reading one stream" into "each job handled by exactly one runner." The group tracks which member received which entry; a member that stops heartbeating has its unacknowledged jobs reclaimed by others (XAUTOCLAIM in Redis Streams). It is the same deliver-once-track-and-reclaim shape a work queue always needs, whatever the technology under it. Cgroup (control group): the kernel mechanism that caps what a group of processes can consume - memory, CPU share, process count. The cap is enforced by the kernel itself: exceed the memory limit and the process is killed mid-allocation; hit the process cap and the fork bomb's next fork just fails. Our memory and fork-bomb defenses are one configuration line each, because the kernel does the enforcement. CPU time vs wall time: wall time is what a stopwatch measures - including every moment the program sat waiting for its turn on a busy host. CPU time counts only the computing the program actually did. Verdict limits use CPU time so that platform load can never fail a candidate; turnaround targets (p95 < 5s) use wall time, because the candidate's experience of waiting is exactly what wall time measures. Same clock on the wall, two different questions.

Concluding Discussion

Potential Bottlenecks

  • Issue: The deadline spike - thousands of candidates in one scheduled assessment submitting in the final five minutes, a 10x burst by construction.
    Mitigation: the queue absorbs it with honest position feedback, the fleet autoscales on depth, and pre-warmed sandbox pools are deepened ahead of scheduled events - hiring seasons are on a calendar, so pre-provisioning is allowed to cheat.
  • Issue: Heavy languages hog the fleet - a Java compile plus JVM startup can cost 10x a Python run, so mixed traffic starves the cheap jobs behind the expensive ones.
    Mitigation: per-language resource profiles and separate worker pools/queues, so a wall of JVM jobs queues against itself, not against everyone.
  • Issue: Sandbox construction (100-300ms each) eats the latency budget at burst rates.
    Mitigation: the pre-warmed pool moves construction off the critical path - built ahead, used once, destroyed, replaced in the background.

Single Points of Failure

  • The Redis job stream: if it is lost, no jobs flow - but no work is lost either, because the queue is delivery, not truth: every Submissions row still marked queued re-enqueues on recovery. Run it replicated; the failure mode is minutes of delay, never a lost submission.
  • PostgreSQL: the evidence store - submissions, verdicts, sessions. Hot standby with failover; autosave buffers in Redis and browser-local code mean even a failover window costs candidates seconds of typing, not work.
  • The runner fleet is naturally redundant - any runner can take any job, and leases reclaim work from dead ones. The subtle single point is the pool manager and image registry: pre-pull images to every node so a registry outage degrades pool refill speed, not execution itself.

Security Improvements

  • Defense in depth on the sandbox: no network namespace, read-only filesystem, cgroup caps, seccomp allowlist, non-root, process limits - each wall blocks a different attack class, and the design assumes any single wall can fail.
  • The trust boundary is the API schema: hidden tests and expected outputs are never serialized to the client, and verdicts are computed exclusively server-side.
  • Candidate code is treated as both IP and PII: artifacts are access-controlled, links are signed and expiring, and recruiter access is scoped to their own assessments.
  • Session event logs (joins, saves, submit timing, paste bursts) are append-only - the audit surface that grading disputes, and any future proctoring integration, stand on.

Critical Knowledge

If the interviewer asks "how do you safely run code you do not trust?":

  • Enumerate the walls and what each blocks: no network namespace (no cheating mid-test, no exfiltrating the problem bank), read-only filesystem (no tampering, nothing to steal), cgroups (no memory bombs or fork bombs - the kernel enforces the caps), seccomp allowlist (no dangerous system calls), non-root user.
  • Then the two operational rules: a FRESH sandbox per submission, destroyed after use, and defense in depth - assume any single wall can fail.
  • Close with the honesty ladder: containers share the host kernel, so the escalation path is hardened containers → gVisor → microVMs (Firecracker) → VMs, trading startup speed for isolation strength. Cloud providers running arbitrary tenant code sit at microVMs; short-lived, network-less assessment code justifies hardened containers - knowing which rung you are on, and why, is the answer.

If the interviewer asks "why put a queue between submit and execution?":

  • Because demand is spiky by construction (everyone submits at the deadline) while sandboxed CPU is expensive to hold idle - the queue reconciles a 10x burst with a right-sized fleet, and autoscaling reads queue depth as its signal.
  • It converts overload into honest feedback: "queued, position 12" instead of timeouts.
  • It is the fairness control point: round-robin across assessments so one 5,000-candidate event cannot starve a small interview.
  • And it is the reliability mechanism: leases plus reclaim (a dead runner's jobs re-run elsewhere), safe because result writes are idempotent per submission - at-least-once delivery plus idempotent receipt, the same invariant as Design WhatsApp, applied to compute jobs.

If the interviewer asks "why destroy every container instead of reusing them?":

  • Reuse leaks state in both directions: the previous submission may have planted a background process or poisoned /tmp (attacking the NEXT candidate), and its leftovers - memory pressure, lingering processes - can distort the next run's timing into an unfair verdict.
  • So the rule is: fresh sandbox, one use, destroy - correctness and security both demand it.
  • The cost is 100-300ms of construction per job, and the fix is the pre-warmed pool: build ahead of need, off the critical path, use once, replace in the background.
  • The transferable principle: reuse is an optimization you purchase with correctness risk. Here the verdict IS the product, so the purchase is refused - and the latency is bought back with preparation instead.

If the interviewer asks "how do you make verdicts fair?":

  • Name the failure first: wall-clock limits make verdicts depend on platform load - the same code passes at 2am and fails TLE at peak, and that difference can cost someone a job.
  • The fixes: measure CPU time (what the candidate's code actually computed, reported by the kernel), pin resources per sandbox (dedicated CPU allocation, fixed memory), and freeze the environment (language versions, images) so every run is identical by construction.
  • Memory verdicts come from the cgroup kill, not a crash heuristic.
  • Frame it the way the requirement deserves: determinism here is not engineering hygiene - a nondeterministic verdict is an unfair hiring decision, so "same code, same verdict" is an ethical property of the system.

If the interviewer asks "is this not just a CI system?":

  • Nearly - and recognizing that is the strong answer. The skeleton is identical: jobs in a queue, isolated runners, results and artifacts out - one platform pattern, "execute code on demand, safely, at scale."
  • The knobs differ by trust and purpose. Trust: CI runs your own team's code (semi-trusted, so it caches dependencies and workspaces aggressively for speed); assessments run strangers' code under incentive to cheat (zero trust, so nothing is cached or reused and everything is destroyed).
  • Structure: CI jobs form dependency graphs (build, then test, then package); submissions are independent, so scheduling is trivially parallel.
  • Stakes: CI optimizes developer minutes; assessment optimizes verdict fairness.
  • See Design a CI System for the same skeleton with the trust dial turned up and the caching dial turned on.