pt.md

Patient-Tracker: System Architecture, Database Design & Concurrency Deep Dive

Overview: Comprehensive technical breakdown of the patient-tracker platform. This document explores the end-to-end architecture, the complete Prisma/PostgreSQL schema design, real-time doctor-patient communication with Socket.IO, the PostgreSQL partial unique index concurrency guard, and an analysis of alternative concurrency control patterns.


Table of Contents

  1. End-to-End System Architecture
  2. Database Design & Schema Breakdown (Prisma & PostgreSQL)
  3. Real-Time Doctor-Patient Communication with Socket.IO
  4. Concurrency Control: Solving Slot Double-Booking
  5. Alternative Concurrency Solutions & Trade-off Matrix

1. End-to-End System Architecture

High-Level Architecture Diagram

+========================================================================================+ | CLIENT TIER (Single Page Applications) | | | | +------------------------------------+ +------------------------------------+ | | | Patient Portal (React / Vite) | | Doctor Portal (React / Vite) | | | +------------------------------------+ +------------------------------------+ | +====================================+===============================+===================+ | | REST API (Axios + JWT) | | WebSocket / Engine.IO v v +========================================================================================+ | TRANSPORT, SECURITY & REAL-TIME TIER | | | | +------------------------------------+ +------------------------------------+ | | | CORS & Express JSON Body Parsers | | Socket.IO Server Engine | | | +-----------------+------------------+ | - Chat Rooms: join-chat, leave | | | | | - Real-Time: send/receive-message | | | v | - Push: medications-updated | | | +------------------------------------+ +-----------------+------------------+ | | | JWT Auth Middleware (RBAC Guards) | | | | +-----------------+------------------+ | | +=====================|===========================================|======================+ | | v | +=================================================================|======================+ | APPLICATION CONTROLLER TIER | | | | | | +----------------+----------------+----------------+ | | | | /auth | /doctor | /patient | | | | | Auth & JWT | Profiles/Slots| Records/Adhere| | | | +----------------+----------------+----------------+ | | | | /appointments | /chats | /api/medicines| | | | | Booking Engine| Message Store | Drug Catalog | | | | +----------------+----------------+----------------+ | | | | | | | +---------------------+---------------------+ | | | | | v | | +-------------------------------------+ | | | Prisma Client (ORM & DAL) | | | +------------------+------------------+ | +===========================================|============================================+ | v +========================================================================================+ | PERSISTENCE & DATA TIER | | | | +-------------------------------------+ | | | PostgreSQL Relational DBMS | | | | | | | | [Concurrency Guard Engine] | | | | Partial Unique B+ Tree Index: | | | | ON "Appointment"(doctorId,slotStart| | | | WHERE status <> 'cancelled'; | | | +-------------------------------------+ | +========================================================================================+

Tier Breakdown

  1. Frontend Tier (React 18 + Vite + Tailwind CSS):

    • Single Page Application (SPA) architecture offering responsive views for both Patients and Doctors.
    • Centralized API communication via Axios instances configured with automatic JWT authorization header injection.
    • Real-time client via socket.io-client maintaining a persistent full-duplex socket for live chat rooms and medication notifications.
  2. Application Tier (Node.js + Express):

    • Single-threaded, event-loop driven runtime (libuv) optimized for high I/O concurrency.
    • Dual-server architecture mounted on the same native Node HTTP server:
      • Express Application: Handles stateless request-response REST APIs.
      • Socket.IO Server: Manages stateful, persistent TCP/WebSocket connections.
    • Middleware pipeline for authentication (authenticate), CORS, and background scheduling triggers (triggerMedicationReminder, checkForMissedMedications).
  3. Persistence Tier (PostgreSQL + Prisma ORM):

    • Relational DBMS providing strict ACID guarantees.
    • Prisma acts as the type-safe Data Access Layer (DAL), generating TypeScript/JavaScript query models, managing database connections, and running automated schema migrations.

Authentication & Authorization Model

  • Stateless Tokens: Authentication is handled using JSON Web Tokens (JWT). When a user logs in via /auth/login, the server issues a signed JWT containing id, email, and role ("patient" or "doctor").
  • Role-Based Access Control (RBAC): The authenticate middleware decodes the token from Authorization: Bearer <token>, attaching req.user to the request context. Route handlers enforce strict role guards (e.g., only patients can invoke createAppointment, and only doctors can create prescriptions).

2. Database Design & Schema Breakdown (Prisma & PostgreSQL)

Entity Relationship Diagram (ERD)

+--------------------------------------------------------------------------------------------------+ | ENTITY RELATIONSHIP DIAGRAM | +--------------------------------------------------------------------------------------------------+ +--------------------------+ +--------------------------+ | PATIENT | | DOCTOR | +--------------------------+ +--------------------------+ | * id : Int (PK, AI)|<----+ +---->| * id : UUID (PK) | | name : String | | | | name : String | | email : String (UQ) | | M:N | | email : String (UQ) | | password : String? | +======+ | specializ: String | | age : Int? | DoctorToPatient| password : String | | gender : String? | +-------------+------------+ | status : String | | +-------------+------------+ | | | +---------+---------+---------+---------+ | | 1:N | 1:N | 1:N | 1:N | 1:N | v v v v v | +-------+ +-------+ +-------+ +-------+ +-------+ | |Medical| |Prescr-| |Appoint| |Medici-| |Health | | |Record | |iption | |ment | |neAdher| |Metrics| | +-------+ +---+---+ +---+---+ +-------+ +-------+ | | | | 1:N | 1:N | | | +-----------+---------------+ | | | | +-------------------+--- (Doctor 1:N Appointment) | | +-----------------------------+--- (Doctor 1:N Prescription) | v 1:N +------------------+ |PrescribedMedicine| +------------------+ | id : UUID | | prescripId: FK | | medicineId: Str | | timing : Json | +------------------+ +-----------------------+ +-----------------------+ | CHAT | 1:N | MESSAGE | +-----------------------+ +-----------------------+ | * id : UUID +------------->| * id : UUID | | patientId : Int (FK)| | chatId : UUID | | doctorId : Str (FK)| | senderId : String | | createdAt : DateTime| | senderType : String | | updatedAt : DateTime| | content : String | +-----------------------+ | readStatus : Boolean| +-----------------------+

Model-by-Model Analysis: What, How & Why

1. Patient

  • What it is: The central user entity for individuals receiving healthcare services.
  • How it is used: Stores core demographic data, authentication credentials, and acts as the parent anchor for all clinical records, appointments, and telemetry.
  • Why it is used: Isolates patient identity and privacy. Uses an auto-incrementing integer Int @id @default(autoincrement()) for compact indexing and fast joins across millions of health records.

2. Doctor

  • What it is: Represents licensed medical practitioners on the platform.
  • How it is used: Contains credentials, specialization, and relationships to assigned patients, issued prescriptions, and booked appointments.
  • Why it is used: Uses UUID primary keys (String @id @default(uuid())) to prevent ID enumeration (preventing scrapers from guessing sequential doctor IDs). Maintains an explicit M:N relation with Patient (@relation("DoctorToPatient")) allowing doctors to access medical histories only for their assigned patients.

3. Appointment

  • What it is: The temporal booking record binding a Patient, a Doctor, and a specific calendar time slot.
  • How it is used: Stores appointmentDate, slotStart (normalized to exact hour boundaries), status ("scheduled", "completed", "cancelled", "missed"), and purpose.
  • Why it is used: Serves as the critical scheduling engine. Employs a PostgreSQL Partial Unique Index on (doctorId, slotStart) where status <> 'cancelled' to provide hardware-level concurrency enforcement against double-booking.

4. Prescription & PrescribedMedicine (Parent-Child Pattern)

  • What it is: A normalized master-detail relationship representing medical prescriptions.
  • How it is used:
    • Prescription stores metadata: consultation date, condition/diagnosis, doctor ID, and patient ID.
    • PrescribedMedicine stores individual line items: dosage, instructions, duration, and a Json column for timing ({ morning: true, afternoon: false, evening: true }).
  • Why it is used:
    • Normalizing medicines into a separate table allows querying which patients are taking a specific drug without regex parsing.
    • Using PostgreSQL Json for timing provides flexibility without requiring 3 extra boolean columns in the relational table.

5. Chat & Message

  • What it is: The persistence backing for doctor-patient consultations.
  • How it is used:
    • Chat defines a conversation channel between one doctor and one patient.
    • Message stores individual messages with senderId, senderType ("doctor" or "patient"), content, and readStatus.
  • Why it is used: Decouples conversation threads from individual messages. Enables fast unread-badge calculation (WHERE chatId = ? AND readStatus = false AND senderType != role) and rapid query pagination (ORDER BY createdAt ASC).

6. MedicineAdherence & HealthMetrics

  • What it is: Longitudinal telemetry and medication compliance tracking.
  • How it is used:
    • MedicineAdherence records daily intake state ("Taken", "Missed", "Pending"), scheduling windows ("morning"), and reminder flags (reminderSent).
    • HealthMetrics records time-series values (e.g., Blood Pressure, Heart Rate, Glucose).
  • Why it is used: Enables background cron workers to run daily queries, detect missed doses, update adherence scores, and dispatch automated real-time notifications.

ID Strategy: Auto-Increment vs. UUID

The schema applies a deliberate, mixed primary key strategy:

ModelID StrategyTechnical Rationale
PatientInt (Autoincrement)Highly internal, frequently joined across all tables. 4-byte integers maximize B+ Tree branch fan-out in memory compared to 16-byte UUIDs.
DoctorString (UUID v4)Public-facing identifiers exposed in URLs and client state. UUIDs eliminate enumeration attacks (e.g., competitor crawling doctor IDs sequentially).
Appointment, Prescription, Chat, MessageString (UUID v4)Distributed generation readiness. UUIDs can be generated by client or server instances without consulting a centralized database sequence or locking a table counter.

3. Real-Time Doctor-Patient Communication with Socket.IO

What is Socket.IO? (Under the Hood)

Many developers assume Socket.IO is simply a wrapper around WebSockets. In reality, Socket.IO is a specialized real-time transport and messaging framework built on top of an underlying engine called Engine.IO.

Core Architectural Mechanics:

  1. Engine.IO Handshake & Transport Fallback: When a client initiates a connection, Socket.IO does not open a WebSocket immediately. It begins with HTTP Long-Polling (GET /socket.io/?EIO=4&transport=polling). Once the initial handshake succeeds, it probes whether the network/firewall supports the WebSocket protocol. If supported, it upgrades the connection via HTTP status code 101 Switching Protocols.
  2. Heartbeats & Liveness Monitoring: Socket.IO continuously exchanges lightweight ping-pong packets (e.g., every 25 seconds). If a client drops offline without cleanly closing the TCP socket (e.g., phone enters airplane mode or loses Wi-Fi), the server automatically detects the timeout and fires the disconnect event.
  3. Packet Framing & Buffering: Messages are framed with metadata headers (packet type, namespace, ack ID). If a connection drops temporarily, Socket.IO automatically buffers messages and attempts transparent reconnection.
  4. Rooms and Namespaces: Server-side logical partitioning. A "room" is an in-memory channel on the server. Sockets can join or leave rooms dynamically without requiring separate TCP connections.

Socket.IO vs. Raw WebSockets vs. HTTP Polling

DimensionHTTP Short/Long PollingRaw WebSockets (ws)Socket.IO
ProtocolStandard HTTP/1.1 or HTTP/2RFC 6455 TCP ProtocolEngine.IO (Polling $\to$ WebSocket Upgrade)
LatencyHigh (header overhead per poll)Ultra-low (sub-millisecond frames)Ultra-low (sub-millisecond frames)
Firewall / Proxy Compatibility100% (Standard HTTP ports)Moderate (can be blocked by corporate proxies)100% (falls back gracefully to HTTP long-polling)
Reconnection & HeartbeatsManual implementation requiredManual ping/pong & reconnection logic requiredBuilt-in automated reconnection, jitter, & heartbeat detection
Channel / Room MultiplexingManual routingManual routingBuilt-in (socket.join(room))

How Doctor-Patient Chat Works (Step-by-Step Flow)

+--------------+ +---------------+ +--------------+ +-------------+ |Patient Client| |Express API(DB)| |Socket.IO Srv | |Doctor Client| +-------+------+ +-------+-------+ +------+-------+ +------+------+ | | | | |=== Step 1: Handshake & Room Subscription ======================================| | | | |-- 1. Connect (Handshake HTTP -> WS) ----------------->| | | |<-- 2. Connect (WS) ----| |-- 3. emit("join-chat", chatId) ---------------------->| | | [socket.join(chatId)] |<-- 4. emit("join-chat")| | | [socket.join(chatId)] | | | |=== Step 2: Message Creation & Dual-Write ======================================| | | | |-- 5. POST /chats/:chatId/messages {content} --------->| | | (with JWT Bearer Token) | | | | |-- 6. Verify Auth & Chat| | | | Membership | | | |-- 7. prisma.message.create() | | |-- 8. prisma.chat.update(now) | |<- 9. HTTP 201 Created -------| | | | (Confirmed in PostgreSQL) | | | | | |-- 10. emit("send-message", messageData) ------------->| | | |-- 11. io.to(chatId). | | | emit("receive- | | | message", data) | | |----------------------->| | | (Instant Push Delivery)| |<- 12. "receive-message" echo -------------------------| | | | | |=== Step 3: Real-Time UI Render ================================================| | | | | [UI Appends Message Bubble] | [UI Appends Message] | | | |

Dual-Write Pattern: REST Persistence + Socket Broadcast

In frontend/src/Components/Chat/ChatComponent.jsx, message dispatch uses a Dual-Write Architecture:

// 1. Prepare message payload const messageData = { content: newMessage, chatId: chatId, senderId: userId, senderType: userRole, createdAt: new Date().toISOString() }; // 2. Step 1: Write to Database via REST (Guarantees persistence & auth) await apiClient.post(`/chats/${chatId}/messages`, { content: newMessage }, { headers: { Authorization: `Bearer ${token}` } } ); // 3. Step 2: Emit to Socket.IO Room (Instant zero-latency delivery) if (socket) { socket.emit('send-message', messageData); }

Why use this Dual-Write pattern instead of sending messages purely over WebSockets?

  1. Reliability & Idempotency: If the WebSocket connection blips or is reconnecting, the REST call ensures the message is safely committed to disk in PostgreSQL.
  2. Security & Auditing: The REST controller (backend/chat/messageController.js) verifies JWT claims against the database, ensuring a malicious user cannot emit messages into rooms they don't belong to:
    const isPatient = decoded.role === 'patient' && chat.patientId === parseInt(decoded.id); const isDoctor = decoded.role === 'doctor' && chat.doctorId === decoded.id; if (!isPatient && !isDoctor) { return res.status(403).json({ error: 'Forbidden' }); }
  3. Server-Side Room Dispatch: The Socket server (backend/chat/chatSocket.js) simply broadcasts the verified payload to all sockets joined to that room:
    socket.on("send-message", (messageData) => { io.to(messageData.chatId).emit("receive-message", messageData); });

Background Telemetry & Medication Alert Broadcasting

Socket.IO is also integrated with the platform's automated healthcare monitors.

In backend/app.js:

global.io = io; configureSocketIO(io);

When scheduled tasks in backend/scheduleTasks.js or middleware detect that a patient missed a dose or that new adherence telemetry has been logged, the server accesses global.io to emit:

io.emit("medications-updated", { patientId, status: "updated" });

The patient's dashboard (frontend/src/Pages/PatientDashboard.jsx) listens for medications-updated and dynamically refreshes the medication checklist without requiring a page reload.


4. Concurrency Control: Solving Slot Double-Booking

The Problem: TOCTOU Race Condition

Consider Dr. Sharma who has an open slot at 10:00 AM on Monday. Two patients, Patient A and Patient B, open the booking page simultaneously.

Time Patient A (Thread 1) Patient B (Thread 2) │ t1 findFirst(Dr. Sharma, 10:00 AM) │ ──► Returns null (Slot is free!) │ t2 findFirst(Dr. Sharma, 10:00 AM) │ ──► Returns null (Slot is free!) │ t3 create({ Dr. Sharma, 10:00 AM }) │ ──► INSERT SUCCESS! │ t4 create({ Dr. Sharma, 10:00 AM }) │ ──► INSERT SUCCESS! (DOUBLE BOOKING BUG!) ▼

This is a classic Time-of-Check to Time-of-Use (TOCTOU) race condition:

  • The check (findFirst) and the action (create) are two separate, non-atomic database operations.
  • Pure application-level checks (if (!conflictingAppointment)) are completely blind to concurrent requests executing on other Node.js event-loop ticks or horizontal server instances.

The Chosen Solution: PostgreSQL Partial Unique Index

Instead of adding heavy locking infrastructure, the system utilizes PostgreSQL's native write-ahead logging and B+ Tree uniqueness guarantees via a Partial Unique Index.

The Core Concept

A standard unique index enforces uniqueness across all rows in a table. A Partial Unique Index enforces uniqueness only on a subset of rows that satisfy a specific SQL WHERE predicate.

CREATE UNIQUE INDEX "Appointment_doctorId_slotStart_active_key" ON "Appointment" ("doctorId", "slotStart") WHERE status <> 'cancelled';
Table: Appointment ┌──────────────────┬──────────────────────┬─────────────┬────────────────────────────────────┐ │ doctorId │ slotStart │ status │ Evaluated by Partial Unique Index? │ ├──────────────────┼──────────────────────┼─────────────┼────────────────────────────────────┤ │ doc-uuid-1 │ 2026-09-20 10:00:00 │ scheduled │ YES ──► Enforces Uniqueness │ │ doc-uuid-1 │ 2026-09-20 10:00:00 │ cancelled │ NO ──► Ignored by Index! │ │ doc-uuid-1 │ 2026-09-20 10:00:00 │ scheduled │ REJECTED! (Duplicate Active Slot) │ └──────────────────┴──────────────────────┴─────────────┴────────────────────────────────────┘

Why is this design brilliant?

  1. Hardware-Enforced Atomicity: PostgreSQL's storage engine locks the index leaf node during insertion. Two concurrent transactions can never both insert active bookings for the same doctor and slot. Exactly one succeeds; the second is aborted by PostgreSQL with a unique constraint violation error.
  2. Slot Re-bookability without Data Loss: In healthcare systems, you cannot delete records when an appointment is cancelled (audit trail and legal compliance require retaining cancelled rows). If we had used a normal unique index on (doctorId, slotStart), cancelling an appointment would permanently block that slot forever! The WHERE status <> 'cancelled' clause leaves cancelled rows in the table while freeing the slot in the index for new bookings.
  3. Zero Runtime Overhead: No external cache checks, no distributed lock managers, and no database table locks.

How the Migration Was Executed (SQL Deep Dive)

In backend/prisma/migrations/20250806120000_add_appointment_slot_unique_constraint/migration.sql:

-- Step 1: Add slotStart column (nullable during backfill to avoid locking the table) ALTER TABLE "Appointment" ADD COLUMN "slotStart" TIMESTAMP(3); -- Step 2: Backfill from existing appointmentDate, truncating to exact hour boundary UPDATE "Appointment" SET "slotStart" = date_trunc('hour', "appointmentDate") WHERE "slotStart" IS NULL; -- Step 3: Keep appointmentDate aligned with the normalized slot UPDATE "Appointment" SET "appointmentDate" = "slotStart" WHERE "slotStart" IS NOT NULL; -- Step 4: Resolve legacy duplicate active bookings using Window Functions before adding constraint WITH ranked AS ( SELECT id, ROW_NUMBER() OVER ( PARTITION BY "doctorId", "slotStart" ORDER BY "createdAt" ASC ) AS rn FROM "Appointment" WHERE status <> 'cancelled' ) UPDATE "Appointment" AS a SET status = 'cancelled' FROM ranked AS r WHERE a.id = r.id AND r.rn > 1; -- Step 5: Enforce NOT NULL after backfill is complete ALTER TABLE "Appointment" ALTER COLUMN "slotStart" SET NOT NULL; -- Step 6: Create the Partial Unique Index CREATE UNIQUE INDEX "Appointment_doctorId_slotStart_active_key" ON "Appointment" ("doctorId", "slotStart") WHERE status <> 'cancelled';

Application-Level Error Handling (Prisma P2002)

In backend/appointment/createAppointment.js:

try { // Fast-path check (optimistic check to avoid DB errors under normal traffic) const conflictingAppointment = await prisma.appointment.findFirst({ where: { doctorId: doctorIdStr, slotStart, status: { not: "cancelled" }, }, }); if (conflictingAppointment) { return res.status(409).json({ error: "This time slot is already booked" }); } // Attempt the insert; database partial unique index acts as the final concurrency barrier const appointment = await prisma.appointment.create({ data: { patientId: patientIdInt, doctorId: doctorIdStr, appointmentDate: slotStart, slotStart, status: "scheduled", purpose: purpose || "General checkup", }, }); return res.status(201).json(appointment); } catch (error) { // Catch PostgreSQL unique index violation translated by Prisma if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002" ) { return res.status(409).json({ error: "This time slot is already booked" }); } console.error("Error creating appointment:", error); return res.status(500).json({ error: "Failed to create appointment" }); }
  • If two concurrent requests pass findFirst, the first insert wins.
  • The losing insert fails at the PostgreSQL index level. Prisma intercepts the PostgreSQL constraint violation (SQLSTATE 23505) and exposes it as code P2002.
  • The controller catches P2002 and returns a clean, user-friendly HTTP 409 Conflict, completely preventing data corruption.

5. Alternative Concurrency Solutions & Trade-off Matrix

While the Partial Unique Index is the optimal solution for this architecture, interviewers (especially at Juspay) love testing your ability to evaluate alternative concurrency control patterns.

Solution 1: Pessimistic Row-Level Locking (FOR UPDATE)

How it works:

Before inserting or modifying, lock the resource explicitly at the database level inside an active transaction:

BEGIN; -- Locks all matching rows for the duration of this transaction SELECT * FROM "Appointment" WHERE "doctorId" = 'doc-123' AND "slotStart" = '2026-09-20 10:00:00' AND status <> 'cancelled' FOR UPDATE; -- Check if row count > 0; if 0, proceed to insert: INSERT INTO "Appointment" (...) VALUES (...); COMMIT;

Why it was not chosen here:

  • The Phantom Read Dilemma: If no row exists yet for that slot, SELECT ... FOR UPDATE has no row to lock! Under standard Read Committed isolation, two concurrent transactions will both see 0 rows and both proceed to insert (unless you use Predicate Locking or Gap Locking, which are expensive and complex).
  • Throughput Bottleneck: Acquiring database row/table locks blocks concurrent worker threads, increasing latency and connection pool pressure.

Solution 2: Optimistic Locking with Versioning (CAS)

How it works:

Treat the doctor's calendar schedule as an explicit entity with a version integer:

-- Step 1: Read doctor's schedule version SELECT id, version FROM "DoctorSchedule" WHERE "doctorId" = 'doc-123'; -- Step 2: Update conditionally using Compare-And-Swap (CAS) UPDATE "DoctorSchedule" SET version = version + 1 WHERE "doctorId" = 'doc-123' AND version = 3;

If rows_affected == 0, another transaction updated the schedule first; abort and retry.

Why it was not chosen here:

  • Requires maintaining an artificial DoctorSchedule or slot inventory table with pre-allocated slots for every doctor and hour of the year.
  • High abort-and-retry rate under peak traffic (e.g., popular doctor slot opening).

Solution 3: Distributed Locks via Redis (Redlock / SET NX)

How it works:

Acquire an exclusive lock across application servers using an in-memory Redis cluster before touching the database:

const lockKey = `lock:appointment:${doctorId}:${slotStart.getTime()}`; // Atomically acquire lock with 5-second TTL const acquired = await redis.set(lockKey, "locked", "NX", "EX", 5); if (!acquired) { return res.status(409).json({ error: "Slot currently being booked. Try again." }); } try { // Execute database check and insert } finally { // Release lock via Lua script await releaseLock(lockKey); }

Trade-offs:

  • Pros: Fast in-memory acquisition; prevents requests from even hitting the relational database.
  • Cons: Introduces an external infrastructure dependency (Redis). If the Redis cluster crashes or experiences clock drift/network partitions, locks can be prematurely released, leading to race conditions. Overkill when PostgreSQL can enforce it natively.

Solution 4: Serializable Transaction Isolation

How it works:

Run the booking query inside a transaction with isolation set to SERIALIZABLE:

await prisma.$transaction(async (tx) => { const existing = await tx.appointment.findFirst({ ... }); if (existing) throw new Error("Booked"); return await tx.appointment.create({ ... }); }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable });

Trade-offs:

  • Pros: Pure application logic without custom indexes; mathematically guarantees serial execution.
  • Cons: PostgreSQL enforces Serializable isolation using SSI (Serializable Snapshot Isolation) which tracks read/write dependency locks (SIREAD locks). Under concurrent bookings, PostgreSQL will frequently throw serialization failure errors (40001: could not serialize access), requiring complex retry logic and wasting CPU cycles.

Solution 5: Single-Threaded Job Queue / Actor Model

How it works:

Enqueue all booking requests into a message queue (e.g., BullMQ / RabbitMQ) partitioned by doctorId. A single worker process handles bookings for a given doctor sequentially.

Client Requests ──► [Queue Partition: Doctor_A] ──► [Single Worker Thread] ──► Database

Trade-offs:

  • Pros: Completely eliminates database race conditions by eliminating concurrency altogether at the execution layer.
  • Cons: Converts asynchronous HTTP requests into asynchronous polling jobs (the client must wait or poll for job completion). Dramatically increases architectural complexity.

Comprehensive Trade-off Comparison Matrix

Concurrency MechanismImplementation ComplexityLatency / PerformanceExternal DependenciesResiliency to High ContentionSlot Re-bookability
PostgreSQL Partial Unique Index (Chosen)Very Low (Single SQL index + catch P2002)Ultra-Fast (Native B+ Tree write-time check)None (PostgreSQL native)Highest (Deterministic serialization at disk page)Seamless (WHERE status <> 'cancelled')
Pessimistic Locking (FOR UPDATE)Medium (Requires explicit transactions & gap locks)Slow (Locks held for entire transaction duration)None (PostgreSQL native)Poor (Prone to deadlocks & phantom reads)High
Optimistic Locking (CAS)High (Requires versioned slot inventory tables)Fast for low contention; slow on retriesNone (PostgreSQL native)Moderate (High abort rate on hot slots)High
Distributed Lock (Redis SET NX)High (Requires Redis cluster, TTL tuning, Lua scripts)Fast (In-memory)High (Redis cluster)Moderate (Risk of split-brain or TTL expiry during GC pauses)High
Serializable IsolationLow (Framework transaction configuration)Slow (High overhead of tracking SIREAD locks)None (PostgreSQL native)Poor (Frequent 40001 serialization failures requiring retries)High
FIFO Message Queue / Actor ModelVery High (Worker fleet, async status polling, job state)Moderate (Queue latency + async turnaround)High (Kafka / RabbitMQ / Redis)High (Completely eliminates concurrency)High

6. Key Takeaways for Senior Engineering Interviews

  1. Architecture is defined by its boundaries: Synchronous REST is for the critical path where the client requires immediate consistency; asynchronous queues and WebSockets are for decoupled side-effects, real-time broadcasts, and background telemetry.
  2. Socket.IO is not just WebSockets: It is an entire real-time orchestration system with Engine.IO HTTP handshakes, automatic transport upgrades, heartbeat timeout detection, transparent reconnection, and in-memory room multiplexing.
  3. Always solve concurrency at the lowest viable layer: Application-level checks are vulnerable to TOCTOU races. Solving slot double-booking at the database engine level using a Partial Unique Index provides bulletproof concurrency guarantees with zero additional network hops or infrastructure dependencies.
  4. Partial indexes provide business agility: By scoping the unique index with WHERE status <> 'cancelled', the business retains historical and audit records of cancelled appointments while immediately restoring slot availability to other patients.