Skip to content

[Implementation 6] Rescheduling after Closures and Equipment Downtime

Small robotic workers move red appointment tiles from a closed clinic schedule and broken medical equipment through a rescheduling conveyor into a new schedule board on a dark miniature workbench
An operational disruption requires more than another free slot. Preserve the original commitment, record why it changed, review alternatives, and communicate the confirmed outcome.

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.

StageData to preservePurpose
Determine impactClinic, incident date, REQUESTED and CONFIRMED appointmentsExclude completed and cancelled work
Await replacementPrevious state, PENDING_RESCHEDULE, reasonExpress that no replacement is confirmed yet
Create candidatesOriginal ID, date, time, doctor, priorityMake calculation output reviewable business data
ConfirmSelected candidate, new ID, original IDPreserve the lineage between commitments
Complete historyOriginal becomes RESCHEDULED, reasonKeep 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.

Diagram showing a clinic closure implementation moving active appointments to pending reschedule and persisting candidates, an equipment downtime implementation returning conflicts, and both paths meeting at review, confirmation, persistence, and notification boundaries
The closure service reaches candidate persistence and confirmation; the equipment service reaches conflict detection. Review, final persistence, event publication, and notification remain explicit boundaries rather than assumed automation.
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 appointmentNew appointment
Status becomes RESCHEDULEDStatus starts as CONFIRMED
Original patient, treatment, and equipment data remainCandidate date, time, and doctor are used
History records the reschedulerescheduleFromId 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:

  1. Revalidate state and tenant ownership.
  2. Atomically write the original state change, new appointment, history, and candidate selection.
  3. After commit, publish a rescheduled event or write a reliable outbox record.
  4. Retry and record notification failure without rolling back the appointment transaction.
  5. 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.

Comments

Leave a note or reaction with your GitHub account.