A secure, real-time, and scalable code execution & evaluation platform inspired by LeetCode and HackerRank.
- 📘 Overview
- 🧩 Features
- 🔄 System Architecture
- 🏃 Execution Workflow
- 🧱 Tech Stack
- 💾 Database Design
- 🔐 Backend System
- 🚀 Code Execution Service (Worker)
- 🐳 Quick Setup (Docker Compose)
- 🧠 Challenges Faced & Solutions
- ❓ Next steps
Hashcodex allows users to solve coding problems online, write code in a browser-based editor, and receive instant feedback in real time.
It supports multiple languages, asynchronous execution, and safe code execution in sandboxed environment.
-
🖥️ Responsive User Interface built with
Next.jsandMonaco Editor -
🔐 JWT Authentication with
refresh token mechanism. -
🧑💻 Online code editor that support
code highlighting,auto-indent, andline numbering. -
💻 Multi-language support (
Java,c++,Python) -
▶️ Run code withcustom testcases -
✅ submit code with
predefined testcases. -
⚙️ Asynchronous execution using
RabbitMQ queues. -
📦 Secure sandboxed code execution
(Docker isolation). Containers run inNetworkMode=nonewithlimited CPU/memoryandno privileged operations -
🔄 Real-time result streaming via
Server-Sent Events (SSE). -
🗃️ Database persistence for user sessions, submissions, and problems.
The system follows a distributed event-driven architecture where user submissions are processed asynchronously by Go-based workers executing code securely inside Docker sandboxes.
After login into the platform,
-
User selects problem and writes code then request
Run/submitto backend by sending code, language, problemId andcustom testacases (in case of Run)to backend. -
Backend verify the request and then
fetch problem detailslike driver code, solution code(in case of Run), testcases(in case of Submit). Also, create new submission entry inDB with status PENDING. -
After collecting all the problem detail, create a new submission payload and a
unique submission id. -
Pushthe submission payload toSubmission Request Queue. -
Here,
- The
unique submission Idis thenreturn back to the user. - The
Submission palyloadis consume by theGo Worker Service.
- The
-
Here,
-
The
unique submission Idis immediately send to backend to open aSSE connection. -
Here, in the Go worker, new
docker containeris spin up based on the language and the code is copied in the container,compile (if needed). Then code is executed with testcases in case of Submit or testcases (expected output) are created based on solution code in case of Run.
-
-
Then the
resultis constructed based on thestdOutandstdErrand push into theSubmission Response Queue. -
The Submission Response is
Consumeby theSpring boot serverand pick up the stored result. -
The
status(Accepted or Wrong Answer or TLE or MLE or RunTime Eror)is updated into thedatabase. -
Then the result is send back to the
frontendusing theSSE.
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | Next.js (React) | Code editor & UI |
| Backend | Spring Boot | API, Auth, SSE, queue producers/consumers |
| Worker | Go | Code execution in Docker sandbox |
| Message Queue | RabbitMQ | Async messaging |
| Database | PostgreSQL | Store User, Problems, testcases, submissions |
| Cache/Token Store | Redis | Temporary storage for verification & password reset tokens |
| Containerization | Docker | Safe sandboxing |
-
Stores
user informationlike name, email, hashed Password, etc. -
Rolefield is enum whose values areUSER,ADMIN. -
Email and password are the primary source of authentication.
email_verifiedfield store whether user have verified there email or not. -
public_idis the UUID which can beexpose to public (frontend)instead of the database id.
-
Session Table stores the logged in
user session datalike session id, user id, creation timestamp and expiry timestamp. -
The
expiryof the session is usually long like1 week. -
It also helps to
refersh the Access Tokenwhich isshort-lived JWTusually of15 minutes. -
There
One-To-Manyrelationship fromUser to Session.
-
Stores
problem detailslike title, difficulty, description, etc -
numberfiled is th unique number assigned to each problems representing the problem number. -
slugis the unique indentifcation of problem which isURL friendlyname of the problem title. -
paramsis a string that representparamters in the function/methodsignature in code.Multiple paramtersare separated by\n.Example:
class Solution{ public int[] twoSum(int[] nums, int target){ // user code } }
Here, the params will be
"nums\ntarget" -
activefield is used by ADMIN tohide or unhidea problem from USER. -
time_limitcontrols the how much time a code havemaximum time to execute in seconds, when it exceed it will giveTime Limit Exceed (TLE).
-
Store language and the corresponding code.
-
driver_codeis the full main code that contains main function/method and there is a placeholder{{code}}in the user solution code.#include <bits/stdc++.h> {{code}} int main(){ // input testcase paramters // Call Solution with the input // Display Result (for comparison with expected output) return 0; }
-
starter_codeis for the users to which is shown in the code editor in frotend. User needs to complete this code.class Solution { public: vector<int> twoSum(vector<int>& nums, int target) { // write your code here } };
-
solution_codeit is like starter code but there is optimized solution of the problem.class Solution { public: vector<int> twoSum(vector<int>& nums, int target) { unordered_map<int, int> seen; for (int i = 0; i < nums.size(); ++i) { int need = target - nums[i]; auto it = seen.find(need); if (it != seen.end()) { return { it->second, i }; } seen[nums[i]] = i; } return {}; } };
-
Store the testcases related to a problem.
-
inputfield represents the input testcase. In case of multiple parameters, inputs are seprated by\n.The testcase in the classic
Two Sumproblem is :nums = [2, 7, 3, 9, 4] target = 9It is stored as
"2 7 3 9 4\n9"similar to theparamsfield"nums\ntarget"in Problems.So, frontend can easily
split both input and paramsby\nandrender UI accordingly. -
outputfield stores the expected output. It is compare as it is with the output return by the code. -
samplerepresent the testcase issample testcase or not.
-
It stores the
problem topics. likeArray,Linked List,Stack, etc. -
There is
Many-to-Manyrelationship betweenproblems and topics.
-
It stores the
User Problem Submssionwith therelanguageandcode. -
statusrepresent the outcome of the submission. status is initially set toPENDINGwhen create a new submission request. -
After the code execution, the status of result is updated in the Database.
-
Here, status value are :
- PENDING = code is being judge,
- CTE = Compile Time Error
- RTE = Run Time Error
- WA = Wrong Answer
- TLE = Time Limit Exceed
- MLE = Memeory Limit Exceed
- SOLVED = Submission Accepted
- SERVER_ERROR = Something went wrong
- Language : Java (JDK 21)
- Framework: Spring Boot v3.5
- Build System: Gradle
- Architecture: Controller - Service - Repository
- Database Migrations : Flyway
- Security : Spring Security
- Persistence API: Spring data JPA
After creating new account in the platform, then a confirmation link is sent to the registered email address to verify the account.
After verification,
-
User login with the registered email and password.
-
Server verifies the credentails.
-
Then a new entry is created in the
Session Tablewith the expiration value fetch from theapplication.propertiesfile. -
Also, and Access Token which is a
short-lived JWTis created where theuser public id as subject. user's role, and the unique session id is also stored in the JWT paylod and the expiration is also set using value store d inapplication.properties. -
The generated access token is send to frontend as HTTP-Only Cookie
-
On every Request, the cookie is automatically send along with the request.
-
In secure endpoints, the token cookie is extracted and verify before reaching to
controller. -
If the token is expired, and then the current session is fetch using session id stored in the JWT payload, if the session is not expired then
new access token is generatedand the expiration of session is updated. -
Then the request is forwarded to the controller.
Redis is used to store the authentication token during
email verificationandpassword reset.
-
After creating new account, the email need to verify.
-
Then a secure random token is generated and stored in redis. The
token is stored as valueand theuser public id as key.verify_<User-Public-Id> : <Secure-Random-Token> -
Similary, for password reset,
pswd_<User-Public-Id> : <Secure-Random-Token>
Purpose: Send verification and password reset emails asynchronously using an event-driven architecture.
-
To keep the main request fast and resilient, email notifications are handled asynchronously using Spring Boot’s
@EventListenerand@Asyncannotations. -
This design decouples notification logic from core business logic — making the system scalable
-
In DEV mode, notifications are simply printed to the console for easy debugging.
-
In PROD mode, the listener delegates to the appropriate Notifier implementation (e.g., EmailNotifier).
-
Here is exmple code of Notification Event Listener
@Component @RequiredArgsConstructor public class NotificationEventListener { private final List<Notifier<? extends NotificationPayload>> notifiers; private final ProfileProperties profileProperties; @Async @EventListener public void handleNotification(NotificationPayload payload) { switch (profileProperties.active()) { case DEV -> { System.out.println("To: " + payload.recipient()); System.out.println(payload.content()); } case PROD -> notifiers.stream() .filter(notifier -> notifier.getPayloadType().isInstance(payload)) .findFirst() .ifPresent(notifier -> NotificationEventListener.sendNotification(notifier, payload)); } } private static <T extends NotificationPayload> void sendNotification( Notifier<T> notifier, NotificationPayload payload ) { T typedPayload = notifier.getPayloadType().cast(payload); notifier.send(typedPayload); } }
Hashcodex uses RabbitMQ to decouple the API/backend from the execution workers and to build a robust, scalable, event-driven pipeline for running and returning submission results.
-
There are two Queue Submission Request Queue and Submission Response Queue.
-
In rabbit MQ, we need exchange and routing keys. Also, need to bind the queue, exchange, routing key.
-
The values of the Queue, Exchange and Routing Keys are
// Request Request Exchange = hashcodex.req.exchange Request Queue = hashcodex.req.queue Request Routingkey = hashcodex.req // Response Response Exchange = hashcodex.res.exchange Response Queue = hashcodex.res.queue Response Routingkey = hashcodex.res // Binding Request Request Exchange + Request Routingkey --> Request Queue // Binidng Response Response Exchange + Response Routingkey --> Response Queue -
In Submission Request Queue, the
producer is the spring boot serverwhich actally push submission payload to the queue. And,Go Worker is the consumerof this queue who listen to the queue and fetch the payload .Message Payload structure:
{ "submissionId" : 1, // DB submission id (null in case of RUN) "language" : "JAVA", // or CPP or PYTHON "solutionCode" : null, // null in case of SUBMIT "code" : "// driver + user code...", "startLine" : 4, "testcases" : [ {"input", "1 2 3 4\n5", "output" : "0 3"} ], "submissionType" : "SUBMIT" // or "RUN" }Note: Along with the message payload, a
correlation idis sent. this is the unique submission id. It uniquely identify each submission payload.The
submissionIdin the payload is the Database submission id which helps to update the submission response status in the database. It can benullin case ofRUN, running with custom testcase doesnot create submission entry in database.startLineis the interger value which store from which line the user code start in the main code after merging driver code and user code. Helps inaligning Error linewith the user code. -
In Submission Response Queue, The
Go Worker is the producerwho push the submission result/response to the queue andspring boot server is the consumerwho listen to the queue and fetch the submission result.{ "id": 1, // submission id "total": 10, // total testcases "passed": 5, // total passed "status": "WA", // overall status "compileError": null, // compile error if any "timeMs": "200", // total time taken in milli second "cases": [ { "input": "// input testcase", "output": "// output testcase", "expected": "// expected value", "error": "// error is any else null", "status": "// testcase status" } ], "errorMessage": null, "submissionType": "SUBMIT" // or "RUN" }Note: Along with the response payload, the same
correlation idis send back.
I choose Server-Sent Events (SSE) because my platform only requires one-way, real-time updates from the backend to the frontend, not a full duplex connection like WebSockets.
-
Unidirectional communication fits perfectly
In my use case, after a user submits code, the backend just needs to stream status updates.
The client doesn’t need to continuously send messages back so a simple, server-to-client stream is ideal.
SSE is lightweight,and built on standard HTTP — no special protocol handling.
-
Better than Polling
Traditional polling (e.g., hitting
/statusevery few seconds) is wasteful causes high server load.SSE provides instant push updates over a single long-lived HTTP connection.
-
Simpler than WebSockets for this use case
WebSockets provide full-duplex communication which is not required as the platform doesn’t require bidirectional communication, only required server → client updates (for code execution results).
The Code Execution Service (written in Go) is a distributed worker responsible for compiling and executing user-submitted code securely inside isolated Docker containers.
It consumes jobs from RabbitMQ queues and publishes results back once execution is completed.
The worker executes user code using ephemeral containers with strict isolation.
-
Docker images for the respective languages are :
- Java :
openjdk:21-jdk - C++ :
gcc:13 - Python :
python:3.11-alpine
- Java :
Docker Containers Configuration,
-
NetworkMode : "none" - completely isolated (no internet access)
-
No Linux capabilities
-
No privileged containers
-
Limited memory and CPU
For Memory (MB) CPUNanos Pids Limit Compile 512 2,000,000,000 256 Run 256 1,000,000,000 128 To ensure fairness, Hashcodex
dynamically scales time per languageusing predefined multipliers. C++ is fastest among these 3 languages, so the base time decided by theexecution time of C++and it is set by the problem admin.Language Time Factor C++ 1.0x Java 2.0x Python 5.0x
Code Compilation & Execution
After docker container is created,
-
source code is copied into the container.
-
Then code is compiled (if applicable).
-
Then the code is run per testcases. Testcase are feeded by standard input.
-
The job of the driver code is to accept the input and call the Solution with input.
Result Collection and Formatting
-
Output is collected with the StdOut and StdErr.
-
In case of Compilation Error & Run Time Error, the error message is passed through a layer that format the error message.
-
When something error happend like a
compile time erroror aRun Time Error, the error message contains the line number where the error happens. This -
Error line number is based on th merged code (driver code + user code). So the Error message is updated based on the user code by the help of the
startLinevalue send in the submission paylod.
Here is the step by step guid to setup the project locally and run it using docker compose.
Before setup, make have installed git and docker in your system.
Also, Docker should be running in your system.
- Clone the repository
git clone https://github.com/shahrohit/hashcodex.git- Go into the
hashcodexdirectory.
cd hashcodex-
create
.envfile in the frontend root directory -
Copy the below code into the
.envfile of the frontend (created just before)
NEXT_PUBLIC_BACKEND_URL=http://localhost:8000
- Now build the project
docker compose build- Pull the docker image of compiler for each language. Run the below command one by one.
docker pull gcc:13
docker pull openjdk:21-jdk
docker pull python:3.11-alpine
- Now, Run the project.
docker compose up -dNote: Make sure the each service is running just like in the above image
-
Now click on this url, http://localhost:3000. You will see the home page of
hashcodex. You will see the some problem topics like Array, Linked List, etc and a problemTwo Sum. If not then your services are not working properly. In that case restart the docker containers. -
Click on the
Get startedbutton in the top right corner. -
You will ask for login, the default login id and password is given below. These credentails have the
admin access. You will maybe redirect to the admin page after login where you can manage problems.
- Demo Email Address:
hikameb683@lorkex.com - Demo Password :
admin@123
Note: The give email is temporary mail. Is is just used for demo purpose.
-
Again go to the home page http://localhost:3000 then select the
Two Sumproblem. Then start solving your problem -
To Stop the docker containers, run the below command,
docker compose down-
Executing external user code securely
Challenge: Running arbitrary user code in the backend introduces security risks.
Solution: All user code is executed inside isolated Docker containers with strict resource limits, no network access, and dropped Linux capabilities.
-
Ensuring complete isolation and safety
Challenge: Preventing malicious code from accessing the host system.
Solution: Containers run with:
- NetworkMode: none (no internet access)
- CapDrop: ["ALL"] (no Linux capabilities)
- SecurityOpt: ["no-new-privileges"]
- Limited CPU, memory, and PID resources.
-
Designing LeetCode-style solution structure
Challenge: Allow users to only implement the function body, not boilerplate or I/O code.
Solution: Each problem stores a driver code with a
{{code}}placeholder. User-submitted logic replaces this placeholder at runtime before execution.
-
Structuring testcases with multiple parameters
Challenge: Problems often contain multiple input parameters and outputs.
Solution: Each problem stores params, input, and output fields as newline-separated strings (e.g.,
"nums\ntarget"), allowing dynamic UI generation and flexible parsing.
-
Handling submissions with predefined testcases
Challenge: Evaluate user code against official testcases during “Submit”.
Solution: Worker runs the code using problem’s stored testcases and compares outputs with the expected results for each case.
-
Handling custom runs with user-defined testcases
Challenge: Allow users to test code with custom input before submission.
Solution: During
“Run”, worker first executes solution code to generate expected output, then runs user code with the same input for comparison.
-
Evaluating testcase results
Challenge: Accurately compare outputs and determine verdicts.
Solution: Normalize both outputs (trim spaces, newlines) and compare line-by-line to classify results as Accepted, Wrong Answer, TLE, RTE, etc.
-
Disabling user output streams
Challenge: Preventing print/debug statements from interfering with expected results.
Solution: In driver code, output streams are temporarily disabled while calling the user’s function, then re-enabled for displaying final results.
-
Aligning error line numbers with frontend editor
Challenge: Compiler/runtime errors include extra lines from driver code, confusing users.
Solution: Worker reformats error messages, removing absolute paths and adjusting line offsets to match user code lines.
-
Asynchronous execution & real-time feedback
Challenge: Ensuring non-blocking execution and real-time result updates.
Solution:
- Submissions are processed asynchronously via RabbitMQ.
- Worker sends results to Response Queue.
- Backend streams live updates to the frontend using Server-Sent Events (SSE).
- Implement rate limiting for the problem submission.
- Setup a CI/CD pipeline.
- Implement container orchestration with kubernetes.
- Implement horizontal scaling of worker service.



