The system design question
Design a coding contest platform like LeetCode's weekly contest. Thousands of people join a timed contest, submit code that's run against tests, and see a live leaderboard.
Agree on the scope (5 minutes)
Say up front what you're covering.
| We'll design | We'll skip |
|---|---|
| Starting a contest | Writing problems |
| Submitting and judging code | Cheating detection |
| The live leaderboard | Ratings after the contest |
| Handling the rush | Payments |
Then agree on what "good" means:
- Fair: everyone starts at the same time, and judging order doesn't change anyone's score.
- Safe: people's code can't break our servers or see other people's code.
- Quick results: most submissions should be judged within seconds.
- Live leaderboard: the leaderboard should update within a few seconds.
Rough numbers (5 minutes)
Made up, but reasonable: 50,000 people, 4 problems, a 90-minute contest, and about 3 submissions each per problem.
- 600K
- submissions per contest
- ~500
- submissions per second in the final rush
- ~1,000
- CPU cores busy judging at the peak
- ~5K
- leaderboard requests per second
| Question | Rough math | What it means |
|---|---|---|
| How much judging? | 500 a second × about 2 seconds of running each | About 1,000 cores at once, so judging is the expensive part |
| How busy is the leaderboard? | 50,000 people refreshing every 10 seconds | About 5,000 reads a second |
| What happens at the start? | 50,000 people loading problems at once | A huge spike, but it's the same 4 pages for everyone |
The overall design (10 minutes)
- Problems are hidden until the start, then served from a cache close to users.
- When someone submits, we save the code and put a 'judge this' message on a queue.
- Runners take submissions off the queue and run them against the tests, each in its own locked-down container.
- The result is saved and the participant is told.
- If it passed, the participant's score and leaderboard position are updated.
What the apps need to do
| Action | Who does it | What's sent |
|---|---|---|
| Register for a contest | Participant | Contest ID |
| Get problems | Participant | Contest ID, once it's started |
| Submit code | Participant | Problem, language, code and a submission ID |
| Check a submission | Participant | Submission ID, and we return its result |
| View the leaderboard | Anyone | Contest ID and a page, or 'around me' |
What we store and where
| Data | Where it goes | Why |
|---|---|---|
| Submissions and results | Relational database | The permanent record of what happened |
| Waiting submissions | Queue | Lets judging catch up with bursts |
| Leaderboard | Sorted set in memory | Fast to update and to read ranks |
| Problems and tests | Storage, cached near users | Same for everyone, read constantly at the start |
Step 1: Judging submissions (15 minutes)
Never run code in the web request. Save it, put a message on a queue, and return 'pending'. Runners pull from the queue, so when the end-of-contest rush hits, submissions just wait a bit longer and nothing falls over.
Every submission runs in its own container with no network access and strict limits on time, memory and CPU. Someone will try something nasty, and it shouldn't matter.
Score by submission time, not by when judging finishes, so a backed-up queue never costs anyone points.
Judging twice
If a runner crashes, its message goes back on the queue and another runner judges it again. Saving the result by submission ID means a repeat overwrites the same result instead of counting twice. That's idempotency.
Step 2: The live leaderboard (10 minutes)
Keep a sorted set in memory, like Redis, with each participant's score. Most contests rank by problems solved, then by total time with penalties, which you can combine into one number. Updating a score and finding someone's rank are both quick.
Most people look at the top of the board or their own spot. Cache the top pages for a few seconds, so thousands of refreshes are one read.
If the in-memory leaderboard is lost, rebuild it from the saved results.
Step 3: The rush at the start (5 minutes)
Have people register beforehand, so the start is just loading problems. Problems are the same for everyone, so serve them from a cache close to users the moment the clock starts.
Add judging capacity before the contest starts, not after the queue backs up. Rate limit submissions per person, say one every 10 seconds, so nobody floods the queue.
Choices you should be able to defend
| We chose | Instead of | Because | We'd reconsider if |
|---|---|---|---|
| A queue and runners | Running code in the request | Bursts wait instead of crashing things | Results needed to be instant and traffic was steady |
| Scoring by submission time | Scoring by when judging finished | A slow queue never costs anyone points | We had no reliable submission timestamps |
| An in-memory sorted set | Ranking with a database query | Updates and rank look-ups are instant | The contest was small enough not to matter |
| Containers | Full virtual machines | They start faster and use less | We needed stronger isolation for some languages |
What could go wrong (5 minutes)
| What happens | What we do about it |
|---|---|
| A runner dies mid-judge | The message goes back on the queue and another runner judges it |
| The queue backs up | Show 'pending' and add runners. Scores use submission time, so it's still fair |
| The leaderboard store is lost | Rebuild it from the saved results |
| Someone submits malicious code | It's stuck in a locked-down container with no network and strict limits |
| A test case is wrong | Fix it and rejudge that problem's submissions from the queue |
Follow-up questions (5 minutes)
How would you catch cheating?
After the contest, compare submissions for suspicious similarity and flag them for review.
How would you update ratings?
Once the final standings are locked, run a batch job that updates everyone's rating.
What about contests across the world?
Run runners in several regions, but keep one leaderboard as the source of truth.
How the 60 minutes add up
| Minutes | What you're doing |
|---|---|
| 0–5 | Agree on what you're building |
| 5–10 | Rough numbers |
| 10–20 | Draw the overall design |
| 20–35 | Judging submissions |
| 35–45 | The live leaderboard |
| 45–50 | The rush at the start |
| 50–55 | What could go wrong |
| 55–60 | Follow-up questions |
Key concepts used here
- Message queues and event streamsKeep services from depending on each other, and handle traffic spikes.
- Idempotency and retriesMake sure a retried request doesn't happen twice.
- Caching strategiesMake reads faster, and figure out what to do when data changes.
- Rate limitingStop too much traffic with token buckets and sliding windows.
- Real-time deliveryPush updates to users the moment they happen, with WebSockets and friends.
Further reading
We wrote this sample answer ourselves.
Practice Loop is independent and isn't affiliated with or endorsed by Meta.