[Implementation 6] Rescheduling after Closures and Equipment Downtime

Part 5 modeled clinic closures and equipment downtime as hard constraints. Those constraints prevent a future solution from using an impossible interval. They do not finish the work when a clinic closes unexpectedly or a machine fails after appointments have already been confirmed.
The application must find affected appointments, preserve the original commitment, mark that a replacement is needed, generate alternatives, record the approval, and tell the patient. This article compares the implemented closure-rescheduling path with the implemented equipment-conflict path, then identifies the operational boundaries between them.
Preserve the Relationship between the Incident and the Appointment
Section titled “Preserve the Relationship between the Incident and the Appointment”Overwriting an appointment with a new date destroys the old commitment. It becomes difficult to answer what the patient
originally booked, why it changed, and which appointment replaced it. The current closure flow retains the original row,
moves it to PENDING_RESCHEDULE, and records state history before proposing alternatives.
| Stage | Data to preserve | Purpose |
|---|---|---|
| Determine impact | Clinic, incident date, REQUESTED and CONFIRMED appointments | Exclude completed and cancelled work |
| Await replacement | Previous state, PENDING_RESCHEDULE, reason | Express that no replacement is confirmed yet |
| Create candidates | Original ID, date, time, doctor, priority | Make calculation output reviewable business data |
| Confirm | Selected candidate, new ID, original ID | Preserve the lineage between commitments |
| Complete history | Original becomes RESCHEDULED, reason | Keep the outcome auditable |
PENDING_RESCHEDULE is an operating state, not a decorative enum value. It means the original slot can no longer be
fulfilled but a new commitment does not yet exist. That distinction supports no-candidate cases, patient confirmation,
and manual intervention without pretending that rescheduling has completed.
A Closure Connects State History to Persisted Candidates
Section titled “A Closure Connects State History to Persisted Candidates”ClosureRescheduleService
loads REQUESTED and CONFIRMED appointments on the closure date. It moves them to PENDING_RESCHEDULE, writes the
closure reason, and searches later dates with the original doctor and treatment type. The default search is seven days;
the API limits it to 1 through 30 days.
val affected = appointmentRepository.findActiveByClinicAndDate( clinicId, closureDate, AppointmentState.ACTIVE_STATUSES)
appointmentRepository.updateStatusByClinicAndDate( clinicId, closureDate, AppointmentState.ACTIVE_STATUSES, AppointmentState.PENDING_RESCHEDULE,)
stateHistoryRepository.save( AppointmentStateHistoryRecord( appointmentId = appointment.id!!, fromState = appointment.status, toState = AppointmentState.PENDING_RESCHEDULE, reason = "임시휴진으로 인한 재배정", ))Candidate generation reuses SlotCalculationService from Part 3. It walks forward from the day after the closure,
assigns increasing priority to available times, and saves each RescheduleCandidateRecord. Persisting proposals instead
of immediately choosing one lets staff compare alternatives and inspect what the system offered for a given appointment.
Send Batch Progress outside the Database Transaction
Section titled “Send Batch Progress outside the Database Transaction”An administrator may watch a large closure batch through server-sent events. Sending SSE while a database transaction is open keeps a JDBC connection occupied by network latency. A slow or disconnected client can exhaust the pool or turn a delivery failure into an unintended batch rollback.
streamClosureReschedule() therefore writes status changes and history in one shared transaction, then saves candidates
in a separate transaction per appointment. It invokes onProgress only after that transaction commits and releases its
connection. The SSE controller owns the terminal event.
for (appointment in affected) { val candidateCount = transaction { // Calculate and persist candidates for this appointment. } onProgress(appointment.id!!, candidateCount) // outside the transaction}This is an operational boundary rather than callback placement trivia. Database atomicity and network delivery have different lifetimes and failure modes. Per-appointment commits also make partial progress easier to reconcile.
Equipment Downtime Currently Stops at Conflict Detection
Section titled “Equipment Downtime Currently Stops at Conflict Detection”Equipment failure can trigger rescheduling, but its implemented automation differs from the closure path.
EquipmentUnavailabilityService
stores one-time or recurring unavailability. SKIP exceptions omit one occurrence, while RESCHEDULE exceptions move
that occurrence to a different date or time before the service expands concrete unavailable periods.
previewConflicts() finds overlapping appointments before a rule is saved. detectConflictsByTenant() finds overlaps
for a persisted rule after validating tenant ownership. Neither method changes appointment state or creates replacement
candidates.
That gap is intentional evidence about the domain. Equipment downtime introduces choices that a clinic-wide closure does not answer:
- Can another unit of the same equipment serve the treatment?
- Can the patient move to a time or clinic with a working unit?
- Is there an alternative treatment method that does not require the equipment?
- Must staff and the patient decide manually?
“Conflict detected” and “appointment rescheduled” therefore need an explicit policy boundary. The current source should not be described as automatic equipment rescheduling.

Confirmation Links a New Appointment instead of Erasing the Old One
Section titled “Confirmation Links a New Appointment instead of Erasing the Old One”When staff select a candidate, confirmReschedule() verifies that it belongs to the original appointment. The
tenant-scoped API also verifies the original appointment and candidate doctor against the tenant, and checks that the
doctor belongs to the original clinic.
The method creates a new CONFIRMED appointment at the candidate date and time and stores the original ID in
rescheduleFromId. It changes the original to RESCHEDULED, writes “재배정 확정” to state history, and marks the
candidate selected. The automatic endpoint does not bypass this contract: it selects the highest-priority candidate and
calls the same confirmation path, returning null when no candidate exists.
| Original appointment | New appointment |
|---|---|
Status becomes RESCHEDULED | Status starts as CONFIRMED |
| Original patient, treatment, and equipment data remain | Candidate date, time, and doctor are used |
| History records the reschedule | rescheduleFromId links back to the original |
This lineage supports patient questions, audit, and reporting. It is not redundant storage: it distinguishes the current commitment from the reason and source of its change.
Solver Persistence and Notification Are Separate Boundaries
Section titled “Solver Persistence and Notification Are Separate Boundaries”Part 4 and Part 5 showed how SolverService can compare a global arrangement when several appointments compete for
resources. The current solver result is still a proposal. The closure service does not invoke the solver or persist its
result automatically. A separate workflow must decide who reviews the solution and which state checks precede persistence.
Notification has the same gap. appointment-notification contains a NotificationEventListener for rescheduled events,
but ClosureRescheduleService.confirmReschedule() does not publish that domain event today. A successful reschedule must
not be assumed to have notified the patient.
A complete use case should make this sequence explicit:
- Revalidate state and tenant ownership.
- Atomically write the original state change, new appointment, history, and candidate selection.
- After commit, publish a rescheduled event or write a reliable outbox record.
- Retry and record notification failure without rolling back the appointment transaction.
- Expose delivery status and staff follow-up.
Operational Checklist: Rescheduling Must Include Confirmation, Notification, and Failure Tracking
Section titled “Operational Checklist: Rescheduling Must Include Confirmation, Notification, and Failure Tracking”Before calling rescheduling complete, verify the responsibility boundaries as carefully as the algorithm:
- Who confirms incident scope and effective times?
- Which appointment states are eligible?
- How are appointments with no acceptable candidate surfaced?
- Who approves ranked candidates or a solver proposal?
- Can the original, replacement, candidate, and history be traced together?
- Does progress delivery release database connections first?
- Are event publication and patient delivery independently observable?
- Is rerunning the same incident idempotent?
The current implementation supplies the core closure states and candidates, equipment conflict detection, and tenant ownership guards. Automatic equipment rescheduling, solver-result persistence, rescheduled-event publication, and idempotent incident reruns remain follow-up requirements. Keeping implemented and required behavior separate is the foundation of safe incident automation.
Explore the Implementation
Section titled “Explore the Implementation”- Closure rescheduling service: Pending state, candidates, manual and automatic confirmation, and appointment lineage.
- Closure rescheduling tests: Candidate generation, confirmation, no-candidate behavior, invalid candidates, and tenant isolation.
- Equipment unavailability service: Expands rules and exceptions, previews conflicts, and detects persisted conflicts.
- Equipment unavailability API: Protects tenant, clinic, and equipment boundaries around conflict operations.
- SSE batch transaction lesson: Explains why network progress moved outside the database transaction.
- Notification event listener: Connects in-process appointment events to notification channels.
Comments
Leave a note or reaction with your GitHub account.