Hrishikesh Sonawane

Full stack Developer

Building a Code Relay Judge with Node.js and Docker

GitHub Repository

What happens when competitive programming becomes a relay race?

Code Relay Judge is a team-based competitive programming platform where programmers don't just race against other teams—they also race against the clock before handing their code over to the next teammate.

I built the system to handle the complete competition flow: problem solving, code submissions, automated judging, team rotations, scoring, and a live leaderboard.

This post goes through how it works under the hood.


What Is Code Relay?

Code Relay changes the usual competitive programming format.

Instead of one person working on a solution from start to finish, programmers compete as a team.

Each team gets a fixed amount of time, but individual members can only code during their assigned window.

The flow looks something like this:

Discuss → Player 1 codes → Player 2 continues → Player 3 continues → Submit → Judge

When one programmer's time expires, the next teammate takes over the existing code.

That means you're not only solving algorithmic problems—you also need to write code that your teammates can quickly understand and continue.


Architecture

The judge is built around a relatively small set of components:

                    ┌─────────────────┐
                        Frontend     
                     Editor / Timer  
                      Leaderboard    
                    └────────┬────────┘
                             
                             
                    ┌─────────────────┐
                     Express Server  
                        REST API     
                    └────────┬────────┘
                             
                             
                    ┌─────────────────┐
                     Submission Queue│
                    └────────┬────────┘
                             
                             
                    ┌─────────────────┐
                     Docker Runner   
                    └────────┬────────┘
                             
                       Run Test Cases
                             
                             
                    ┌─────────────────┐
                     Results + Score 
                    └────────┬────────┘
                             
                             
                    ┌─────────────────┐
                      Leaderboard    
                    └─────────────────┘

A submission travels through the server, enters the judging queue, executes against the problem's test cases inside Docker, and finally updates the team's score.

Let's break those pieces down.


1. Express.js Server

The backend is built using Node.js and Express.js.

The server handles requests for things such as:

  • submitting source code
  • uploading solution files
  • checking submission status
  • fetching leaderboard data
  • checking queue status
  • managing teams
  • updating competition timers

For example, a submitted solution isn't judged directly inside the HTTP request.

Instead, the server stores the code and creates a job for the submission queue.

The client receives a job ID that can then be used to check the result.

This keeps the HTTP layer separate from the actual judging process.


2. Submission Queue

Running several compilers and programs simultaneously can quickly consume resources.

So instead of immediately executing every submission, Code Relay uses an in-memory submission queue.

Conceptually:

Submission A ─┐
Submission B ─┼──► Queue ──► Judge ──► Result
Submission C ─┘

Jobs are processed sequentially.

Each submission moves through states such as:

queued  processing  completed

or:

queued  processing  failed

The queue also has an application-level timeout so a judging request isn't allowed to wait indefinitely.

For a small event running on a single machine, this approach keeps the system simple while preventing multiple compiler workloads from fighting for resources simultaneously.

For a larger deployment, this queue could eventually be replaced by something like Redis-backed workers or another distributed job system.


3. Running Code with Docker

This is the fun part.

A programming judge has one unusual requirement:

It has to execute code written by other people.

Running submitted programs directly on the host would be a terrible idea.

Instead, Code Relay launches submitted solutions inside disposable Docker containers.

The current runner supports:

  • Python
  • Java
  • C++
  • JavaScript

Each language maps to a Docker image and an execution command.

Conceptually:

Python       python image
Java         OpenJDK image
C++          GCC image
JavaScript   Node.js image

For each test case, the judge mounts the submitted source file and input file into a container and executes the program.

The resulting standard output is captured and compared against the expected output.


4. How Judging Works

Every problem contains a collection of test cases.

For example:

problems/
└── problem1/
    └── testcases/
        ├── input1.in
        ├── input1.out
        ├── input2.in
        ├── input2.out
        ├── input3.in
        └── input3.out

For each .in file, the judge:

  1. Loads the corresponding expected .out file.
  2. Starts the appropriate Docker environment.
  3. Runs the submitted program with the test input.
  4. Captures its output.
  5. Compares the actual and expected outputs.
  6. Records whether the test passed.

The result contains information for each test case, including the expected and actual output.

So instead of receiving only:

Wrong Answer

participants can get useful feedback about what happened.


5. Partial Scoring

A submission doesn't necessarily need to solve every test case to earn points.

The judge tracks how many tests passed and calculates a partial score.

More importantly, the leaderboard stores the team's best score for each problem.

Suppose a team submits Problem 1 three times:

Attempt 1  40 points
Attempt 2  80 points
Attempt 3  60 points

Their score for that problem remains:

80 points

The total leaderboard score is calculated from the team's best scores across all problems.

This means retrying a problem can't accidentally destroy a better previous result.


The Relay System

The judging engine is only half of the project.

The feature that makes Code Relay different from a normal online judge is the team rotation system.

Teams contain multiple programmers, and the system keeps track of:

  • team members
  • current programmer
  • discussion time
  • coding time
  • team state

For example, with a 20-minute coding period:

3 players  400 seconds each
4 players  300 seconds each

When one player's coding window ends, control moves to the next member.

The frontend displays the timer so everyone knows exactly how much time remains.

This creates an interesting constraint that normal competitive programming doesn't have:

your code has to survive a handoff.

A clever solution isn't very useful if the next teammate can't understand what you were trying to do.


Live Leaderboard

The frontend periodically retrieves leaderboard information from the server so teams can follow the competition as it progresses.

A leaderboard entry contains the team's total score along with its best scores for individual problems.

After a successful judging job, the leaderboard can be recalculated and sorted by total score.

That gives participants quick feedback without requiring the page to be manually refreshed.


Frontend

The frontend lives inside public/ and is intentionally lightweight.

It provides the main competition interface:

  • code submission
  • language selection
  • problem selection
  • problem descriptions
  • test-case feedback
  • team information
  • coding timer
  • leaderboard

Styling is handled using Tailwind CSS, while the competition logic is implemented with regular browser JavaScript.

The timer state is also persisted in browser storage so accidentally refreshing the page doesn't immediately destroy the visible timer state.


Storage

For this project I deliberately avoided adding a full database server.

Competition state is stored using JSON files.

db/
├── leaderboard.json
└── results.json

config/
├── teams.json
└── active_team.json

For a small competition running on one machine, this has some nice properties:

  • easy to inspect
  • easy to debug
  • easy to reset
  • no database server required
  • minimal setup

Obviously, this isn't how I'd store state for a judge serving thousands of concurrent users.

But that wasn't the goal.

For a small event, keeping the architecture simple was more valuable than introducing infrastructure the system didn't need.


Execution Isolation

Submitted programs run inside disposable Docker containers rather than directly on the host machine.

This gives the judge an important isolation boundary and makes it much safer than directly executing submissions through the host shell.

However, Docker alone should not be treated as a complete security sandbox for arbitrary hostile code.

The current implementation is designed for controlled competitions rather than exposing unrestricted code execution to the public internet.

A production-grade version should additionally enforce things such as:

CPU limits
Memory limits
PID limits
Network isolation
Read-only filesystems
Non-root execution
Dropped Linux capabilities
Stricter process termination

For even stronger isolation, technologies such as gVisor, Firecracker, or dedicated sandboxing infrastructure could sit between submitted programs and the host.


A Note About Timeouts

The submission queue currently implements an application-level timeout around judging.

That's useful for preventing the queue from waiting forever, but there's an important distinction:

stopping the JavaScript promise from waiting isn't the same thing as forcibly terminating the underlying container.

A stronger implementation would enforce execution limits directly on the container/process as well.

That's one of the areas I'd improve when hardening the judge further.


Project Structure

The project is roughly organized like this:

code-relay-judge/

├── server.js
├── queue.js
├── run_code.js
├── database.js
├── team_config.js

├── submissions/
   └── submitted source code

├── problems/
   ├── problem1/
   ├── problem2/
   └── ...

├── db/
   ├── leaderboard.json
   └── results.json

├── config/
   └── team configuration

└── public/
    ├── frontend pages
    ├── JavaScript
    └── CSS

Each part has a relatively narrow responsibility.

server.js handles HTTP.

queue.js manages judging jobs.

run_code.js executes solutions.

team_config.js manages the relay competition state.

public/ handles the participant experience.

That separation also makes individual pieces easier to replace later.


What I'd Improve Next

The current architecture works well for the environment it was designed for, but there are several obvious directions to take it further.

Harden the sandbox

Add explicit Docker CPU, memory, process, network, filesystem, and privilege restrictions.

Stronger timeout handling

Terminate containers rather than only timing out the promise waiting for them.

Persistent job queue

Move the in-memory queue to something persistent so queued jobs survive server restarts.

Database

Replace JSON files with SQLite or PostgreSQL if the competition grows large enough to require concurrent writes and stronger consistency.

Push-based updates

Replace frontend polling with WebSockets or Server-Sent Events for true server-pushed leaderboard and submission updates.

Better judging

Add configurable time limits, memory limits, floating-point comparison, custom checkers, compilation caching, and more detailed verdicts such as:

Accepted
Wrong Answer
Time Limit Exceeded
Memory Limit Exceeded
Runtime Error
Compilation Error

At that point, the project starts moving from a competition tool toward a more general-purpose online judge.


Why Build It This Way?

The architecture mostly came down to choosing the simplest component that solved each problem.

Express provides the HTTP layer.

An in-memory queue prevents judging workloads from piling up simultaneously.

Docker provides an execution isolation boundary.

JSON files keep competition state simple.

Tailwind + vanilla JavaScript keep the frontend lightweight.

And the team rotation system adds the rule that makes the whole thing a Code Relay rather than just another programming judge.

The result is intentionally not a distributed, production-scale judging platform.

It's a focused system built to run a specific kind of programming competition.

And sometimes that's exactly the architecture you need.


tl;dr

Code Relay Judge is a team-based competitive programming system built with Node.js, Express, Docker, and Tailwind CSS.

Teams rotate programmers on a timer while solving problems. Submitted solutions enter a queue, execute against test cases inside disposable Docker containers, receive partial scores, and update a live leaderboard.

It's small enough to understand, hack on, and host yourself—and there's plenty of room to make the judging infrastructure more sophisticated.

If you want to run your own Code Relay competition, the source is on GitHub.

Have fun breaking the test cases.