01 / The Context
A hospital appointment system sounds simple on paper: patients view doctors, select a time slot, and book it. In reality, it is a highly volatile, concurrency-heavy environment.
Unlike a standard SaaS app with predictable traffic, hospital systems experience aggressive spikes. When a highly sought-after specialist's schedule opens for the month, you get hundreds of concurrent requests trying to read the same schedule and book the exact same 15-minute slots.
The original MVP of this platform was built as a standard monolithic Express.js application. Under load testing, it crumbled. Response times spiked to 4+ seconds, the event loop blocked, and double-bookings occurred. The problem was not Node.js — it was how the API routing and middleware were structured.
02 / The Middleware Bloat Problem
In many Express.js applications, developers use global middleware indiscriminately. The original codebase had something that looked like this at the top of the server.js file:
Because authenticateUser and fetchUserRoles were applied globally, a patient simply trying to view the public list of doctors was triggering a JWT verification and a MongoDB user lookup.
During a traffic spike, these unnecessary database lookups exhausted the MongoDB connection pool, causing the entire API to hang.
Middleware is not free. Every function you chain to a route adds latency and consumes event loop cycles. Global database lookups are the silent killer of Express applications.
03 / Decoupling with Modular Routers
The first step was tearing down the global middleware and adopting a strictly modular routing architecture. In Express, express.Router()allows you to create isolated mini-applications.
I split the API into public routes (read-heavy, heavily cached) and private routes (write-heavy, strictly authenticated).
By isolating the routes, public schedule lookups dropped from 250ms to 15ms because they no longer hit the users collection in the database.
04 / The Async Error Wrapper
One of the biggest issues with Express.js is how it handles asynchronous errors. If a database query fails inside a route and you forget to wrap it in a try/catch block, you get an Unhandled Promise Rejection, which can crash the entire Node.js process.
The original codebase was littered with massive try/catch blocks that made the business logic unreadable. I replaced them all with a single Higher-Order Function (HOF) wrapper.
Any error thrown inside catchAsync is automatically forwarded to the Express global error handling middleware via next(). This eliminated process crashes and reduced controller file size by 30%.
05 / Route-Level Redis Caching
When a specialist opens their schedule, 95% of the traffic is read-only — users refreshing the page to see available slots. Hitting MongoDB for every schedule read is a massive waste of resources.
I implemented Redis caching specifically as a route-level middleware.
The critical part is cache invalidation. Whenever an appointment is successfully booked, the booking controller immediately deletes the relevant Redis key (redisClient.del(req.cacheKey)). This guarantees that patients never see "stale" slots that have already been taken.
06 / Transaction Safety in Bookings
The hardest problem was the "double booking" race condition. If two users click "Book" on the 10:00 AM slot at the exact same millisecond, they both read the slot as "available", and both proceed to book it.
In MongoDB, I solved this by moving away from separate read/write operations and utilizing MongoDB's atomic findOneAndUpdate with strict query conditions.
Because MongoDB executes this single document update atomically, the race condition is completely eliminated at the database level. No complex distributed locks required.
07 / Results & Takeaways
API Response times dropped by 90%
By removing global auth middleware from public routes and implementing Redis, the doctor search and schedule endpoints went from 250ms to ~15ms.
Zero Double-Bookings
Replacing two-step read/write logic with atomic findOneAndUpdate operations mathematically guaranteed that two patients could never secure the same slot.
Process Stability
The catchAsync wrapper eliminated Unhandled Promise Rejections. Errors are now caught, formatted, and returned as clean JSON to the React frontend without ever crashing the Node.js instance.
Controller Clarity
Moving logic out of massive server files into modular routers, and stripping away try/catch boilerplate made the codebase highly maintainable.
Next Case Study
ONDC Infrastructure →