[Design 7] Why Every Clinic Has Different Booking Rules

Why can the same request produce a different result?
Section titled “Why can the same request produce a different result?”A patient can select the same product and request the same time while receiving a different result at another clinic. Clinic A may require a staff member to confirm the request. Clinic B may confirm it immediately when the conditions have already been validated. This is not a matter of scattering clinic-name exceptions through booking code. The policy used for the decision is composed from a tenant default and a clinic-specific rule.
To explain the difference, keep four boundaries separate.
| Boundary | Question it answers |
|---|---|
| Scope | Does this rule apply to every clinic or only to one clinic? |
| Selection | Does the clinic inherit the default, replace its value, or disable an optional feature? |
| Time | When was the decision made, and when will the visit service happen? |
| History | Can we explain the original decision after the policy changes? |
A settings table alone cannot answer the last two questions. The design therefore stores policies as versioned contracts and produces an effective policy snapshot for the booking decision.
A tenant default and a clinic override are composed deliberately
Section titled “A tenant default and a clinic override are composed deliberately”Policy scope has two layers.
| Scope | Role | Example |
|---|---|---|
| Tenant default | The baseline used by every clinic in the tenant | Default approval mode, safety ceilings, operating hours |
| Clinic override | A clinic’s deliberate operational difference | Extra review for one service, disabling an optional feature |
A clinic override has three explicit intents.
| Intent | Meaning | Result |
|---|---|---|
INHERIT | Use the tenant default as-is | The default value is recorded in the effective policy |
SET | Choose a clinic-specific value | The clinic policy becomes the source for that path |
DISABLE | Do not use an optional feature | The disabled path is recorded separately |
INHERIT must not be treated as null. Null can mean “not known yet”; INHERIT means “deliberately follow the tenant value.” Conversely, a required policy or safety ceiling cannot be disabled or relaxed through DISABLE.

Keeping this composition in one place avoids reinterpreting the same rule in every caller. It also gives operations a direct answer to “why was this clinic handled differently?” The implementation separates INHERIT, SET, and DISABLE in the payload contract, while the compiler records the source of each policy path.
A policy is a versioned contract, not just a setting
Section titled “A policy is a versioned contract, not just a setting”Changing a number or option on an operations screen must not silently affect every booking. A change needs validation, approval, an activation time, and a boundary for failure or recovery.
The policy lifecycle is intentionally constrained.
DRAFT ──▶ SCHEDULED ──▶ ACTIVE ──▶ RETIRED └──────────────▶ ACTIVE └────────────────────────────▶ RETIRED| Stage | Meaning | What operations should verify |
|---|---|---|
DRAFT | A policy that is not active yet | Required fields and safety ceilings pass validation |
SCHEDULED | A policy to apply at a future time | The activation time and expected impact are clear |
ACTIVE | The policy used for current reads | It was activated with the expected generation and versions |
RETIRED | A policy no longer used for new decisions | Its historical record remains traceable |
Drafting, validation, preview, approval, scheduled or immediate activation, and retirement are separate steps. Approval is not the same thing as a lifecycle transition. An approved policy still needs a fresh check when another operator has changed the generation or activated a different version first.
An effective policy snapshot explains the decision later
Section titled “An effective policy snapshot explains the decision later”The booking decision needs more than “the clinic settings right now.” It needs the policy bundle that was effective at that moment. The snapshot carries the following fields together.
| Field | Role |
|---|---|
decisionAt | When the policy was evaluated |
serviceAt | When the visit service is expected to happen |
generation | The tenant and clinic policy generation vector |
sourceVersions | The tenant and clinic source versions used for each policy kind |
sourceByPath | Whether each value came from the tenant default or clinic override |
snapshotHash | A hash that confirms identical input produced identical output |
Conceptually, this is an immutable value object:
data class EffectiveSchedulingPolicy( val decisionAt: Instant, val serviceAt: Instant, val generation: PolicyGenerationVector, val sourceVersions: Map<SchedulingPolicyKind, SourceVersion>, val sourceByPath: Map<String, PolicyValueSource>, val snapshotHash: String,)The compiler also rejects a serviceAt that is earlier than decisionAt. The resulting snapshot keeps booking requests, previews, and operations from reading policy independently, and makes the decision reproducible later. It does not mean the policy is automatically applied to every booking; it means the policy to apply has been fixed as one verifiable result.
Close activation conflicts by preserving the previous policy
Section titled “Close activation conflicts by preserving the previous policy”Two operators can try to activate a policy for the same clinic at the same time. If the last request always wins, an earlier approved change can be overwritten without notice. The activation request therefore carries an expected revision and generation, and the write uses a CAS (Compare-And-Set) boundary.
When the generation has changed, the system should preserve the previous active policy rather than forcing the new one through:
- Reject activation when the expected generation no longer matches.
- Return the same result when a request is retried with the same idempotency key.
- Keep the previous
ACTIVEpolicy when a scheduled activation misses its activation point. - Let the operator preview and approve a new generation after understanding the conflict.
This removes the assumption that a button click means “the policy must have changed.” Activation success and a fresh effective-policy read are separate checks that let operations confirm the outcome.
A policy change does not rewrite an existing confirmed commitment
Section titled “A policy change does not rewrite an existing confirmed commitment”Policy changes are future-only. Recalculating a visit that has already been confirmed with the current policy would change a commitment the system has already made to the patient.
| Existing state | Default treatment when policy changes |
|---|---|
PROPOSED | May be evaluated with the new policy |
HELD | May use the new policy when the hold expires |
CONFIRMED | Protect the confirmed visit commitment; do not rewrite it automatically |
If a confirmed time or price must change, create a new proposal and obtain the patient’s consent again instead of overwriting the existing commitment. This is why the policy snapshot and the visit-commitment snapshot are separate. A policy is a rule for future decisions; a confirmed visit is an outcome already promised externally.
The boundary that is implemented, and the boundary that is still separate
Section titled “The boundary that is implemented, and the boundary that is still separate”This foundation creates the boundary for authoring, validating, reading an effective snapshot, and activating a policy safely. Connecting the booking-creation path so it consumes that policy is a separate integration step. The feature flags are therefore staged and disabled by default:
- shadow compile
- effective read
- admin write
- preview worker
- scheduled activation
Staging the flags makes it possible to verify that persisted policy data does not unexpectedly affect the existing booking flow. “The system can store a policy” and “booking outcomes change according to that policy” are different release claims.
Summary
Section titled “Summary”Clinics have different booking rules not because exceptions are scattered through the code. The service composes a tenant default with an explicit clinic override, then fixes the result as an effective policy snapshot with generation, source, and hash information.
The boundaries are deliberate:
INHERIT,SET, andDISABLEexpress different intent.- A policy has versions and a lifecycle.
- CAS and generation checks close activation conflicts.
- An existing
CONFIRMEDvisit commitment is not rewritten by a future policy.
The next article follows this boundary into the waitlist. Who should receive a proposal when a slot becomes available depends on when the policy is read and on the rule that protects a confirmed visit commitment.
See the visual companion
Section titled “See the visual companion”Sources
Section titled “Sources”- clinic-appointment repository
- Scheduling policy foundation design
- Scheduling policy API contract
- Scheduling policy activation runbook
SchedulingPolicyCompilerEffectiveSchedulingPolicySchedulingPolicyPayloadCodec- A waitlist is a state machine, not a queue of names
Comments
Leave a note or reaction with your GitHub account.