For technical research and educational purposes only. All system designs, architecture decisions, data models, and API patterns described in this article are proprietary and confidential. They may not be reproduced, adapted, reverse-engineered, or used commercially without explicit written permission from the author. Code references and schema details have been simplified for illustrative purposes and do not represent the exact production implementation.
A technical walkthrough of how a hospital outpatient department tames the chaos of patient queues — from data modelling and booking flows to real-time token tracking and shift orchestration.
Walk into any busy hospital outpatient department before this system and you will find the same scene: rows of anxious patients clutching paper tokens, no idea when they will be called, staff shouting names over a noisy lobby, and doctors sitting idle because the next patient stepped out for tea.
The pain points break down into three categories:
The QMS backend follows a classic layered architecture built on Spring Boot. A single controller routes all incoming requests to two specialised service classes, which in turn talk to the database through JPA repositories.
The QmsAppointmentController does no business logic. It unpacks the session from the request attribute, delegates entirely to a service, and wraps the response in a standard ApiResponse<T> envelope. This keeps services fully testable in isolation and makes the controller trivially readable.
The split between QmsAppointmentService and QmsShiftSessionService reflects a clean domain boundary: one service owns the appointment lifecycle (bookings, tokens, queue ordering), the other owns the shift session lifecycle (starting/ending doctor shifts). They are deliberately decoupled.
The entire system pivots around a single entity: QmsAppointment. It inherits audit fields from BaseEntity (id, createdAt, deleted, createdBy, etc.) and extends them with all appointment-specific concerns.
Every table in this system inherits from BaseEntity, a mapped superclass that automatically injects creation and update timestamps (both human-readable LocalDateTime and epoch-second longs), a soft-delete flag, and creator/updater tracking. The dual timestamp approach (LocalDateTime + epoch) is deliberate — epoch integers are fast to compare in queries while the LocalDateTime forms are human-readable in logs and UI.
The fields cluster into five logical groups:
The deleted flag on BaseEntity means no appointment is ever physically removed. This preserves visit history for regulatory compliance, enables "uncancel" workflows, and ensures invoice references never become dangling foreign keys.
The system supports two distinct booking modes that map directly to two controller endpoints. They share the same request DTO but follow different validation and queue-insertion logic.
A critical difference between the two flows: in immediate booking, the response message is dynamic — "Appointment booking failed!" when the service returns null, versus the generic "Appointment booked." for pre-booking (where a null response is treated as success with a flag). This is intentional: immediate booking failure is user-visible, while pre-booking failures throw exceptions earlier in the chain.
Every appointment travels through a state machine. The QmsAppointmentStatus enum drives the entire queue logic — which appointment is currently active, what's next, and what's done.
Separate from the token number, the position field represents the appointment's place in today's live queue. It starts equal to the token number but can be reordered via PUT /position/update. This lets a receptionist drag an emergency case to the top without changing anyone's token number (the token is the permanent identifier sent to the patient's phone).
A doctor's shift is not just a time window — it's a session object that must be explicitly started. This models the real-world handoff: a doctor arrives, signals readiness, and the system activates the first appointment. Without an explicit shift start, no appointment advances to IN_PROGRESS.
The QmsShiftSessionService manages three operations: start, end, and status check.
Shift hours alone cannot reliably indicate "the doctor is ready." Doctors may be late, handle paperwork first, or start early. The POST /shift/start endpoint gives the clinic control and creates an audit record: who started the shift and when. The shift status check endpoint (GET /shift/status) lets the frontend disable the "Start shift" button if it's already running, preventing duplicate starts.
The token system is how a patient knows their turn without standing in line. When an appointment is booked (or confirmed at the shift start), the patient receives a short-link via SMS or email. That link carries an encrypted payload that uniquely identifies their appointment in the queue.
The GET /status?data= endpoint accepts an encrypted string rather than a raw appointment ID. This design prevents patients from polling other patients' statuses (no sequential ID enumeration) and hides the internal database key from the public-facing URL.
The status response includes the current token number being served and an estimated wait time. This is computed from the appointments ahead in the queue multiplied by the average consultation duration (stored as duration_in_seconds on past appointments). As each appointment completes, all ETA values downstream automatically become more accurate.
All endpoints are under the base path /v1/qms-appointments. Session context (workspace, user identity) is resolved from a SessionDetail object injected by the authentication filter, not from request parameters.
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /pre/book | Book an appointment for a future date. Validates shift, assigns token. |
| POST | /immediate/book | Walk-in booking on the day of appointment. Checks live queue capacity. |
| GET | /doctor/shifts | List available shifts for a doctor on a given date. Used to populate booking UI. |
| PUT | /{appointmentId}/update | Update appointment status (COMPLETED, CANCELLED, SKIPPED, etc.). |
| PUT | /position/update | Drag-and-drop reorder: change a patient's position in the live queue. |
| GET | /visits | Paginated visit history with filters: date, doctor, patient, status, search. |
| GET | /status?data= | Public endpoint. Patient polls queue position and ETA via encrypted token. |
| GET | /tokens/send | Bulk-send token SMS/email to all patients booked for a given date. |
| POST | /shift/start | Start a doctor's shift. Marks the first BOOKED appointment as IN_PROGRESS. |
| POST | /shift/end | End a doctor's shift. Closes out remaining appointments. |
| GET | /shift/status | Check whether a specific shift has been started or ended. |
| POST | /bulk/create | Testing utility. Creates multiple appointments in one call. |
| GET | /token/generate/all | Testing utility. Generates tokens for all appointments on a date. |
ApiResponse<T> with success (boolean), message (human-readable), and data fields. This lets clients check success before deserialising the payload, and gives frontend teams predictable error messaging without inspecting HTTP status codes alone.