Design LeetCode


Patterns, Technologies, and Concepts

LeetCode

Key Takeaways

Requirements

You initially missed a key latency requirement for code submission but quickly corrected it in your second attempt, finishing with a strong and well-justified set of non-functional requirements.

  • When a user is actively waiting for a result, that interaction needs an explicit latency target. For a code execution system like LeetCode, 5 seconds is the upper bound for submission feedback before the experience feels broken. Always ask yourself who is waiting and for how long when defining latency requirements.

  • Eventual consistency is a spectrum, not just a binary choice. For leaderboards, you can say reads tolerate up to 1 minute of staleness. This is more precise than saying ‘real-time if possible’ and directly ties back to your overall consistency stance, making your design reasoning cleaner and easier to defend.

Core Entities

You initially missed the User entity in your data model but corrected it on your second attempt, showing good adaptability when given feedback.

  • Always include a User entity in any system that has accounts or personalized actions. In a LeetCode-style system, Users are the anchor that ties together Submissions, Competitions, and Leaderboard entries. Without Users, you cannot track who did what.

API

You started with a strong API design but had a small gap around submission endpoint structure, and you fixed it cleanly in your second attempt.

  • When designing a submission endpoint, always include a response type that reflects the instant feedback requirement. A Submission response object should include fields like status, runtime, memory usage, and test case results so the client knows what to expect back.

  • Nest resource endpoints when a clear parent-child relationship exists. For example, POST /problems/:problem_id/submissions is more RESTful than POST /submissions because it makes the relationship explicit in the URL. If you already have problem_id in the request body, moving it to the path is a natural and cleaner fit.

High Level Design

You demonstrated solid end-to-end thinking across all four sections and maintained a consistent rating throughout, though you repeatedly left small but important gaps between what you drew on the whiteboard and what you described verbally.

  • Code stubs in a coding platform should be stored per language in the Problems table, not as a single generic field. This means one problem row links to multiple stub entries like Python, JavaScript, Java, and Go. The client sends its preferred language and the server returns the matching stub, which is how the editor knows what starter code to show.

  • When designing a code execution flow, be explicit about who fetches the test cases and who passes them to the container. The cleanest pattern is to have the worker fetch test cases from the database and then pass them directly into the container as part of the job payload. This keeps the container stateless and avoids giving it direct database access, which simplifies security.

  • A submission record should include both a status field and a result field as separate things. Status tracks where the job is right now, like pending, running, or completed. Result holds the final outcome like passed or failed with details like runtime or test cases passed. This separation lets the client poll for progress before the final answer is ready.

  • A live leaderboard in its simplest form can be built by polling a GET endpoint like GET /leaderboards/:competition_id on a short interval. The leaderboard data can be derived from the Submissions table by grouping by userId and competitionId and ranking by a clear signal like number of solved problems or total score. If you do not name the ranking signal explicitly, the sort order is ambiguous and the design is incomplete.

Deep Dives

You demonstrated strong instincts across most areas of this design, and you improved quickly when given feedback, but you had recurring gaps around the specific Redis data structures and enforcement logic needed to make your leaderboard design airtight.

  • When using a Redis sorted set for a leaderboard, updates must be incremental and per-submission, not batch aggregations. When a submission is accepted, immediately call ZINCRBY on the sorted set to update that user’s score. Reading the top N users is then a fast ZREVRANGE call. Never recompute the leaderboard from a database query on each read.

  • To enforce a ‘first accepted solution only’ policy in a competition leaderboard, you need a fast membership check before updating the sorted set. The best approach is a Redis Set per user keyed like solved:{competitionId}:{userId} that stores solved problem IDs. Before incrementing the sorted set score, call SISMEMBER to check if this problem was already solved. This is O(1) and avoids a slower DynamoDB round-trip in the hot submission path.

  • In an async submission system, the API should return a submission ID immediately after enqueuing the job, and the client polls a status endpoint for the result. The worker, not the API server, is responsible for writing the final result to the database and updating Redis after execution completes. This keeps the API fast and separates accepting work from processing work.

  • When scaling workers for code execution across multiple languages, use per-language queues so a backlog in one language cannot starve others. Each queue gets its own auto-scaling policy tied to queue depth, for example scaling out Python containers when the Python queue depth exceeds a threshold. Less popular languages can share a single queue and worker pool to avoid the overhead of maintaining many small fleets.