Behavioral Spec میگوید در رقابت برای یک بازه دقیقاً یک reschedule موفق باشد. اما `if (slotFree) update()` در دو request همزمان ممکن است برای هر دو «آزاد» ببیند و هر دو را موفق کند. Technical Design پل میان رفتار و مکانیزم است: قراردادها، data model، transition، خطاها و failure mode را آنقدر روشن میکند که implementation، migration و rollback بر سر یک مدل عمل کنند.
## اهداف یادگیری
پس از این درس میتوانی برای rescheduling یک technical design بنویسی که API contract، data model، state transition، idempotency، concurrency، error، observability، migration و rollback دارد؛ تصمیم معماری پایدار را در ADR ثبت کنی؛ و میان policy رفتاری و mechanism فنی مرز بگذاری.
## جای درس در چرخهٔ سرتاسری
Behavioral Spec درس ۱۳ outcome و invariant را canonical کرده است. این درس در Specify → Slice → Test قرار میگیرد: راه رسیدن به آن outcome و محدودیت تغییر Brownfield را طراحی میکند. ExecPlan درس ۱۵ design پذیرفته را به گامهای pathدار و قابلverify تبدیل میکند. طراحی هنوز evidence پیادهسازی نیست؛ باید قابل نقد باشد.
## از Rule به مرزهای فنی
هر بخش design باید یک Rule یا constraint بیرونی را پاسخ دهد. R-03 «حداکثر یک موفقیت همزمان» به transaction، database constraint و concurrent integration test میرسد. R-05 «تکرار همان request اثر دوم ندارد» به ذخیرهٔ idempotency record و بازپخش response میرسد. اما «refund انجام شود» اگر non-goal است، حتی design تمیز هم مجاز به افزودنش نیست.
طرح را با assumptions بنویسید: PostgreSQL یا datastore واقعی، transaction isolation در دسترس، appointmentها row version دارند، و migration pipeline چگونه اجرا میشود. اگر اینها از repository map با confidence پایین آمدهاند، link/provenance بدهید. Agent نباید database یا package تازه را تنها چون نمونهٔ اینترنتی آن را پیشنهاد میکند، فرض کند.
## قرارداد API و مدل داده
PATCH /v1/appointments/{appointmentId}/reschedule
Idempotency-Key: 8b9c4e10-61aa-43d3-b2d7-0f0a4e7cc812
Content-Type: application/json
{ "startLocal": "2026-06-15T11:00:00", "endLocal": "2026-06-15T11:30:00" }
200 { "id": "A", "startAt": "2026-06-15T07:00:00Z",
"endAt": "2026-06-15T07:30:00Z", "version": 8 }Authorization derives `tenantId` and `actorId` from authenticated context, never from a client body field. Clinic timezone comes from the clinic record and interprets local input before UTC persistence. The accepted contract is: `400 INVALID_IDEMPOTENCY_KEY` for a missing/invalid key; `403 APPOINTMENT_NOT_OWNED` for a visible same-tenant appointment owned by another patient; `404 APPOINTMENT_NOT_FOUND` for an unknown or cross-tenant id; `409 RESCHEDULE_WINDOW_CLOSED`, `SLOT_CONFLICT`, `IDEMPOTENCY_KEY_REUSED`, `REQUEST_IN_PROGRESS`, or `APPOINTMENT_NOT_RESCHEDULABLE`; and `422 INVALID_INTERVAL` or `INVALID_LOCAL_TIME`. Every error body has `code` and `correlationId`, never SQL, stack trace or PII.
A minimal logical model is:
appointments(id, tenant_id, doctor_id, patient_id, status,
start_at_utc, end_at_utc, version, updated_at)
idempotency_records(tenant_id, actor_id, operation, key,
request_fingerprint, response_status, response_body, expires_at)
appointment_audit(id, appointment_id, tenant_id, actor_id,
event_type, occurred_at, correlation_id)Unique scope for the key includes tenant, actor and operation so another tenant cannot replay it and a cancel key cannot collide with reschedule. `request_fingerprint` lets the service distinguish an honest retry from a different intent. Response storage/replay needs retention and privacy review: do not store secrets or surplus PII merely for convenience.
## حالتها و مسیر transaction
confirmed ──valid reschedule──> confirmed (new interval, version + 1)
confirmed ──cancel separately──> cancelled
cancelled ──reschedule─────────> rejected: 409 APPOINTMENT_NOT_RESCHEDULABLEReschedule is a transition with guards: same tenant, actor ownership/role, confirmed state, valid local time, policy deadline, available interval and valid idempotency key. `422 INVALID_LOCAL_TIME` و `422 INVALID_INTERVAL` به مقدار ورودی نامعتبر مربوطاند؛ `409 APPOINTMENT_NOT_RESCHEDULABLE` یعنی request معتبر است اما state فعلی resource انتقال را نمیپذیرد. It is not delete-then-create: identity and audit continuity matter. A successful transaction atomically reserves the new interval, updates the target appointment, writes audit, and records the idempotency response. If any guard/constraint fails, it emits a defined error and leaves the appointment unchanged.
Concurrency is not solved by preflight availability read. The selected PostgreSQL mechanism enables `btree_gist` and adds `EXCLUDE USING gist` as a partial GiST exclusion constraint on `(tenant_id, doctor_id, tstzrange(start_at_utc,end_at_utc,'[)'))` where `status='confirmed'`. The reschedule transaction reads/claims idempotency, locks the target appointment with `SELECT id FROM appointments WHERE id = $1 FOR UPDATE`, updates it, writes audit and commits. SQLSTATE `23P01` maps to domain `SLOT_CONFLICT`, then to HTTP `409 SLOT_CONFLICT`; the test runs two barrier-synchronized transactions. This protects another write path too, while the service still owns contract translation.
Avoid holding a database transaction across notification or external provider calls. Commit the booking state first; publish an outbox/audit event atomically if notification enters Scope later. Otherwise a timeout could hold locks, repeat side effects, or report a successful external message when the booking rolled back.
## Idempotency and error semantics
Idempotency starts by atomically claiming/reading the scoped key. If an existing fingerprint matches a completed record, return its recorded semantic response; do not append another business audit event. If it differs, return the defined key-reuse error. If a prior execution is in progress, contract must choose wait, retryable conflict, or status resource; the choice affects client behavior and needs a timeout/recovery story. Do not expose a raw database unique violation as API behavior.
Failure paths need equal care. A conflict must not change either old or competing appointment. A database retry caused by serialization failure is internal only if final response obeys the same contract; bounded retry, telemetry and final error must be specified. A crash after booking commit but before a response is exactly why idempotency replay exists. A client timeout proves neither success nor failure; retry with the same key is the recovery contract.
## Migration and compatibility
For an existing production table, use expand/contract:
1. Add nullable/new structures, index or constraint in a compatible migration; assess lock duration on production-sized data. 2. Create `apps/api/src/config/feature-flags.ts` with `RESCHEDULE_V1_ENABLED=false` by default; staging test/release explicitly sets it true before code writes old-compatible and new records. 3. Backfill only if needed, in bounded batches with progress and safe restart. 4. Verify reads/writes and constraint violations with metrics; only later remove old paths in a separately reversible change.
Schema migration is not transaction rollback. A destructive column removal may be impossible to reverse after new writes. Rollback means set `RESCHEDULE_V1_ENABLED=false` in `apps/api/.env.staging`, reload API configuration, deploy prior code only while schema remains backward compatible, retain idempotency/audit records, and define manual repair for partial backfill. `down` SQL is not sufficient evidence. The plan must name point-of-no-return, backup/restore authority and observable stop condition.
## Observability as part of the design
Emit structured, privacy-safe counters/fields: `reschedule.outcome` (success/conflict/invalid/deadline), tenant hash or approved scoped identifier, correlation ID, idempotency result (new/replay/reused), transaction retry count and latency. Never log raw appointment medical notes, auth tokens or raw key. Dashboard/alert thresholds are operational decisions, not guaranteed by a metric name. Trace links from request through transaction to audit explain a disputed outcome without claiming logs are authorization evidence.
## ADR: تصمیم پایدار، نه داستان جلسه
# ADR-021: پایگاهداده مرجع یکتایی interval رزرو است
Status: Accepted — 2026-09-05
Context: R-03 نیاز دارد دو درخواست concurrent فقط یک interval confirmed بسازند.
Decision: PostgreSQL `btree_gist` و partial GiST exclusion constraint روی tenant، doctor و UTC range
برای status confirmed؛ transaction با `SELECT ... FOR UPDATE` target، update و audit را commit میکند.
SQLSTATE 23P01 به domain `SLOT_CONFLICT` و HTTP 409 تبدیل میشود.
Options: preflight read only (رد: race)؛ distributed lock تنها (رد: bypass path و recovery).
Consequences: migration/index و مشاهدهٔ contention لازم است؛ A-08 دو transaction barrierدار را اجرا میکند.
Links: Behavioral Spec R-03/A-08؛ ExecPlan P3 و P5.ADR نمیگوید policy deadline چیست مگر تصمیم معماری مانند time authority را ثبت میکند. decision باز adjacent slot یا code-level naming یک ADR نیست. اگر datastore mechanism نامعلوم است، ADR Accepted ننویسید؛ discovery و `DECIDE` باقی میماند.
## مثال واقعی: design reschedule
Route شناسهٔ appointment را میگیرد، identity tenant/actor را از middleware میگیرد، clinic timezone را میخواند و local input را validate میکند. service fingerprint را canonical میکند، record idempotency را claim میکند، سپس در transaction ownership/state/deadline را میسنجد و mutation را به constraint میسپارد. commit موفق audit و response را با هم durable میکند. test integration دو transaction هماهنگ را آزاد میکند و دقیقاً یک 200 و یک 409، یک interval confirmed و audit یکتای موفق را assert میکند. E2E مسیر UX را جدا بررسی میکند؛ E2E جای proof race نیست.
مثال ضعیف و علت شکست
if (await slots.isFree(doctorId, start, end)) {
await appointments.update(id, { start, end });
return { ok: true };
}بین read و update درخواست دیگر میتواند slot را بگیرد. tenant، state، deadline، DST، response replay و audit هم ناپدیدند. افزودن یک mutex process-local نیز برای چند instance و bypass admin path کافی نیست. این snippet رفتار Spec را نشان نمیدهد، فقط happy path ترتیبی را نشان میدهد.
مثال اصلاحشده
در transaction:
idempotency record scoped را atomically claim/read کن؛ fingerprint ناسازگار → 409.
actor/tenant/state/deadline/local-time را validate کن.
appointment را با guard version و datastore constraint به UTC interval تازه منتقل کن.
audit و response replay record را در همان commit بساز.
پس از commit، response را برگردان؛ conflict constraint → 409 بدون mutation.این pseudo-flow هنوز به migration و engine دقیق وابسته است، اما atomic boundary و error translation را صریح میکند.
## آزمایشگاه: design و ADR را بازبینی کن
**ورودی:** Behavioral Spec R-01 تا R-06 و repository map. **گامها:** ۱) API request/response/error contract را بنویس. ۲) model و state transition را رسم کن. ۳) برای هر Rule mechanism و test level بده. ۴) atomic boundary، key scope/fingerprint و in-progress retry را مشخص کن. ۵) migration expand/contract و rollback point را بنویس. ۶) ADR concurrency را با گزینه و consequence ثبت کن. ۷) telemetry بدون PII طرح کن.
**فرمانهای read-only پیشنهادی:**
rg -n "migration|transaction|appointment|idempotency" apps packages
rg -n "test.*appointment|appointment.*test" package.json apps packages**خروجی مورد انتظار:** فرمان نخست pathهای schema/transaction و دومی command/test candidateها را نشان میدهد؛ match نبودن permission ساخت schema تازه نیست. design نهایی یک contract، transition، atomic boundary، migration/rollback و ADR link دارد و هر کدام به Rule/Example trace میشوند.
**Recovery:** اگر constraint موردنظر توسط datastore پشتیبانی نمیشود، pseudo-SQL جعل نکن؛ گزینهٔ serialization/allocator را با load و failure mode بررسی و ADR را pending نگه دار. اگر migration lock خطرناک است، deploy را متوقف و DBA/operations owner را وارد کن. اگر rollback فقط code را برمیگرداند ولی schema را نه، آن را صریح و compatible schema را release شرط اعلام کن. اگر log حاوی PII است، field را redact/design را اصلاح کن، نه اینکه privacy risk را به test بعدی موکول کنی.
## خطاهای رایج و نشانهها
- **check-then-act:** availability read جدا از write است و concurrent proof ندارد.
- **idempotency ظاهری:** key ذخیره میشود ولی fingerprint یا response replay ندارد.
- **transaction بیرونی:** network call داخل transaction lock و retry را غیرقابلپیشبینی میکند.
- **rollback نمایشی:** `down` migration هست اما write جدید/feature flag/repair path نیست.
- **ADR بیتصمیم:** context زیاد است ولی گزینه، دلیل یا consequence ندارد.
## چکلیست Verification
- [ ] هر Rule به contract/model/mechanism/test trace شده است.
- [ ] tenant/actor از auth context میآیند و error policy معلوم است.
- [ ] concurrency invariant به mechanism اتمی و integration test وصل است.
- [ ] key scope، fingerprint، replay و in-progress behavior معلوماند.
- [ ] migration، compatibility، rollback limit و point-of-no-return مشخصاند.
- [ ] telemetry نتیجه و latency را بدون secret/PII کافی ثبت میکند.
## تمرین مستقل
برای «لغو idempotent با refund احتمالی»، API contract، transition، migration اثرپذیر و یک ADR لازم/غیرلازم بنویس.
**Rubric پاسخ:** پاسخ owner/tenant، canceled state و replay پاسخ نخست را تعیین میکند؛ refund را policy `DECIDE` نگه میدارد؛ key را با actor/tenant/operation و fingerprint scope میکند؛ cancellation event را دوبار نمیسازد؛ rollback را از schema restore جدا میکند؛ و ADR را فقط برای trade-off پایدار مانند outbox یا deduplication store با گزینه/پیامد مینویسد.
## جمعبندی
Technical Design behavior را به مرزهای atomic، contract و data قابل اجرا پیوند میدهد. concurrency با read خوشبینانه حل نمیشود؛ idempotency با key تنها کامل نیست؛ migration بدون rollback واقعی امن نیست. ADR تصمیم معماری و پیامد آن را کوتاه نگه میدارد. درس آخر این design را به ExecPlan و task graph با evidence تبدیل میکند.
## منابع مستقیم
- [Michael Nygard — Documenting Architecture Decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) — منشأ الگوی ADR؛ قالب ثابت جای تحلیل trade-off نیست.
- [GitHub Spec Kit](https://github.com/github/spec-kit/) — نمونهٔ artifactمحور plan/design؛ انتخاب datastore را تعیین نمیکند.
- [NIST Secure Software Development Framework](https://csrc.nist.gov/pubs/sp/800/218/final) — چارچوب کنترل توسعهٔ امن و traceability؛ implementation آماده ارائه نمیکند.
- [OpenAI Codex — ExecPlans](https://developers.openai.com/cookbook/articles/codex_exec_plans) — مرجع رسمی برای plan اجرایی؛ نه طراحی concurrency اختصاصی.
به زبان ساده
Technical Design توضیح میدهد یک invariant رفتاری چگونه با قرارداد، transaction، data model و failure mode محقق میشود. Migration و rollback هم بخشی از طراحیاند، چون revert کد لزوماً دادهٔ نوشتهشده را برنمیگرداند.
مثال واقعی
در طراحی فرضی `booking-platform`، قید PostgreSQL `EXCLUDE` میتواند همپوشانی intervalهای confirmed را در مرز داده مهار کند؛ این گزینه باید با ADR و تست رقابتی همان پروژه تأیید شود، نه با حدس Agent.