Marouane Boufarouj

How I simplified 95 and made it faster

Marouane BoufaroujMarouane Boufarouj
January 05, 2026
4min read
Software Design
How I simplified 95 and made it faster

context

95 (aka ninefive) is a distributed online judge system built to execute and validate user-submitted code against a set of predefined test cases. the original architecture was intentionally over-engineered for learning purposes: microservices, async communication, sandboxed execution, and more.

over time, this complexity became the bottleneck.

this article explains why i changed it, what i removed, and what improved.

before: heavy server-side execution

in the original design, all code execution happened on the backend. the request lifecycle was defined by a long chain of synchronous and asynchronous handoffs:

  1. frontend: the user uploads a zip file via the ui.
  2. api gateway: intercepts the request, handling jwt authentication and rate limiting.
  3. submission service:
    • stores submission metadata in its own postgresql schema.
    • uploads the raw zip file to minio object storage.
    • pushes an execution job to rabbitmq.
  4. execution worker: listens to the queue, retrieves the job, and initiates the sandbox.
  5. docker-in-docker sandbox:
    • spins up ephemeral, isolated containers.
    • enforces strict cpu, memory, and i/o limits.
    • collects stdout, stderr, and test case results.
  6. result processing: the worker pushes results back through rabbitmq.
  7. finalization: the submission service consumes the result, updates the database, and notifies the user via websockets.
  • hybrid communication: we mixed synchronous rest calls (inter-service) with asynchronous rabbitmq jobs.
  • database fragmentation: each microservice maintained its own schema in postgresql.

- what worked well?

  • pretty strong isolation for untrusted code
  • event-driven execution pipeline
  • clear service boundaries
  • good learning value :)

- what didn't?

  • high latency
  • memory footprint
  • operational overhead
  • slow iteration when adding new challenges
  • tricky to add support for new languages; basically i had to build images specifically for those languages!

after: lightweight cli-based architecture

the core shift: "code execution does not need to happen on the server." by moving the runtime to the user's machine, we eliminated the need for heavy backend orchestration.

the new workflow

  1. cli execution: user runs a command locally.
  2. fetch specs: cli pulls problem metadata and test requirements from the api.
  3. local build & test: code is compiled and validated on the user's hardware.
  4. submit results: cli sends only the execution report/logs to the api gateway.
  5. backend update: takes care of comparing the results to the assertions.
  6. live notification: results are pushed to the web ui via websockets.

example of test definition - from "build your own shell" challenge:

{
  "stageNumber": 0,
  "stageName": "Print a prompt",
  "testType": "cli_interactive",
  "programConfig": {
    "executable": "${user_program}",
    "args": [],
    "env": {}
  },
  "tests": [
    {
      "testName": "Shell prints dollar prompt",
      "stdin": "\n",
      "timeoutSeconds": 5,
      "assertions": [
        {
          "type": "stdoutContains",
          "value": "$",
          "description": "Must print $ as prompt"
        },
        {
          "type": "exitCode",
          "expected": 0
        }
      ]
    }
  ]
}

we don't send the assertions to clients to avoid cheating.

key gains

  • speed: response time dropped significantly.
  • simplicity: removed docker-in-docker, minio, and rabbitmq.
  • efficiency: drastically lower server cpu/memory footprint.
  • agility: faster to add new challenges without managing container images.

local-first: leverages the user's local environment for all resource-intensive tasks.

$ 95 login
 █████╗ ███████╗
██╔══██╗██╔════╝
╚██████║███████╗
 ╚═══██║╚════██║
 █████╔╝███████║
 ╚════╝ ╚══════╝

Build your coding skills, one challenge at a time

Opening browser for GitHub authentication...
✓ Logged in successfully!

$ 95 init --cmd "python main.py"
✓ Config saved to .95.yaml

$ 95 test a85fdf04-a98e-4747-aa38-6e38babe663c
Stage 01: Handle echo command
├─ Echo simple string
     $ echo hello
       hello

├─ Echo email address
     $ echo test@example.com
       test@example.com

└─ Echo numbers
     $ echo 123
       123

$ 95 run a85fdf04-a98e-4747-aa38-6e38babe663c
Stage 01: Handle echo command
  ├─ ✓ Echo simple string
  ├─ ✓ Echo email address
  └─ ✓ Echo numbers

✓ All 3 tests passed!

→ Check your browser for live progress updates and stage completion!

by the way, the cli tool is open source; feel welcome to contribute.

links

95 is still a work in progress. while my current priority is finding a job, i have many ideas for 95. spoiler: dungeons, hunters, and the shadow monarch are coming. get ready!