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”:
The raw JSX (not transpiled code) is sent to the backend
Backend creates a
solutionIdThat 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:
Takes JSX string
Transpiles it again using Babel
Launches Puppeteer
Injects the code into a real browser environment
Simulates user actions
We don’t just check output.
We simulate behavior.
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 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
solutionIdEmits 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
User writes code
Code runs in iframe (preview)
User submits
Backend returns solutionId
Frontend opens WebSocket
Backend pushes job to queue
Worker picks job
Code executed via Puppeteer
Result emitted via queue event
Backend sends result via WebSocket
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
