DEV Community

RjS
RjS

Posted on

System Architecture of React Playground: What Actually Happens Behind the Scenes

In the previous blog, I talked about what I built and the problems I faced.

This time, I want to break down the architecture, not in a dry, textbook way, but in the same way I understood it while building it.


The Mental Model

At a high level, this is what’s happening:

  • The frontend behaves like an IDE
  • The backend acts like a coordinator
  • Workers actually run your code
  • Redis connects everything
  • S3 stores user solutions

So let’s go step by step.


Step 1: Writing Code (Frontend Sandbox)

When you type JSX in the editor:

  • Monaco Editor captures the code
  • Babel (standalone) transpiles JSX → JavaScript
  • That JS is injected into an iframe

Why iframe?

Because if your code crashes, I don’t want the entire app to crash with it.

So the execution happens inside an isolated environment:

  • Separate DOM
  • Separate JS context

React and ReactDOM are loaded via CDN, and then your component is rendered inside that iframe.

This gives you instant feedback.


Step 2: Submitting Code

Now comes the important part.

When you click “Submit”:

  1. The raw JSX (not transpiled code) is sent to the backend
  2. Backend creates a solutionId
  3. That ID is returned to the frontend

At this point, nothing has been executed yet.


Step 3: Why WebSockets?

After getting the solutionId, the frontend:

  • Opens a WebSocket connection
  • Registers itself using that ID

Why not polling?

Because polling would look like:
“Is it done?” → repeat forever

Instead:

  • Backend will push the result when ready

This removes unnecessary load and makes the system real-time.


Step 4: The Queue

Here’s where the architecture becomes interesting.

Instead of executing code directly in the backend:

  • The backend pushes the job into a queue (BullMQ)
  • The job contains: code + metadata

Why?

Because executing user code is:

  • Slow
  • Unpredictable
  • Potentially dangerous

If we did this inside the main server:

  • It would block everything
  • One bad job could kill performance

So we separate execution.


Step 5: Worker Picks It Up

The worker continuously listens to the queue.

As soon as a job arrives:

  • Worker takes the job
  • Starts processing

This is completely asynchronous.

The backend doesn’t wait.


Step 6: Execution Pipeline

The worker does this:

  1. Takes JSX string
  2. Transpiles it again using Babel
  3. Launches Puppeteer
  4. Injects the code into a real browser environment
  5. Simulates user actions

We don’t just check output.

We simulate behaviour.

For example:

  • Find a button
  • Click it
  • Check if UI updates correctly

This is why frontend transpilation is not enough.

We need:

  • A DOM
  • An event loop
  • Real browser behavior

Node.js alone cannot do this.

Puppeteer solves that.


Step 7: Isolation

Running user code is risky.

So we isolate at multiple levels:

  • Each execution runs in a new Puppeteer page
  • No shared state between runs
  • Worker is separate from the backend process

Even if something goes wrong, it stays contained.


Step 8: Returning the Result

Once execution is done:

  • Worker marks job as completed (BullMQ)
  • Backend listens to queue events
  • Backend finds the correct socket using solutionId
  • Emits result via WebSocket

Frontend receives it instantly and updates UI.


Step 9: Redis

Redis handles:

  • Queue storage (BullMQ)
  • Job events (completed/failed)
  • Caching layer (for faster reads)

Earlier, I was using raw Pub/Sub.

But it required:

  • Manual channel management
  • Manual message parsing

Switching to BullMQ simplified everything:

  • Built-in lifecycle events
  • Retry handling
  • Cleaner architecture

Step 10: Storage with S3

Another problem:

Where do we store user code?

If we store it directly in MongoDB:

  • It becomes heavy
  • Not scalable

So instead:

  • Code is stored in AWS S3
  • Only the file key is saved in DB

When needed:

  • Backend generates a signed URL
  • Frontend uses that to fetch code

This ensures:

  • Security (no public access)
  • Scalability
  • Clean separation

Step 11: The Full Flow

  1. User writes code
  2. Code runs in iframe (preview)
  3. User submits
  4. Backend returns solutionId
  5. Frontend opens WebSocket
  6. Backend pushes job to queue
  7. Worker picks a job
  8. Code executed via Puppeteer
  9. Result emitted via queue event
  10. Backend sends result via WebSocket
  11. Frontend updates UI

Key Design Decisions

1. Double Transpilation

Frontend:

  • For preview

Backend:

  • For validation with real DOM

2. Queue-Based Execution

  • Prevents blocking
  • Enables scaling workers independently

3. Puppeteer Instead of Node Execution

  • Needed for DOM + interaction
  • Ensures accurate validation

4. WebSockets Instead of Polling

  • Real-time updates
  • Lower load

5. S3 Instead of DB Storage

  • Better scalability
  • Cleaner architecture

Top comments (0)