DEV Community

Cover image for I Built the Ultimate All-in-One Platform for Software Engineers — Here's the Full Architecture
Feroz
Feroz

Posted on

I Built the Ultimate All-in-One Platform for Software Engineers — Here's the Full Architecture

Explore the Platform: slavecode.codes

Support on GitHub: github.com/phero20 (Give it a star)

"Your code is your master. Serve it well."

That's the philosophy I built this around. Not just a tagline — a design constraint. Every system decision in SlaveCode comes back to that principle: the code should be judged fairly, the infrastructure should serve users well, and nothing should break under pressure.

SlaveCode is a competitive programming platform I built from the ground up. It is not a weekend project. It is a full production system with:

  • 11,000+ coding problems across multiple difficulty levels
  • 82+ language learning tracks in the Academy (from Go to Haskell to Gleam)
  • Real-time multiplayer Arena for up to 50 players simultaneously
  • 460+ company interview profiles (Google, Meta, Amazon, Apple, and more)
  • 56-topic System Design curriculum with an AI-powered diagram workspace
  • A global contest aggregator pulling from LeetCode, Codeforces, AtCoder, CodeChef
  • A compiler playground available to guests with zero login required

This post is a full architecture walkthrough. Not "here's what I used" — but why every decision was made, what breaks if you get it wrong, and how it all fits together.


The Big Picture: A Multi-Cloud Architecture

Before diving into any single system, it's worth understanding the physical topology first. Where things run matters as much as how they run.

┌──────────────────────────────────────────────────────────────────────┐
│                        VERCEL EDGE NETWORK                           │
│  Next.js 15 (App Router) — SSR + Static + CDN-distributed assets    │
└───────────────────┬───────────────────────────────┬─────────────────┘
                    │ HTTPS REST                     │ WSS WebSockets
                    ▼                                ▼
┌────────────────────────────────────────────────────────────────────┐
│                   GOOGLE CLOUD PLATFORM (Compute Engine VMs)        │
│                                                                      │
│   ┌──────────────────┐   ┌───────────────────┐   ┌─────────────┐   │
│   │  Hono API Server │   │  Golang Arena Hub │   │ BullMQ      │   │
│   │  (Bun runtime)   │   │  (WebSocket mgr)  │   │ Workers     │   │
│   └──────────────────┘   └───────────────────┘   └─────────────┘   │
└──────────┬─────────────────────────────────────────────┬────────────┘
           │ Redis Protocol                              │ HTTP POST
           ▼                                             ▼
┌──────────────────────────┐          ┌─────────────────────────────┐
│  MANAGED DBaaS           │          │  MICROSOFT AZURE (VMs)      │
│  ┌──────────────────────┐│          │  ┌─────────────────────────┐│
│  │ Neon (Postgres)      ││          │  │ Judge0 Code Sandbox     ││
│  │ MongoDB Atlas        ││          │  │ (Dockerized, isolated)  ││
│  │ Aiven Valkey (Redis) ││          │  └─────────────────────────┘│
│  └──────────────────────┘│          └─────────────────────────────┘
└──────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

There are three intentional multi-cloud decisions here:

Why GCP for the API and Arena? Raw Compute Engine VMs give persistent connections, no cold starts, and no 10-second serverless timeout that would kill a long-running BullMQ worker mid-job. Vercel serverless functions cannot hold a BullMQ consumer — period.

Why Azure for Judge0? This is the most important security decision in the entire platform. The code execution sandbox runs on an entirely separate cloud provider (Azure), isolated from the primary databases (GCP). If a sophisticated attacker crafts a zero-day Docker escape exploit and compromises the Judge0 VM, they land on Azure — not inside the same VPC as the API, Postgres, or MongoDB. The "blast radius" is physically separated across two clouds.

Why Vercel for the frontend? Edge CDN distribution, automatic CI/CD on push, SSR support, and zero infrastructure management for static assets. It offloads a dimension of complexity I do not need to operate.


The Complete Tech Stack

Layer Technology Why
Frontend Next.js 15 (App Router), React, TailwindCSS, Zustand App Router enables co-located server components for SEO + client islands for interactivity
Code Editor Monaco Editor Same engine as VS Code — mature, syntax-aware, extensible
Whiteboard Tldraw (infinite canvas) Best-in-class React-native infinite canvas for System Design diagrams
REST API Hono on Bun Hono is 2-3× faster than Express on Bun. Bun's native speed matters for hot paths
Dependency Injection Awilix Enables clean service-layer architecture at scale without tight coupling
Multiplayer Engine Golang (Fiber, WebSockets) Go goroutines handle tens of thousands of persistent WebSocket connections with far less overhead than Node
Background Jobs BullMQ (Bun Workers) Reliable Redis-backed queue with retries, delays, and priority scheduling
Relational DB PostgreSQL (Neon serverless) Serverless Postgres — scales to zero, no idle VM cost
Document DB MongoDB Atlas (Mongoose) Flexible schema for problems, test cases, submissions — highly nested JSON
In-Memory Store Aiven Valkey (Redis fork) Open-source Redis drop-in, handles cache + queue + pub/sub + arena state
Auth Clerk Cryptographic JWT edge verification, OAuth, webhook user sync
Code Execution Judge0 (primary) + Wandbox (fallback) Dockerized sandbox that compiles and runs untrusted code safely
AI Integration Unified AI proxy (multiple providers) Key rotation, load balancing, and automatic provider failover
ORM Drizzle ORM (Postgres) + Mongoose (MongoDB) Type-safe SQL generation for Postgres; Mongoose for Mongo document modeling
Image Storage Cloudinary Bug report screenshots, user avatars

Database Strategy: Why Three Databases?

This is the question I get most often. The answer is that the three databases own fundamentally different shapes of data, and forcing one data model onto all three use cases would be an architectural mistake.

PostgreSQL (Neon) — The Social Graph & Analytics Engine

Postgres owns everything that requires joins, aggregations, and referential integrity:

-- A simplified view of what Postgres owns
users            user_stats         (1:1)
users            user_activity      (1:many  daily heatmap)
users            user_solved_problems (many:many with Mongo IDs as text FK)
users            follows            (self-referential many:many)
users            solutions          (community editorial posts)
categories       category_problems  (hierarchical DSA roadmap nodes)
contests        (aggregated from external APIs by a cron worker)
Enter fullscreen mode Exit fullscreen mode

Notice user_solved_problems.problemId is a text column that stores a MongoDB ObjectId string. This is a cross-database foreign key — a deliberate trade-off. I get relational semantics for user stats without forcing the problem definitions into Postgres where they don't fit.

Drizzle ORM handles this with full type safety. The column is declared as text() but semantically treated as a foreign reference in the service layer.

MongoDB Atlas — The Content Engine

MongoDB owns everything that is large, nested, or schema-flexible:

  • Problem definitions — title, description, constraints, hints, editorial, companies tagged, difficulty, language support flags. A single problem document can be 20KB+ of nested JSON.
  • Hidden test cases — hundreds of {input, expectedOutput} pairs per problem that the judge evaluates against.
  • Submissions — the full execution log: source code, stdout per test, stderr, time and memory per test case, final verdict. This can easily reach 100KB per submission.
  • Academy tracks — deeply nested exercise trees with descriptions, hints, and starter code per language.
  • System Design workspaces — Tldraw serialized JSON documents. These are opaque blobs that Postgres has no business indexing.

Forcing this into Postgres would mean either (a) JSONB columns everywhere and losing schema enforcement, or (b) a schema migration nightmare every time a problem field changes.

Aiven Valkey (Redis) — Four Jobs, One Store

Redis is the most overloaded layer in the system. It does four distinct things:

Role Key Pattern TTL Owner
Job Queue bull:submission-evaluation:* Until consumed BullMQ Workers
Job Queue bull:arena-cleanup:* Until consumed BullMQ Workers
Job Queue bull:contest-sync:* Until consumed BullMQ Workers
Arena State arena:room:{roomId} 24 hours Go Arena Hub
Arena State arena:user_room:{userId} 24 hours Go Arena Hub
API Cache company:*, llm:*, diagram:* 24h–7 days Hono API
Pub/Sub arena:events:{roomId} N/A (channel) API → Go
Pub/Sub arena:match:started:{roomId} N/A (channel) API → Go
Pub/Sub arena:submission:{roomId} N/A (channel) Worker → Go

The most interesting part is that Redis decouples services that would otherwise need to know each other's IP addresses. The Bun API and the Golang Arena Hub never talk directly. They shout into Redis, and the other side listens.


The Submission Pipeline: Async Code Evaluation

This is the most complex workflow in the platform. When a user clicks "Submit", this is what actually happens:

Phase 1: Synchronous Fast Path (the API's job)

Client → POST /submissions/submit { sourceCode, languageId, problemId }
       → SubmissionService: Insert { status: "PENDING" } into MongoDB
       → BullMQ: Enqueue job { submissionId }
       → Response: 202 Accepted { submissionId }
Enter fullscreen mode Exit fullscreen mode

The API responds in ~50ms. It does not wait for compilation. It cannot — compilation can take 1–30 seconds depending on the language and problem complexity.

The client immediately starts polling:

// Client-side polling — every 2 seconds
const poll = async (submissionId: string) => {
  const result = await fetch(`/api/submissions/${submissionId}/status`);
  if (result.status === "PENDING" || result.status === "RUNNING") {
    setTimeout(() => poll(submissionId), 2000);
  } else {
    displayVerdict(result);
  }
};
Enter fullscreen mode Exit fullscreen mode

Phase 2: Asynchronous Execution (the worker's job)

BullMQ dequeues job
  → MongoDB: Fetch public + hidden test cases for problemId
  → ExecutionService.runFullSubmission(sourceCode, tests)
      → For each test case batch:
          → Judge0: POST { source_code, language_id, stdin }
          → Wait for Judge0 response: { stdout, stderr, time, memory, status }
  → Calculate overall verdict
Enter fullscreen mode Exit fullscreen mode

This is where the Driver layer matters. Before sending code to Judge0, a language-specific wrapper is applied. For a Python problem, the driver injects the boilerplate that calls the user's function with the test input and captures stdout. This means users only write the algorithm — not the I/O scaffolding. The /driver directory contains these wrappers for Java, C++, C, Go, Rust, Python, and more.

Judge0 is the primary and definitive judge. It compares the program's stdout against the expected output for each hidden test case. For 95%+ of problems, this is all that runs.

For a small subset of problems where multiple valid outputs exist — such as questions asking for "any valid topological order" or "all valid parentheses combinations" — a pure string comparison would incorrectly reject correct answers. In those cases, an AI-based semantic check runs as a fallback, evaluating whether the user's output is logically equivalent even if the string differs. This is the exception, not the rule.

Phase 3: Finalization

Worker → MongoDB: Update submission { status, executionResults, finalVerdict }
Worker → Postgres (parallel): Update user_stats { totalSolved++, streak, points }
Worker → Redis Pub/Sub: Publish arena:submission:{roomId} (if arena match)
Enter fullscreen mode Exit fullscreen mode

The stats update runs in parallel with the MongoDB save using Promise.all. It handles streak calculation (checking lastSolveDate against today's date), difficulty-specific counters (easySolved, mediumSolved, hardSolved), language distribution tracking via a jsonb languageCounts column, and ELO-style arena point deltas.


Arena: Real-Time Multiplayer at Scale

The Arena is the feature that required the most architectural thought. Coordinating 50 simultaneous players — each submitting code, getting verdicts, and watching a live leaderboard update — requires a completely different stack than the REST API.

Why Golang?

Node.js uses an event loop — it is single-threaded I/O multiplexed. For WebSockets, this means each message goes through the same event queue. Under high concurrency, this creates latency jitter. A single slow operation (like a heavy JSON parse) can cause perceptible delays for unrelated clients.

Go's concurrency model is fundamentally different: each WebSocket client gets its own goroutine (a lightweight green thread, ~2KB stack vs ~8MB for OS threads). The Go runtime scheduler multiplexes thousands of goroutines across available CPU cores. Communication between goroutines uses channels — typed, blocking data pipes that are inherently safe without mutex locks.

// Arena Hub (simplified) — each room is a Go goroutine cluster
type Hub struct {
    rooms  map[string]*Room
    mu     sync.RWMutex
    redis  *redis.Client
}

type Room struct {
    id      string
    clients map[string]*Client  // userId → websocket connection
    pub     chan []byte          // outbound broadcast channel
}

// Each client has a dedicated read goroutine
func (c *Client) readPump() {
    for {
        _, message, err := c.conn.ReadMessage()
        if err != nil { break }
        c.hub.handleMessage(c, message)
    }
}
Enter fullscreen mode Exit fullscreen mode

Redis as the State Store (Lua Atomic Scripts)

The arena room state lives in Redis, not in Go memory. This is a critical design decision: if the Go server restarts (deploy, crash, OOM), the match state is not lost. Redis is the source of truth.

But Redis is also shared between the Bun API (which handles join requests) and the Go Hub (which manages live connections). Two processes writing to the same key creates race conditions. The solution: embedded Lua scripts that execute atomically on the Redis server itself.

-- arena_join.lua (Lua runs atomically in Redis — no concurrent modification)
local roomKey = KEYS[1]
local userRoomKey = KEYS[2]
local userId = ARGV[1]
local maxPlayers = tonumber(ARGV[2])

local roomJson = redis.call("GET", roomKey)
if not roomJson then
    return redis.error_reply("ROOM_NOT_FOUND")
end

local room = cjson.decode(roomJson)

-- Atomic check: is room full?
if #room.players >= maxPlayers then
    return redis.error_reply("ROOM_FULL")
end

-- Atomic check: is user already in a room?
local existingRoom = redis.call("GET", userRoomKey)
if existingRoom then
    return redis.error_reply("ALREADY_IN_ROOM")
end

-- Both checks passed — atomically update
table.insert(room.players, userId)
redis.call("SET", roomKey, cjson.encode(room), "EX", 86400)
redis.call("SET", userRoomKey, KEYS[1], "EX", 86400)

return redis.status_reply("OK")
Enter fullscreen mode Exit fullscreen mode

Without this Lua script, two simultaneous join requests could both pass the "room full" check and push the room over capacity. The Lua script ensures this is compare-and-swap in a single atomic operation — guaranteed by Redis's single-threaded command processing.

The Pub/Sub Bridge

The Bun workers (which handle code evaluation) and the Go Hub (which manages WebSocket clients) need to communicate. But they run on different processes. The bridge is Redis Pub/Sub:

BullMQ Worker evaluates submission
    → Publishes to Redis channel: "arena:submission:{roomId}"
    → Payload: { userId, verdict, testsPassed, totalTests, score }

Go Hub subscribes to "arena:submission:*"
    → Receives message
    → Finds the correct Room by roomId
    → Broadcasts scoreboard update to all connected clients in that room
Enter fullscreen mode Exit fullscreen mode

This design means the Bun API server and the Go Arena Hub are completely decoupled. Neither knows the other's hostname or IP. They only share knowledge of the Redis channel name format. This makes independent scaling and deployment of each service trivial.

The Arena State Machine

An Arena match transitions through these states, managed via Redis + Go:

[*] → WAITING  (Host creates room, others join via PIN)
WAITING → LOBBY  (Host toggles lobby mode for readiness checks)
LOBBY → WAITING  (Host toggles back)
WAITING/LOBBY → PLAYING  (Host starts match — requires ≥ 2 players)
PLAYING → FINISHED  (20-minute timer expires OR host aborts)
Enter fullscreen mode Exit fullscreen mode

The match timer is managed via a BullMQ delayed job: when the host starts a match, a job is enqueued with a delay of matchDurationMs. When that job fires, the Arena Worker publishes a "match:ended" event to Redis Pub/Sub, which the Go Hub receives and broadcasts to all clients.


System Design: AI-Powered Learning Canvas

The System Design section is more than static documentation. It is a 56-topic curriculum backed by an interactive canvas and AI assistant.

Each topic (e.g., "Load Balancer", "Message Queue", "CDN") has:

  1. Structured content — a deep-dive explanation with diagrams and real-world examples
  2. A Tldraw workspace — an infinite canvas where users sketch their architecture diagrams
  3. An AI chat assistant — context-aware of the current topic being studied
  4. AI diagram generation — describe a system in prose, get a Tldraw diagram back

The AI tutor prompt is aware of the current system design topic. If the user is on the "Caching" topic and asks "where does Redis fit?", the AI knows the context and responds with topic-specific advice, not generic information.

The diagram generation pipeline:

User: "Draw a system design for a URL shortener"
   → API: POST /systemdesign/workspaces/{id}/generate
   → AI: Generate Tldraw JSON schema from description
   → Parse + validate the returned JSON
   → Merge into the existing workspace document in MongoDB
   → Return updated Tldraw state to the client
Enter fullscreen mode Exit fullscreen mode

AI responses and diagram outputs are cached in Redis (7-day TTL) so identical prompts never cost a second API call — useful when many users study the same topics concurrently.


The Codebase: 70+ Interconnected Classes

The backend (/api) uses Awilix for dependency injection — a container-based DI system similar to what you'd see in NestJS, but lighter and built for Hono. Every service is registered in the container and injected into controllers via the constructor.

// container.ts
container.register({
  // Repositories
  problemRepository: asClass(ProblemRepository).singleton(),
  submissionRepository: asClass(SubmissionRepository).singleton(),

  // Services
  executionService: asClass(ExecutionService).singleton(),
  submissionService: asClass(SubmissionService).singleton(),

  // Controllers
  submissionController: asClass(SubmissionController).singleton(),
});
Enter fullscreen mode Exit fullscreen mode

The service layer boundary is strict:

  • Controllers — handle HTTP routing, input validation, auth middleware
  • Services — contain all business logic, orchestrate between repositories and external APIs
  • Repositories — contain all database queries, return typed model objects

The codebase has 4,765 indexed nodes and 9,732 dependency edges across all packages. The submission domain alone chains: SubmissionController → SubmissionService → ExecutionService → DriverJudgeExecutionService → LLMService.


Feature Scope at a Glance

Feature What It Does
Problems 11,000+ coding problems with hidden test cases, Judge0 execution, solution voting
Academy 82+ language tracks (Go, Rust, Haskell, Gleam...) with progressive exercise-based learning
Roadmap Interactive DSA roadmap — from Arrays to Tries — with problem links at each node
Arena Real-time multiplayer coding battles, up to 50 players, live leaderboard via Go + Redis
System Design 56-topic curriculum, Tldraw canvas, AI chat tutor, AI diagram generation
Companies 460+ company profiles with tagged interview questions
Contests Unified calendar: LeetCode, Codeforces, AtCoder, CodeChef — auto-synced via cron
Compilers Guest-accessible Monaco playground, full Judge0 execution, built-in Tldraw scratchpad
Leaderboards ELO-style arena rating, problem solve streaks, language-specific stats
Social Follow users, post solutions with voting, GitHub/LinkedIn/LeetCode profile links
Admin Internal dashboard for problem creation, category management, and content moderation

What I Would Do Differently

1. WebSocket for submission status instead of polling. The client polling every 2 seconds works, but it creates unnecessary API load at scale. A Redis Pub/Sub → Server-Sent Events (SSE) channel would push the verdict to the client the moment it's ready, with zero polling overhead.

2. Read replicas for MongoDB. All submission reads go to the primary. Under high read load, a read replica for the GET /submissions/{id}/status endpoint would reduce primary pressure significantly.

3. Separate Redis instances by concern. All four Redis roles (cache, queue, state, pub/sub) share one Valkey instance. Under heavy arena load, the pub/sub throughput could starve the BullMQ queue processing. Separating pub/sub into a dedicated instance is the right move at scale.

4. gRPC instead of Redis Pub/Sub for Go↔Node communication. Redis Pub/Sub is "fire and forget" — there's no acknowledgment. If the Go Hub restarts and misses a message, that leaderboard update is lost. A gRPC streaming connection with delivery guarantees would be more robust.


The Repository Structure

slavecode/
├── /api        → Hono API (Bun, Awilix, Drizzle, Mongoose, BullMQ)
├── /arena      → Go WebSocket Hub (Fiber, goroutines, Redis Pub/Sub)
├── /web        → Next.js 15 frontend (Monaco, Tldraw, Zustand)
├── /admin      → Internal admin portal (Next.js)
├── /driver     → Language-specific code wrappers (Java, C++, Go, Rust...)
├── /cloud      → Azure VM provisioner + health checks
├── /infra      → Docker Compose + Dockerfiles
├── /scripts    → DB seeding, Drizzle migrations, Docker utilities
├── /testings   → Integration tests (network, DB pool, endpoints)
└── /docs       → Full UML diagrams, ERDs, architecture docs
Enter fullscreen mode Exit fullscreen mode

Conclusion

Building SlaveCode taught me that distributed systems are not about choosing the right technology — they're about understanding the data flow and failure modes first, and then choosing the technology that maps cleanly to those constraints.

The Judge0 sandbox is on Azure because blast radius matters. The Arena is in Go because goroutines genuinely outperform an event loop for persistent WebSocket connections at scale. Redis does four jobs because it is the right tool for all four of those jobs at this scale.

Every piece is there because the alternative broke something.


SlaveCode is live at slavecode.codes. Find me on GitHub at @phero20. Your code is your master. Serve it well.

Top comments (0)