A Banker's Algorithm visualizer, rebuilt on the MERN stack (MongoDB · Express · React · Node). The original single-file vanilla-JS app has been split into a React frontend and an Express API, with the algorithm and simulation state living on the server and persisted in MongoDB.
┌────────────────────┐ HTTP/JSON ┌────────────────────┐
│ React (Vite) │ ───────────────────▶ │ Express API │
│ │ │ │
│ components/ │ GET /api/simulation │ routes/ │
│ hooks/useSimulation│ POST /api/.../tick │ controllers/ │
│ - tick loop │ POST /api/.../start │ services/ │
│ - renders state │ ... etc │ - banker.js │
│ │ ◀─────────────────── │ - simulation.js │
└────────────────────┘ { state, analysis } └─────────┬──────────┘
│ Mongoose
▼
┌────────────────┐
│ MongoDB │
│ Simulation │
│ (singleton) │
└────────────────┘
Key design decisions
- The server owns the truth. The Banker's Algorithm (safety check, deadlock detection, recovery) and the full simulation state live in Express + MongoDB — not the browser. The React client never runs the algorithm itself.
- The client drives the animation. Because the simulation animates on a
timer, the React
useSimulationhook callsPOST /api/simulation/tickon an interval while the sim is running, and re-renders whatever state comes back. Speed and therunningflag come from the server, so the loop stops itself on deadlock or completion. - One simulation document. State is stored as a single "singleton"
document (
key: "default"), so it survives server restarts. - Uniform responses. Every endpoint returns
{ state, analysis, message }:state— persisted data (cakes, totals, flags, speed)analysis— freshly computed{ safe, sequence, blocked, available }message— optional toast text (e.g."C1 baked successfully! 🎉")
bakery-mern/
├── server/ # Express + MongoDB API
│ ├── .env.example
│ ├── package.json
│ └── src/
│ ├── server.js # entrypoint: connect DB + listen
│ ├── app.js # express app, middleware, routes
│ ├── config/
│ │ ├── db.js # mongoose connection
│ │ └── constants.js # RES_META + CAKE_EMOJIS
│ ├── models/
│ │ └── Simulation.js # Mongoose schema (singleton)
│ ├── services/
│ │ ├── banker.js # pure Banker's Algorithm
│ │ ├── factory.js # initial state + makeCake
│ │ └── simulationService.js # tick + all control logic
│ ├── controllers/
│ │ └── simulationController.js
│ └── routes/
│ └── simulationRoutes.js
│
└── client/ # React (Vite)
├── index.html
├── vite.config.js # dev proxy /api -> :5000
├── package.json
└── src/
├── main.jsx
├── App.jsx
├── styles.css # original stylesheet (unchanged)
├── api/simulation.js # fetch wrapper
├── hooks/useSimulation.js # state + tick loop + actions
└── components/
├── Header.jsx
├── ResourcesPanel.jsx
├── Conveyor.jsx
├── SafeSequence.jsx
├── ProcessTable.jsx
├── Controls.jsx
├── Legend.jsx
├── AddCakeModal.jsx
└── Toast.jsx
| Method | Path | Purpose |
|---|---|---|
| GET | /api/meta |
Resource metadata (icons, labels, emojis) |
| GET | /api/simulation |
Current state + analysis |
| POST | /api/simulation/tick |
Advance one simulation tick |
| POST | /api/simulation/start |
Begin running (clears blocked → waiting) |
| POST | /api/simulation/pause |
Stop running |
| POST | /api/simulation/reset |
Rebuild the default world |
| POST | /api/simulation/detect |
Detect deadlock |
| POST | /api/simulation/resolve |
Recover (abort cheapest victim) |
| POST | /api/simulation/cakes |
Add a cake — body { "max": [o,b,f,s] } |
| DELETE | /api/simulation/cakes/:id |
Remove a cake |
| PATCH | /api/simulation/speed |
Set speed — body { "speed": 1..10 } |
| GET | /api/scenarios |
List stored scenario presets |
| POST | /api/simulation/load |
Load a preset — body { "key": "..." } |
| POST | /api/scenarios/save |
Save current world — body { "name" } |
A single Simulation document holds the whole world:
Simulation {
key: "default", // singleton marker (unique)
total: [5, 3, 10, 10], // [Ovens, Bakers, Flour, Sugar]
cakes: [
{
id: "C1",
emoji: "🍰",
max: [1, 3, 2, 1],
allocated: [1, 1, 2, 1],
status: "running", // running | waiting | blocked | completed
progress: 0 // 0..100
}
],
deadlock: false,
running: false,
speed: 5,
cakeCounter: 4,
timestamps: true
}Beyond the one live simulation, the database also stores a library of
named configurations in a separate scenarios collection. Built-in
scenarios are seeded into MongoDB on server startup (idempotent upsert),
and the dropdown bar above the grid lets you load any of them — or save
your current world as a new one.
| Key | Category | Demonstrates |
|---|---|---|
sandbox |
safe | The default playground; bakes to completion |
safe-sequence |
safe | Safe only in one order (C2 → C3 → C1) |
circular-wait |
deadlock | 4-cake cycle, one resource per edge; recoverable |
hold-and-wait |
deadlock | Oven exhaustion — two cakes hold 2 ovens each and wait |
Loading a scenario overwrites the singleton simulation's total and
cakes (statuses reset to waiting, progress to 0). Deadlock scenarios
surface as soon as you press ▶ Start or 🔍 Detect, and ♻ Resolve
aborts the cheapest victim to recover. Saved scenarios get category
custom and a generated unique key, so they survive restarts alongside
the built-ins.
Requires Node.js 18+ and a MongoDB database (local
mongodor a free MongoDB Atlas cluster).
cd server
npm install
cp .env.example .env
# open .env and paste your real MONGO_URI
npm run dev # starts http://localhost:5000 (npm start for prod).env example:
PORT=5000
MONGO_URI="mongourlxxxxxx"
In a second terminal:
cd client
npm install
npm run dev # starts http://localhost:5173Open http://localhost:5173. The Vite dev server proxies /api/*
to the backend on port 5000, so no CORS setup is needed in development.
Just hit ↺ Reset in the UI, or delete the simulations collection in
MongoDB — the server recreates the default world automatically.