وقتی یک Epic بزرگ است، پاسخِ «آن را به UI، API و database تقسیم کنیم» فقط صفِ کارِ لایهها را میسازد؛ هنوز کاربر هیچ نتیجهای ندارد و ریسک اتصال تا انتها پنهان میماند. برشِ کارِ عمودی، هر بار یک رفتار کوچک، قابل مشاهده و قابلارزیابی را تحویل میدهد. این کار با Vertical Slice Architecture همنام است، اما یکی روشِ برنامهریزی تحویل و دیگری یک سبکِ سازماندهی کد است.
## اهداف یادگیری
پس از این درس میتوانی یک قابلیت را بر اساس مسیر موفق یا ناموفق، Rule، تنوع داده، Role، operation و Risk خرد کنی؛ هر برش را به behavior و test قابل مشاهده وصل کنی؛ وابستگیهای واقعی را تشخیص دهی؛ و فرقِ تقسیم کار با معماری Jimmy Bogard را بهدقت توضیح دهی.
## جای درس در چرخهٔ سرتاسری
درس ۱۶ نشان داد S1 باید نخستین رفتار موفق را از UI تا API و database عبور دهد و از runner موجود برای proof استفاده کند؛ S0 فقط contract، flag و route است. اکنون میپرسیم پس از آن، کدام رفتار را اول میسازیم. Spec v1.0 و ADR-021 فصل ۴ همچنان مرجعاند: مالکیت از authentication میآید، route جدید با `RESCHEDULE_V1_ENABLED` پیشفرض خاموش است، خطای tenant دیگر نباید افشا شود، و conflict همزمان باید در مرز داده یک success داشته باشد. Slice ترتیب این قراردادها را تعیین میکند، نه معنای آنها را.
## شش محور برش، با یک سؤال روشن
پیش از بریدن، Epic را به یک جملهٔ actor–trigger–outcome برگردانید: «بیمارِ مالک، وقتِ confirmed خود را به بازهٔ آزاد منتقل میکند و نتیجهٔ قطعی میبیند.» سپس هر محور سؤال متفاوتی میپرسد:
| محور | سؤال | مثال reschedule |
|---|---|---|
| Workflow | نخستین مسیر ارزشمند چیست؟ | ارسال موفقِ بازهٔ آزاد |
| unhappy path | کدام شکست، تصمیم کاربر را عوض میکند؟ | deadline بسته بدون mutation |
| Rule | کدام قانون مستقل قابلآزمون است؟ | interval نامعتبر یا خارج از ساعت کلینیک |
| Data variation | کدام داده معنای رفتار را تغییر میدهد؟ | زمان DST در America/New_York |
| Role | کدام actor مجوز/نتیجهٔ متفاوت دارد؟ | owner در برابر actor tenant دیگر |
| operation / risk | کدام mutation یا عدمقطعیت پرخطرتر است؟ | دو درخواست همزمان برای یک slot |
اینها فهرست مرحلههای اجباری نیستند. تیم از هر محوری استفاده میکند که uncertainty را کم و بازخورد را زود کند. «اول happy path» غالباً مفید است، ولی اگر Rule خطرناک یا dependency خارجی شکستپذیر است، Slice ریسکمحور میتواند پیشتر باشد. INVEST از Bill Wake نیز یادآوری میکند که Independent به معنای بینیاز از هر dependency نیست؛ یعنی dependency باید آشکار، کوچک و قابل مذاکره باشد.
## از Epic به backlog رفتاری
فرض کنید Epic میگوید: «کاربر بتواند وقت را دوباره تنظیم کند، reminder بگیرد و calendar را همیشه درست ببیند.» این عبارت سه outcome و چند non-goal نامعلوم را مخلوط کرده است. ابتدا reminder/provider، refund، doctor change و recurring appointment را از Scope v1.0 جدا کنید. سپس یک backlog از Sliceهای قابل review بسازید:
S0 contract+flag+route → default 404، بدون parse/mutation؛ evidence P2
S1 success+auth → owner authenticated + interval آزاد، 200 { id, startAt, endAt, version }، audit یکتا
S2 eligibility+concurrency → A-04 تا A-08، no mutation یا یک 200/یک 409
S3 idempotency+replay → `400 INVALID_IDEMPOTENCY_KEY`، `409 REQUEST_IN_PROGRESS` و replay بدون audit دوم
S4 privacy+isolation → same-tenant non-owner/cross-tenant، 403 یا 404، بدون افشای PII
S5 observability+CI+performance → evidence observable و release-matched
S6 rollout+rollback → cohort یا retirement فقط پس از evidence؛ rollback غیرمخربهر خط یک behavior است، نه یک file. S0 فقط contract، flag و route را مالک است؛ S1 success/auth UI→API→DB را با runner موجود proof میکند. S2 مالک protected-write migration/constraint برای eligibility و race، S3 مالک storage idempotency/replay و S5 مالک CI matrix، artifact و performance است. کوچکبودن Slice با outcome، test، dependency و rollback مستقل سنجیده میشود.
## happy path کافی نیست، اما نقطهٔ شروع است
مسیر موفق authentication، feature flag، HTTP و storage durable را میپیماید و runner موجود آن را proof میکند. سپس برش ناموفق را بر پایهٔ اثر رفتار انتخاب کنید. S2، نه S1، deadline بسته را میسازد: API error code پذیرفته میدهد، version/audit تغییر نمیکند و UI پیام قابلفهم نشان میدهد. S5 matrix، artifact و performance را در CI تثبیت میکند.
نباید همهٔ خطاها را در «S2 error handling» انبار کرد. `INVALID_INTERVAL`، `RESCHEDULE_WINDOW_CLOSED` و `SLOT_CONFLICT` علت، status و recovery متفاوت دارند. اولین مورد دادهٔ ورودی را اصلاح میخواهد؛ دوم پنجرهٔ پذیرفتهشده را بسته میداند؛ سوم نیاز دارد کاربر زمان تازهای انتخاب کند. اگر یک برش نتواند این تفاوت را توضیح دهد، آن را دوباره برش دهید.
## Rule و data variation را پنهان نکنید
یک Rule مستقل اغلب برش بهتر از یک screen است. S2 مالک eligibility است: start باید پیش از end باشد، local time با timezone کلینیک به UTC تبدیل شود و deadline بسته mutation نکند. DST نیز فقط نمونهٔ دیگری از همان string نیست: local time ممکن است مبهم یا ناموجود باشد، پس S2 آن را با `422 INVALID_LOCAL_TIME`، نمونه، response و نمایش UI میسنجد؛ `new Date()` در front end حقیقت domain نیست.
Data variation همچنین idempotency را شامل میشود. کلید دقیقاً در scope `tenantId + actorId + operation` معنا دارد؛ `appointmentId`، `startLocal` و `endLocal` در fingerprint intent هستند، نه scope. S3 key نامعتبر را با `400 INVALID_IDEMPOTENCY_KEY` و claim همزمان را با `409 REQUEST_IN_PROGRESS` پاسخ میدهد؛ key برابر با fingerprint متفاوت نیز `409 IDEMPOTENCY_KEY_REUSED` است، نه replay. آن را در S3 جدا نگه دارید تا reviewer دوام response، hash امن telemetry و نبود audit دوم را بررسی کند.
## Role و operation: مجوز را happy path ندانید
«برای admin هم کار میکند» برش Role نیست مگر actor، policy و outcome روشن باشد. Scope این release بیمارِ مالک را محدود میکند. S1 authorization پایه را بررسی میکند: actor همان patient است و `tenantId`/`actorId` فقط از credential گرفته میشوند؛ body نباید آنها را تزریق کند. S4 isolation را کامل میکند: actor دیگر در همان tenant باید ۴۰۳ `APPOINTMENT_NOT_OWNED` ببیند؛ actor tenant دیگر باید ۴۰۴ `APPOINTMENT_NOT_FOUND` ببیند، بدون نام یا زمان بیمار. این تمایز privacy contract فصل ۴ است و با feature flag عوض نمیشود.
برش operation نیز تفاوت read با mutation را جدی میگیرد. نمایش وقت فعلی میتواند Slice read-only جدا باشد؛ reschedule write با transaction، audit و rollback است. اگر read و write را یکی کنید، failure write ممکن است قابلیت مشاهده را هم deploy نکند. برعکس، جداکردن repository، DTO و button به سه issue، operation را برش نداده است؛ صرفاً لایهها را batch کرده است.
## Risk-first و dependency-first، نه ترتیب نمایشی
Backlog را به ترتیب رنگ UI نچینید. برای هر Slice چهار سؤال بپرسید: آیا نتیجه برای کاربر قابل دیدن است؟ کدام accepted contract را مصرف میکند؟ چه dependency آن را block میکند؟ rollback چه چیزی را واقعاً برمیگرداند؟ S0 برای wiring بنیادی اول و S1 برای success/auth است. S2 پس از S1، invariant eligibility/concurrency را پیش از exposure گسترده ثابت میکند؛ S3 روی success پایدار replay durable را میافزاید؛ S4 isolation را میبندد؛ S5 زمانی میآید که benchmark میتواند version ۴۱→۴۲ را با fixture کنترلشده مشاهده کند؛ و S6 فقط evidence را rollout یا retirement میکند.
dependency-first به معنای ساختن «epic database» نیست. migration additive protected-write در S2 و storage idempotency در S3، همراه consumer، test و rollback limit همان Slice قرار میگیرد. چنانچه schema جدید هنوز خوانده نمیشود، migration جدا ownership را مبهم میکند؛ در درس ۱۸ expand/migrate/contract را بدون جابهجایی این مسئولیت دقیقتر میکنیم.
## Vertical work slicing با Vertical Slice Architecture یکی نیست
Vertical work slicing راه تصمیمگیری backlog است: چه behavior کوچک و deployable را اکنون میسازیم؟ میتواند در monolith لایهای، serviceهای جدا یا حتی یک legacy codebase اجرا شود. خروجی آن Slice، order، dependency، evidence و rollback است.
Vertical Slice Architecture، مطابق نوشتهٔ Jimmy Bogard، یک سبکِ سازماندهی code حول feature/use case است؛ مثلاً route، handler، validator و data access `reschedule` کنار هم باشند، نه اینکه همهٔ controllerها در یک پوشه و همهٔ repositoryها در پوشهای دیگر جمع شوند. این سبک میتواند review هر Slice را آسان کند، ولی تضمین نمیکند behavior deployable، flag، migration، CI یا test race دارید. برعکس، یک تیم میتواند با ساختار لایهای، برشهای کاری کاملاً عمودی تحویل دهد.
پس در review نگویید «پوشهٔ feature داریم، پس Slice داریم». بپرسید: کاربر چه outcomeی میبیند، چه contractی حفظ میشود، test چه Oracleی دارد و rollback چه محدودیتی دارد؟ معماری پاسخِ «کد کجا زندگی میکند» است؛ slicing پاسخِ «چه چیزی چه وقت ارزش و یادگیری میسازد» است.
## مثال ضعیف و علت شکست
Issue 1: تمام componentهای reschedule و calendar را تمام کنید.
Issue 2: همهٔ endpointها، DTOها و error handlerها را بسازید.
Issue 3: همهٔ tableها، indexها و migrationها را اضافه کنید.
Issue 4: سپس یکپارچهسازی و تست را انجام دهید.این گروهبندی افقی، dependencyهای واقعی را پنهان میکند. reviewer نمیتواند بگوید کدام behavior امن شده است؛ route ممکن است flag نداشته باشد، migration امکان rollback نداشته باشد و UI با mock سبز شود. «همه» نیز Scope را به reminder و doctor change باز میکند.
## مثال اصلاحشده
S2 — eligibility و conflict همزمان برای یک interval
Behavior: دو patient مجاز به یک slot میرسند؛ دقیقاً یک mutation commit میشود.
Files: contract error؛ repository transaction/constraint؛ service map؛ fixture barrier؛ integration test؛ CI job.
Tests: A-08، 100 iteration؛ هر بار یک 200، یک 409 SLOT_CONFLICT، یک audit.
Rollback: flag خاموش و code قبلی compatible؛ schema/audit نوشتهشده حذف نمیشود.
Dependency: S1 success/auth؛ protected-write migration/constraint در همین S2.این Slice بدون screen جدید هم ارزش دارد، زیرا خطر اصلی را با Oracle صریح اثبات میکند. E2E معمولی جای barrier را نمیگیرد؛ ممکن است requestها واقعاً همزمان نشده باشند.
## آزمایشگاه: backlog را دوباره برش بزن
**ورودی:** Epic reminder/reschedule، Story Map درس ۱۶، Spec v1.0 و taskهای P1 تا P6. **گامها:** ۱) سه non-goal را جدا کن. ۲) S0 تا S6 بالا را با actor/outcome بازنویسی کن. ۳) کنار هر Slice محور اصلی و dependency بنویس. ۴) برای هر کدام fileهای لازم، test Oracle و rollback limit تعیین کن. ۵) S0 و پرریسکترین Slice بعدی را انتخاب کن. ۶) بررسی کن آیا هر مورد UI/API/DB batching است یا behavior مستقل.
**خروجی مورد انتظار:** backlog S0 تا S6 دارد: contract/flag/route، success/auth، eligibility/concurrency، idempotency/replay، privacy/isolation، observability/CI/performance و rollout/rollback؛ هر کدام behavior/files/tests/rollback دارند. هیچ Slice با نام یک لایه آغاز نمیشود و ترتیب دلیل ریسک/dependency دارد. **Recovery:** اگر یک Slice چند error با recoveryهای متفاوت دارد، آن را split کن. اگر migration بدون consumer مانده، آن را به Slice مالک آن برگردان: protected-write به S2 و idempotency storage به S3. اگر policy باز است، به جای فرض، آن را `DECIDE` با owner ثبت کن.
این فرمانها فقط artifact آموزشی برای booking-platform فرضیاند؛ فقط با database محلیِ ephemeral که نام آن `booking_platform_test` است اجرا کنید، نه staging یا production:
case "${TEST_DATABASE_URL:-}" in *booking_platform_test*) ;; *) echo "use only booking_platform_test"; exit 1;; esac
RESCHEDULE_V1_ENABLED=true pnpm exec vitest run apps/api/src/modules/appointments/reschedule.int.test.ts
RESCHEDULE_V1_ENABLED=true pnpm exec vitest run apps/api/src/modules/appointments/reschedule.int.test.ts --testNamePattern="A-08" --repeat=100## خطاهای رایج و نشانهها
- **Slice به نام فناوری:** «S3 database» هیچ actor یا outcome ندارد.
- **happy path نمایشی:** UI با mock سبز است ولی contract و durable write ندارد.
- **unhappy path انبارشده:** همهٔ statusها یک message و یک test مبهم دارند.
- **Role خیالی:** body actor را تعیین میکند یا tenant دیگر وجود resource را میفهمد.
- **Risk دیرهنگام:** race را پس از rollout عمومی کشف میکنیم.
- **معماریزدگی:** نام پوشه را evidence تحویل میخوانیم.
## چکلیست Verification
- [ ] هر Slice actor، trigger، observable outcome و non-goal دارد.
- [ ] S0 contract/flag/route و evidence P2 دارد؛ S1 UI→API→database→CI واقعی و flag صریح روشن دارد.
- [ ] status/error، tenant privacy و idempotency با Spec v1.0 هممعنا هستند.
- [ ] S2، A-04 تا A-08 را با eligibility و barrier/concurrency میسنجد؛ S3، A-10/A-11 replay را میسنجد.
- [ ] ترتیب Sliceها دلیل dependency یا کاهش ریسک دارد.
- [ ] rollback، schema/audit نوشتهشده را با revert خیالی پاک نمیکند.
## تمرین مستقل
برای «بیمار reminder آینده را میبیند و opt-out خود را ثبت میکند»، پنج Slice طراحی کن. ارسال provider و زمانبندی background خارج از Scope باشد.
**Rubric پاسخ:** پاسخ خوب read خوشمسیر، opt-out mutation، Role/tenant، data/timezone و failure/replay را جدا میکند؛ برای هر Slice behavior/test/rollback دارد؛ provider را پنهانی داخل S0 نمیآورد؛ و سازماندهی پوشه را با اولویت backlog یکی نمیگیرد.
## جمعبندی
برش عمودی، واحد تحویلِ رفتار و یادگیری است، نه واحد لایه یا directory. happy path نخستین سیمکشی را نشان میدهد؛ Rule، داده، Role، operation و Risk مسیرهای بعدی را روشن میکنند. با outcome، dependency، Oracle و rollback برای هر Slice، تیم زودتر ادغام میکند و هر review چیز قابلقبولی برای رد یا پذیرش دارد.
## منابع مستقیم
- [Bill Wake — INVEST and vertical story slicing](https://xp123.com/invest-in-good-stories-and-smart-tasks/) — راهنمای جامعهمحور برای کیفیت story و برش؛ اندازهٔ ثابت داستان تعیین نمیکند.
- [Alistair Cockburn — Elephant Carpaccio](https://alistair.cockburn.us/elephant-carpaccio/) — تمرین برشهای کوچک سرتاسری؛ جای قرارداد domain یا test کافی نیست.
- [Jimmy Bogard — Vertical Slice Architecture](https://www.jimmybogard.com/vertical-slice-architecture/) — سبک سازماندهی code حول feature؛ نسخهٔ اجباری برای همهٔ معماریها نیست.
- [Growing Object-Oriented Software, Guided by Tests](http://www.growing-object-oriented-software.com/) — Walking Skeleton و بازخورد integration زودهنگام؛ معماری معین تجویز نمیکند.
به زبان ساده
Slice را میتوان بر اساس workflow، Rule، Role، تنوع داده یا Risk برید؛ نتیجه باید یک رفتار قابل مشاهده باشد، نه صرفاً یک لایهٔ فنی. Vertical work slicing با Vertical Slice Architecture یکی نیست: اولی روش برنامهریزی تحویل است و دومی سازماندهی code.
مثال واقعی
در backlog فرضی دوره، S2 eligibility و concurrency را مالک است و S3 idempotency و replay را؛ «همهٔ migrationها» یک Slice رفتاری نیست.