Designing a Booking System That Fails Safely Under Concurrency

Home page

Problem

Booking systems are inherently vulnerable to race conditions, where multiple users attempt to reserve the same time slot simultaneously. Without proper safeguards, this leads to double-booking (i.e. two confirmed reservations for a single slot) which breaks data integrity and creates real-world conflicts (e.g. overlapping appointments, lost revenue, or manual reconciliation).

This issue becomes more likely under concurrent traffic, where application-level checks alone cannot guarantee consistency. Even at small scale, a single conflicting booking can undermine trust in the system, since users expect availability to be accurate and final.

In addition, authentication systems are a common attack surface in web applications. Weak or improperly enforced authentication can allow unauthorized access to protected routes, leading to data exposure or unauthorized actions such as booking or modifying appointments.

Constraints

This project was designed under a set of practical constraints to keep the system focused and deployable as a single service.

These constraints intentionally prioritize correctness and clarity over scalability features such as distributed locking or microservices.

Key decisions

Concurrency Control (Database-Enforced Consistency)

The primary risk in a booking system is double-booking caused by concurrent requests. A naive approach (checking availability before inserting a booking) introduces a race condition where multiple requests can pass validation simultaneously.

To eliminate this class of bugs, concurrency control is enforced at the database level using a UNIQUE constraint on time_slot_id in the bookings table.

This guarantees:

Application-level checks are still used for user feedback, but correctness does not depend on them.

Alternatives such as in-memory locks or distributed locking (e.g. Redis) were not used due to added complexity and failure modes (lock expiry, synchronization issues). The database constraint provides a simpler and more reliable guarantee under the given constraints.

Authentication Strategy (JWT-Based Stateless Auth)

Authentication is implemented using JSON Web Tokens (JWT), with tokens issued upon successful login and validated via middleware on protected routes.

This approach was chosen to support a stateless API design:

Trade-offs:

Despite these limitations, JWT provides a clean separation between the frontend (SPA) and backend API, and aligns with the constraint of minimal infrastructure.

Technology Stack (PERN)

The PERN stack (PostgreSQL, Express, React, Node.js) was selected to balance simplicity, control, and data integrity.

Heavier abstractions (e.g. full-stack frameworks or opinionated backends) were intentionally avoided to keep the system transparent and focused on core backend concerns such as concurrency and authentication.

Rate Limiting (Abuse Mitigation)

Rate limiting is applied to authentication endpoints (login) to reduce the risk of brute-force attacks.

This limits repeated failed login attempts from the same client and provides a basic layer of abuse protection without introducing external infrastructure.

The scope of rate limiting is intentionally limited and does not yet cover all endpoints (see Limitations).

Failure Scenarios

Simultaneous Booking Requests (Race Condition)

When multiple users attempt to book the same time slot at the same time, all requests reach the backend and attempt to insert a booking.

Correctness does not depend on request timing or application logic. The database enforces a single valid state, ensuring double-booking cannot occur.

Invalid or Missing Authentication (Unauthorized Access)

Requests to protected routes without a valid JWT are intercepted by authentication middleware.

This ensures that only authenticated users can access booking functionality, and role-based restrictions are enforced consistently.

Duplicate Registration Attempts

If a user attempts to register with an email that already exists:

This prevents account duplication and ensures identity consistency.

Brute Force Login Attempts

Repeated login attempts from the same client are limited by rate limiting:

However, this protection currently applies only to login endpoints (see Limitations).

Token Expiry or Invalid Token Use

If a JWT is expired or tampered with:

This ensures that only valid, untampered tokens can be used to access protected resources.

Client-Side Token Exposure (XSS Scenario)

If an attacker successfully injects JavaScript into the frontend (XSS):

This does not break server-side correctness, but compromises user accounts. This risk exists due to the chosen token storage strategy (see Limitations).

Abuse via Repeated Booking/Unbooking

A user could repeatedly book and cancel the same slot:

No rate limiting or abuse detection is currently applied to booking endpoints, making this a potential area for improvement.

These scenarios were tested using concurrent requests and manual API testing (curl/Postman) to verify correct behavior under failure conditions.

Limitations and Trade-offs

JWTs are stored in localStorage, which makes them accessible to JavaScript running in the browser.

This approach was chosen for simplicity in a SPA context. A more secure production setup would use HTTP-only cookies with CSRF protection and stricter frontend input sanitization.

Limited Rate Limiting Scope

Rate limiting is only applied to login endpoints.

Extending rate limiting or introducing per-user/action-based throttling would improve resilience.

No Abuse Protection for Booking Behavior

The system allows repeated booking and cancellation actions.

Mitigation could include action-based rate limiting, cooldowns, or anomaly detection.

Stateless Auth Trade-offs

Using JWT enables a stateless API but introduces limitations:

This trade-off was accepted to keep the architecture simple and avoid additional infrastructure (e.g. session stores).

Monolithic Deployment Constraints

The application is deployed as a single Node.js service with PostgreSQL.

Future improvements could include service separation or scaling strategies if load increases.

By the end, I ensured that the frontend worked fine as it had some alignment issues, made users automatically redirect to login if they click on a booking, and finally hosted the monolith using PM2 and a reverse Nginx proxy.

See website here. You can also take a look at the demo video or the project's README.