My Experience Building a Leetcode-Like Online Judge and How You Can Build One
Introduction
We all love bikes, don’t we? Imagine the satisfaction of building your own custom bike! I know, I know, It might not be the best in town, but the pride of turning your passion into a product is unmatched.
As software engineers, we love creating software, often relying on various tools to support our development process. Now, imagine building one of those tools. It might not be production-grade, but the satisfaction of mimicking the functionalities of established software is immense. I have one such story to share with you!
One day, while leetcoding, out of nowhere, seeing the green AC text, I found myself wondering how platforms like leetcode and other online judge platforms execute and evaluate submitted code and generate a result. This curiosity led me to develop my own backend solution — an online judge just like leetcode!
In this post, I’ll walk you through how I created a backend solution — Algocode (though not as refined as leetcode, of course, but still a work of pride! :P) as an online judge just like leetcode. I’ll also guide you on how you can build one yourself. Let’s get started!
Architecture of the Project
The Algocode I have built is based on microservices architecture. Why microservices? Well, some systems just don’t fit well within a monolithic structure, and an online judge backend is a prime example. A monolithic approach won’t be able to provide the modularity scalability, flexibility and most importantly the security a Remote Code Execution Engine needs to actually execute foreign source code into your server. A microservices approach is natively modular, scalable, flexible and we can customize the security we need for any services.
Algocode currently consists of three services: Auth Service, Code Manager, and an RCE Engine for C++ Judge. As of now, Algocode fully supports C++
code execution, and RCE Engine for Java
is under development. Future updates will include support for Python
. I'll keep this post updated as new features are implemented and RCE engine for Python
and Java
are ready.
Now, it’s time for some high level understanding into the details of the services that make up the Algocode backend.
Services
The backend for all the services is built using Django 4.2. Here, I’ll give you an overview of the functionalities of Algocode services. For detailed instructions, I strongly recommend to visit Algocode on GitHub for more detailed and step by step guide.
Algocode Auth Service
The Algocode Auth Service serves as the main entry point to Algocode. It manages user registration, authentication, and authorization, using a PostgreSQL database for user data management. For more details, please check out the Algocode Auth Service.
Algocode Code Manager Service
The Code Manager Service is the intermediary between clients submitting their code and the RCE Engine, which executes it. It manages communication between clients and the RCE Engine, which isn’t directly accessible to clients. This communication happens through a message queue (RabbitMQ).
The Code Manager exposes all necessary APIs for clients to submit solutions, check problem lists, submit code, and check results. When a client sends a code submission request, the Code Manager validates it, retrieves test cases and correct answers from a PostgreSQL database, and publishes the processed data to a language-specific queue. The RCE Engine then consumes this data, executes the code, and publishes the result to a unified-result queue.
The Code Manager then processes, caches, and stores the result in a MongoDB database. For more information, visit the Code Manager service. It’s well documented and you’ll love it, I guarantee!
Algocode RCE Engine Service
The Algocode RCE Engine Service is the heart of Algocode. It executes user-submitted code in a secure Docker environment and compares the output against test case answers. RCE Engine can handle any malicious code execution such as fork bomb
, resource starvation
and file hijacking
to name a few. The final result is processed and published to a unified-result queue, which the Code Manager service then consumes to store the result.
The RCE Engine for C++ Judge can handle the following events:
a. AC (Accepted)
b. WA (Wrong Answer)
c. Compilation Error
d. Time Limit Exceeded
e. Memory Limit Exceeded
f. Segmentation Fault
How to Build Your Own Online Judge
If you’re still reading this section, congratulations! You’re one of the most curious individuals I have ever met. Since you’re still here, it’s clear you’re eager to learn how to build your own online judge. We’re software engineers, after all!
Building your own judge is a challenging but rewarding process. If you’re new to microservices architecture, it can be intimidating to manage the various SDLC cycles of all the services. However, this experience will provide you with a deep understanding of maintaining a microservices project through various development stages.
Before you dive into building an online judge, I recommend familiarizing yourself with the following concepts. Assuming you have already built a monolith-based CRUD app with API integration, these foundational concepts will prepare you for the complexities ahead. You don’t need to master them, just have an initial understanding. You’ll gain deeper insights as you build your project.
A. Microservices Architecture
Microservices architecture offers numerous benefits. It’s crucial for you to understand the benefits of scalability, maintainability, agility, and security that microservices architecture provides.
B. Asynchronous Communication
A key aspect of microservices architecture is asynchronous communication. Since microservices often involve multiple services that might be hosted globally, asynchronous communication is essential. If you’re wondering why not synchronous, you need to revisit the core benefits of microservices. Learn about RabbitMQ for effective asynchronous communication between microservices.
C. Docker
Docker is a fundamental component of this project. In modern tech, building software without Docker is almost unthinkable. Your service will be dockerized, and the online judge will use Docker containers to execute user-submitted code securely. You should understand Docker networking, Docker volumes, Docker security, and Docker Compose to tackle a complex project like an online judge.
D. Database
A solid understanding of both SQL and NoSQL databases like MySQL, PostgreSQL, and MongoDB is crucial. In the software industry, there’s no “always best” solution, only “best fit.” As we handle both structured and unstructured data, choosing between SQL and NoSQL databases depends on the use case. Algocode utilizes PostgreSQL and MongoDB.
E. Extra Concepts
Beyond the basics, incorporating several additional concepts can scale your project. Knowledge of file handling, caching, rate limiting, polling, API testing, version control, and development stage management will make your project more eye-catching and industry-ready.
Learning Outcome
Building an online judge project using microservices architecture is challenging. Because you’ll be running code submitted by unknown users on your server, the security aspects of the project are critical. As you progress, you’ll gain a deep understanding of how to secure your codebase. You will learn about spawning sibling containers, managing these containers, implementing the docker in docker concept, handling complex file operations, caching, rate limiting APIs, and ensuring seamless asynchronous communication between services. Additionally, you’ll discover many unsuccessful methods to do a certain thing and gain valuable insights by troubleshooting various bugs and errors along the way.
Resources
- Algocode: To explore the Leetcode-like project I built using microservices, check out Algocode. The project includes a step by step and detailed guide.
- Introduction to Message Queue (My another blog post): Learn the fundamentals of message queues. Message Queue 101: Your Ultimate Guide
- Asynchronous Communication using RabbitMQ: Understand how to use message queues for asynchronous communication. Understanding Message Queues
- Docker Volume: Learn about managing data in Docker. Docker Volumes
- Docker in Docker: Explore how to run Docker within Docker. Run Docker in Docker
- Rate Limiting: Discover various algorithms for implementing rate limiting in APIs. Different Algorithms for Rate Limiting
- Caching with Redis: Implement caching in your Node.js applications using Redis. Caching with Redis