یک Slice وقتی قابلادغام نیست که فقط روی branch خودش «کار کند». API ممکن است body جدید بفرستد در حالی که UI قدیمی آن را نمیفهمد؛ migration ممکن است writer جدید را فعال کند اما reader قدیمی را بشکند؛ و merge بزرگ ممکن است conflict را به پایان sprint منتقل کند. قراردادهای روشن، تغییرِ additive، feature flag و ادغام کوچک این فاصله را کوتاه میکنند.
## اهداف یادگیری
پس از این درس میتوانی مرزهای قراردادِ reschedule را تعیین کنی، تغییر schema را با expand/migrate/contract ترتیب دهی، flag را از مجوز و rollback جدا کنی، branch-by-abstraction را در یک وابستگی قابلتعویض بهکار ببری و evidence ادغام زودهنگام را تعریف کنی.
## جای درس در چرخهٔ سرتاسری
درس ۱۷ نقشهٔ ثابت S0 contract+flag+route؛ S1 success+auth؛ S2 eligibility+concurrency؛ S3 idempotency+replay؛ S4 privacy+isolation؛ S5 observability+CI+performance؛ و S6 rollout+rollback را ساخت. این درس تضمین میکند این Sliceها هنگام coexistence نسخهها یکدیگر را نشکنند. قراردادهای فصل ۴ ثابتاند: `PATCH /v1/appointments/{appointmentId}/reschedule` تنها با `RESCHEDULE_V1_ENABLED` فعال است؛ tenant و actor از credential میآیند؛ actor دیگر همان tenant ۴۰۳ میگیرد و cross-tenant ۴۰۴؛ S2 `409 SLOT_CONFLICT` را میسازد و S3 `400 INVALID_IDEMPOTENCY_KEY`، `409 REQUEST_IN_PROGRESS` و replay idempotent body پذیرفته را مالک است؛ migration پس از write با drop برنمیگردد.
## قرارداد فقط OpenAPI نیست
Contract توافقی قابلبررسی در یک مرز است. HTTP route، method، request fields، status، error code و semantics `version` قراردادند. schema database، constraint interval، migration ordering و retention idempotency نیز قراردادند. UI با API از طریق response و message/error mapping قرارداد دارد. CI با پروژه قرارداد دارد: command، fixture، flag و Oracle باید معلوم باشند. یک type TypeScript مشترک مفید است، اما بهتنهایی نشان نمیدهد client قدیمی ۴۰۹ تازه را چگونه میبیند یا transaction در race چه میکند.
برای S0 یک contract باریک بنویسید:
PATCH /v1/appointments/{appointmentId}/reschedule
Header: Idempotency-Key (scope دقیق: tenantId + actorId + operation)
Body: startLocal, endLocal
Authority: tenantId و actorId فقط authentication
200: { id, startAt, endAt, version }؛ startAt/endAt UTC هستند
403: APPOINTMENT_NOT_OWNED برای actor دیگر در همان tenant
404: APPOINTMENT_NOT_FOUND برای id بیرون از tenant
404: route پنهان وقتی RESCHEDULE_V1_ENABLED=false`appointmentId`، `startLocal` و `endLocal` جزو fingerprint هستند، نه scope کلید. چیزهایی که هنوز S0 پشتیبانی نمیکند هم بخشی از contract است: reminder، refund، doctor change و recurring appointment. «فعلاً پیاده نشده» پاسخ فنی نیست؛ باید status/error یا عدم دسترسیِ flag، و مسیر backlog معلوم باشد. همچنین response موفق باید fieldهای قدیمی موردنیاز UI را حذف نکند، مگر migration client و version policy پذیرفته شده باشد.
## تغییر additive: اول توان coexistence، بعد مصرف
در محیط واقعی deployها ممکن است overlap داشته باشند. یک API instance قدیمی هنوز درخواست میگیرد، worker دیرتر deploy میشود، rollback code رخ میدهد یا query ذخیرهشده چند دقیقه cache است. پس deploy را به فرض atomic بودن همهٔ binaryها طراحی نکنید. تغییر compatible معمولاً این ترتیب دارد:
1. **Expand:** column/table/index/route جدید را additive اضافه کن؛ reader قدیمی همچنان کار کند. 2. **Migrate:** code جدید بتواند هر دو شکل داده را بخواند و در صورت نیاز write dual یا backfill کنترلشده انجام دهد. 3. **Contract:** فقط پس از evidenceِ نبود consumer قدیمی، مسیر/column قدیمی را با migration forward و review جدا حذف کن.
نام «migrate» در اینجا به migration SQL محدود نیست؛ مرحلهٔ همزیستی داده و code است. حذف یک column nullable در همان PR که writer جدید معرفی میشود، expand/migrate/contract نیست؛ شکست پنهانی است.
برای reschedule، S2 مالک migration/constraint `EXCLUDE` روی interval confirmed برای protected write است؛ S3 مالک migration/table `reschedule_idempotency`، fingerprint، validation کلید با `400 INVALID_IDEMPOTENCY_KEY`، claim درحالاجرا با `409 REQUEST_IN_PROGRESS` و replay durable است. endpoint قدیمی باید schema قبلی را بخواند. S1 رفتار موفق/auth را پشت route/flag S0 اجرا میکند و از runner موجود برای proof استفاده میکند؛ S5 matrix، consumer testهای coexistence و artifactهای CI را ثبت میکند. اگر backfill یا consumer قدیمی وجود دارد، S6 فقط پس از evidence سازگار جلو میرود. rollback در این مرحله یعنی flag off و deploy code سازگار قبلی؛ audit، index و recordِ already-written را drop نمیکنیم. اصلاح unsafe، migration رو به جلو و reviewشده است.
## یک جدول سازگاری، پیش از merge
| نسخه | schema جدید را میخواند | schema جدید مینویسد | route جدید را میخواند | حالت امن |
|---|---|---|---|---|
| code قدیمی | باید تحمل کند | خیر | خیر | پیش و پس از expand |
| code S0 | باید تحمل کند | خیر | فقط guard flag | contract/flag/route |
| code S1 | schema additive را تحمل میکند | فقط appointment/audit موجود | بله با flag | success/auth محدود |
| code rollback | باید تحمل کند | خیر | خیر | پس از flag off |
| code contract | پس از اثبات retire شدن قدیمی | ممکن است | بله | فقط migration جدا |
این جدول ادعا نمیکند هر سیستم schema-less است. اگر consumer قدیمی نمیتواند field جدید را تحمل کند، همان ناسازگاری risk است و باید قبل از migration تصمیم، version یا adapter داشته باشد. Compatibility را با «همه testهای branch من سبزند» اثبات نکنید؛ matrix نسخه و testهای consumer لازم است.
## feature flag: کنترل exposure، نه پنهانکنندهٔ نقص
`RESCHEDULE_V1_ENABLED=false` باید default ایمن باشد. route هنگام خاموشی ۴۰۴ میدهد، نه اینکه body را بگیرد و بعد از write خطا دهد. staging و testها flag را صریح true میکنند. flag configuration باید observable باشد: نام، owner، environments مجاز، cohort، expiry/review date و rollback runbook دارد. log نباید Idempotency-Key خام یا patient fixture را برای اثبات flag چاپ کند.
Flag permission نیست. با flag روشن، actor غیرمالک در همان tenant همچنان ۴۰۳ `APPOINTMENT_NOT_OWNED` میگیرد و actor cross-tenant همچنان ۴۰۴ privacy-preserving؛ authorization از authentication و policy میآید. Flag substitute test هم نیست: اگر S2 concurrency در flag-on staging pass نشده، rollout محدود فقط exposure را کم میکند، correctness را ثابت نمیکند. Feature-toggle guidance نیز راهنمای عملی است، نه توجیهی برای انباشت toggleهای بیمالک.
یک runbook کوچک:
Enable: deployment compatible + migration verified + flag true فقط در staging/cohort پذیرفته.
Observe: outcome 200/409/422، latency، correlationId hash-safe و CI artifact.
Disable: flag false، config reload، رفتار route جدید 404؛ audit/idempotency data باقی میماند.
Escalate: mutation ناخواسته، leakage tenant، duplicate audit یا contract drift → rollout متوقف و owner Spec/ADR.
Retire: پس از evidence و تاریخ review، flag و dead path در Slice contract جدا حذف میشود.به این ترتیب rollback عملیاتی است، نه `git revert` خیالی. اگر schema بعد از write حذف شود، code قدیمی شاید درست بالا بیاید اما recordها و audit از بین میروند یا invariant آسیب میبیند.
## branch-by-abstraction برای تغییر dependency
گاهی Slice نیاز دارد implementation یک dependency را عوض کند: مثلاً client قدیمی availability به repository جدیدِ transaction-safe. Branch-by-abstraction یک interface باریک میسازد تا caller به abstraction ثابت وصل باشد، سپس implementation جدید کنار قدیمی قرار میگیرد، flag یا wiring انتخاب میکند، و پس از evidence قدیمی حذف میشود.
RescheduleStore.reserve(intent) -> Reserved | SlotConflict
├─ LegacyAvailabilityStore (فقط read/check قدیمی)
└─ TransactionalRescheduleStore (lock + constraint + audit)
IdempotencyStore.claim(key, fingerprint) -> Fresh | Replay(status, body) | INVALID_IDEMPOTENCY_KEY | REQUEST_IN_PROGRESSاین abstraction مجوز نمیدهد semantics را مبهم کنیم. هر دو implementation باید error contract مشترک را تولید کنند یا gap صریحاً BLOCKED باشد. interface خیلی بزرگ که تمام repository را کپی میکند، migration را سختتر میکند. interface برای S2 فقط reserve outcomeهای eligibility/concurrency را نشان میدهد؛ S3 store جداگانهٔ idempotency/replay را مالک است و canonical outcomeهای `INVALID_IDEMPOTENCY_KEY` و `REQUEST_IN_PROGRESS` را از همان boundary برمیگرداند. هیچ provider reminder یا refund متدی واردش نکنید.
## CI یعنی ادغامِ تکرارشونده با Oracle
Martin Fowler در Continuous Integration بر ادغام کوچک و بازخورد خودکار تأکید دارد. در این فصل معنی عملی آن یک merge روزانه یا قانون repository خاص نیست؛ یعنی branch طولانی و «integration week» نباید تنها محل کشف قرارداد باشد. S1 از runner موجود برای proof مسیر موفق/auth استفاده میکند؛ S5 مالک build/typecheck، matrix سازگاری، contract/integration/E2E، artifact و benchmark در CI است. S0 فقط semantics contract/flag/route را تحویل میدهد و S5 هر دو حالت flag را در matrix اجرا میکند. migration تنها روی DB ephemeral یا staging مجاز انجام میشود.
RESCHEDULE_V1_ENABLED=true pnpm exec vitest run apps/api/src/modules/appointments/reschedule.int.test.ts
RESCHEDULE_V1_ENABLED=true pnpm exec playwright test apps/web/tests/e2e/reschedule.spec.ts
git diff --checkاین فرمانها نمونهٔ artifact آموزشیاند، نه ادعای اجرای آنها در مخزن دوره. Oracleها دقیقاند: A-01 authorization، A-08 یک ۲۰۰/یک ۴۰۹ با barrier، A-09 success با `version` ۴۱→۴۲، و S3 با `400 INVALID_IDEMPOTENCY_KEY`، `409 REQUEST_IN_PROGRESS`، A-10 replay با یک audit و A-11 fingerprint متفاوت. evidence P2 برای flag-default ۴۰۴ است. CI که فقط lint دارد، contract deployment را نمیسنجد. E2E که API را mock میکند، proof Skeleton نیست.
## مثال ضعیف و علت شکست
PR 1: column قدیمی را rename و endpoint جدید را منتشر کن.
PR 2: UI را هفتهٔ بعد تغییر بده.
PR 3: اگر مشکل شد، git revert کن.PR 1 code قدیمی و جدید را ناسازگار میکند؛ UI فرصت ندارد response را بفهمد؛ rollback schema را برنمیگرداند؛ و flag یا test matrix ندارد. این طرح همچنین نمیگوید `tenantId` از کجا میآید یا client هنگام 409 چه میبیند.
## مثال اصلاحشده
S0 contract+flag+route: default 404 و endpoint قدیمی compatible؛ evidence P2.
S1 success+auth: behavior موفق UI/API/DB/CI و A-01/A-09 با { id, startAt, endAt, version }.
S2 eligibility+concurrency: `TransactionalRescheduleStore` و protected-write migration/constraint؛ A-04 تا A-08.
S3 idempotency+replay: migration/table/store، scope tenantId+actorId+operation، fingerprint appointmentId/startLocal/endLocal، `400 INVALID_IDEMPOTENCY_KEY`، `409 REQUEST_IN_PROGRESS` و A-10/A-11.
S4 privacy+isolation: same-tenant 403 و cross-tenant 404، با A-02/A-03/A-12.
S5 observability+CI+performance: matrix سازگاری، consumer test و benchmark release-matched.
S6 rollout+rollback: پس از consumer evidence، cohort/adapter/flag را rollback یا با migration forward retire میکند.هر مرحله change کوچک و قابل review است. S6 «تمیزکاری آزاد» نیست؛ evidence میخواهد که rollback به code قدیمی دیگر نیاز عملیاتی ندارد.
## آزمایشگاه: matrix ادغام بساز
**ورودی:** S0 تا S5 درس ۱۷، ADR-021 و feature flag فصل ۴. **گامها:** ۱) پنج مرز contract را نام ببر. ۲) یک migration additive برای idempotency/audit بنویس. ۳) جدول چهار نسخهٔ بالا را برای reader/writerهای خود کامل کن. ۴) flag owner، default، cohort، expiry و disable procedure تعیین کن. ۵) برای dependency availability interface کوچک و outcomeها را بنویس. ۶) سه command CI و Oracleهایشان را فقط برای محیط امن فهرست کن.
**خروجی مورد انتظار:** هیچ deployی به atomic deploy همهٔ consumerها متکی نیست؛ flag به authorization تبدیل نشده؛ schema writeشده با revert حذف نمیشود؛ و هر compatibility claim یک test یا observation دارد. **Recovery:** اگر reader قدیمی field تازه را reject میکند، contract/adapter را پیش از deploy اصلاح کن. اگر rollout flag-on error ناشناخته ساخت، flag off، artifact evidence و escalation به owner انجام بده؛ response contract را در incident بیصدا تغییر نده.
## خطاهای رایج و نشانهها
- **breaking rename:** حذف/rename schema پیش از coexistence.
- **flag مرده:** owner یا تاریخ حذف ندارد و flag-on test ندارد.
- **flag بهجای auth:** security policy با cohort rollout مخلوط میشود.
- **interface چاق:** abstraction از scope Slice بزرگتر است.
- **CI تزئینی:** test به mock یا DB ناشناخته میخورد.
- **rollback destructive:** برای بازگشت، audit یا دادهٔ مشتری را پاک میکند.
## چکلیست Verification
- [ ] contract HTTP، schema، UI mapping و CI Oracle دارد.
- [ ] expand پیش از writer جدید و contract پس از evidence consumer رخ میدهد.
- [ ] code rollback، schema additive و response قبلی را تحمل میکند.
- [ ] `RESCHEDULE_V1_ENABLED` default false، ownerدار و قابل مشاهده است.
- [ ] authorization و privacy cross-tenant مستقل از flag حفظ میشود.
- [ ] race/idempotency با integration test، نه امید به deploy، سنجیده میشود.
## تمرین مستقل
برای افزودن channel جدید reminder، برنامهٔ expand/migrate/contract و flag بنویس؛ client قدیمی فقط email میفهمد و provider خارجی هنوز در Sandbox است.
**Rubric پاسخ:** پاسخ خوب contract channel را version/adapter میکند، provider را فعال نمیکند، default امن و owner/expiry flag دارد، UI قدیمی را در matrix میبیند، test Sandbox و rollback غیرمخرب مینویسد، و حذف email قدیمی را به evidence مصرفکننده موکول میکند.
## جمعبندی
تحویل تدریجی با تغییر additive و قراردادهای قابلراستیآزمایی ممکن میشود. Expand/migrate/contract امکان coexistence میدهد؛ flag exposure را کنترل میکند اما authorization یا correctness نیست؛ abstraction تغییر dependency را محدود میکند؛ و CI زود contract واقعی را در برابر mock و branch طولانی قرار میدهد.
## منابع مستقیم
- [Martin Fowler — Continuous Integration](https://martinfowler.com/articles/continuousIntegration.html) — راهنمای جامعهمحور برای ادغام کوچک و feedback؛ pipeline یا cadence واحد تعیین نمیکند.
- [Martin Fowler — Branching Patterns](https://martinfowler.com/articles/branching-patterns.html) — تحلیل trade-offهای branching؛ جای policy محلی repository نیست.
- [Martin Fowler — Feature Toggles](https://martinfowler.com/articles/feature-toggles.html) — راهنمای عملی برای دستهها و بدهی flag؛ مدل authorization ارائه نمیکند.
- [GitHub Docs — GitHub flow](https://docs.github.com/en/get-started/using-github/github-flow) — مستند رسمی گردش branch/PR در GitHub؛ تضمین سازگاری schema یا quality gate نیست.
به زبان ساده
تغییر additive به نسخههای قدیمی فرصت همزیستی میدهد: ابتدا expand، سپس migrate و در پایان contract. Feature flag exposure را کنترل میکند، اما جای authorization، تست یا rollback schema نیست.
مثال واقعی
در قرارداد فرضی reschedule، `RESCHEDULE_V1_ENABLED=false` باید route جدید را ۴۰۴ کند و route قدیمی را سالم نگه دارد؛ دادهٔ audit و idempotency پس از flag-off نباید با حذف مخرب پاک شود.