امنیت، هویت، استقرار و تابآوری · فصل ۶ — Chaos، Feature Flags، Rollout و ADR · 107
Module Data Ownership و Cross-module Boundaries
میتواند سرویس را اجرا کند اما lifecycle، health، rollout، observability و recovery را بهصورت سیستماتیک مدیریت نمیکند. موضوع جاری: «Module Data Ownership و Cross-module Boundaries». میتواند برای «Module Data Ownership و Cross-module Boundaries» run/release model و signals/recovery checks لازم را برای یک سرویس محدود طراحی کند.
خلاصهٔ درس
میتواند سرویس را اجرا کند اما lifecycle، health، rollout، observability و recovery را بهصورت سیستماتیک مدیریت نمیکند. موضوع جاری: «Module Data Ownership و Cross-module Boundaries».
میتواند برای «Module Data Ownership و Cross-module Boundaries» run/release model و signals/recovery checks لازم را برای یک سرویس محدود طراحی کند.
خروجی این درس
میتواند برای «Module Data Ownership و Cross-module Boundaries» run/release model و signals/recovery checks لازم را برای یک سرویس محدود طراحی کند.
1. مسئله: «دسترسی دارم» با «مالک هستم» فرق دارد
در یک سامانه ماژولار، اینکه ماژول Ordering بتواند جدول Catalog را بخواند یا بنویسد به این معنا نیست که حق مالکیت روی آن داده دارد. مالکیت یعنی یک ماژول مرجع تصمیم برای داده و invariantهای مربوط به آن است. اگر دو ماژول بتوانند بدون قرارداد روشن یک رکورد را تغییر دهند، در لحظهای که قواعد کسبوکار متفاوت شوند معلوم نیست کدام تغییر معتبر است و چه کسی باید ناسازگاری را ترمیم کند.
هدف این درس ساخت Module Ownership Boundary Contract v1 است: برای هر ماژول مشخص میکنیم چه دادهای را مالک است، چه invariantهایی را حفظ میکند، چه مسیرهای read/command/event مجازند، کدام direct write ممنوع است و مصرفکننده کجا باید داده را به مدل محلی خودش ترجمه کند.
2. مرز منطقی را از مرز فیزیکی جدا کن
یک bounded context یا module الزاماً یک microservice جدا نیست. ممکن است چند ماژول در یک process یا حتی یک database باشند و با این حال مالکیت منطقی روشنی داشته باشند. برعکس، دو database جدا هم تضمین نمیکنند coupling درست شده باشد؛ اگر ماژول مصرفکننده credential یا connection path برای نوشتن مستقیم در store مالک داشته باشد، boundary هنوز شکسته است.
در نتیجه اولین پرسش «چند deployment داریم؟» نیست. پرسش دقیقتر این است: چه کسی حق تغییر state مرجع و اجرای invariant را دارد؟
3. Domain model مشترک همیشه مزیت نیست
وقتی یک کلاس Customer را در Identity، Ordering و Billing بدون ترجمه استفاده میکنیم، ممکن است سه معنای متفاوت را زیر یک اسم پنهان کنیم. Identity به credential و profile اهمیت میدهد، Ordering به shipping/contact snapshot و Billing به payer/account rules. یک شناسه میتواند مشترک باشد، اما مدل دامنه و رفتار لازم نیست مشترک باشند.
قانون عملی: shared identity را از shared domain meaning جدا ثبت کن. اگر دو context روی یک شناسه توافق دارند ولی attribute و invariant متفاوت دارند، هر context مدل خودش را نگه میدارد و ترجمه در boundary انجام میشود.
4. مالکیت را با invariant تعریف کن
فهرست جدولها بهتنهایی ownership contract نیست. برای هر داده باید invariantهای محافظتشده را هم بنویسی. مثال: Catalog مالک ProductPrice است و invariant آن میتواند «قیمت فعال منفی نیست» باشد. Ordering ممکن است snapshot قیمت را در OrderLine نگه دارد، اما حق ندارد قیمت مرجع Catalog را تغییر دهد.
این تفکیک کمک میکند بفهمیم duplicate data همیشه خطا نیست. یک consumer projection یا snapshot میتواند عمداً تکرار شود، چون معنا و چرخهعمر آن در context مصرفکننده متفاوت است.
5. Owner Write API: تنها مسیر تغییر مرجع
هر state مرجع باید یک مسیر نوشتنِ تحت کنترل owner داشته باشد. این مسیر میتواند function داخلی ماژول، application service، command handler یا API باشد؛ مهم نام تکنولوژی نیست، مهم این است که invariant در نقطهای اجرا شود که owner کنترل میکند.
اگر Ordering برای رزرو موجودی لازم است Catalog را تغییر دهد، مسیر درست «نوشتن مستقیم در جدول Catalog» نیست. Ordering یک command معنادار مثل ReserveInventory میفرستد و Catalog تصمیم میگیرد تغییر معتبر است یا نه.
6. Cross-module Read سه شکل رایج دارد
خواندن بین ماژولها میتواند با request همزمان، published read model یا projection محلی انجام شود. انتخاب به latency، consistency، availability و ownership بستگی دارد. هیچکدام بهطور جهانی بهترین نیست.
برای دادهای که باید در لحظه تازه باشد، request همزمان ممکن است مناسب باشد. برای queryهای پرتکرار یا استقلال بیشتر، projection محلی میتواند بهتر باشد. در هر دو حالت consumer نباید structure داخلی owner را قرارداد عمومی فرض کند.
7. Published Language و Anti-corruption Layer
قرارداد مرزی باید واژگان منتشرشدهی owner را از مدل داخلی consumer جدا کند. این همان جایی است که anti-corruption layer یا translation adapter ارزش پیدا میکند: پیام یا DTO بیرونی را میگیرد و آن را به مدل مصرفکننده تبدیل میکند.
این ترجمه از انتشار coupling جلوگیری میکند. اگر Catalog فیلد داخلی خود را rename کند ولی published contract ثابت بماند، Ordering نباید مجبور به تغییر شود.
8. Event یک گزینه است، نه حکم
Integration event برای اعلام یک واقعیت رخداده مناسب است؛ مثلاً ProductPriceChanged. consumer میتواند از آن projection محلی خودش را بهروز کند. اما event برای هر تعامل لازم نیست. اگر عملیات به پاسخ فوری owner نیاز دارد، command/request همزمان ممکن است شفافتر باشد.
قرارداد باید مشخص کند کدام boundary sync و کدام async است و trade-off آن چیست. صرفاً «event-driven» نوشتن بدون نیاز واقعی، ownership را اثبات نمیکند.
9. Failure probe: جدول مشترک با دو نویسنده
فرض کن Catalog و Ordering هر دو ستون available_stock را در یک جدول تغییر میدهند. حتی اگر transactionها سبز باشند، ownership مبهم است: کدام ماژول invariant موجودی را تعریف میکند؟ در race یا rule change چه کسی مرجع است؟
این shortcut باید شکست بخورد. یا یک owner واحد تعیین میشود و ماژول دیگر از command contract استفاده میکند، یا مدل دوباره طراحی میشود. «هر دو مالکاند» پاسخ قابل دفاعی برای یک invariant واحد نیست.
10. Failure probe: دیتابیس جدا ولی foreign write
فاصلهی فیزیکی میتواند کمک کند اما اثبات نیست. اگر Ordering connection string مستقیم به database Catalog دارد و UPDATE اجرا میکند، ما فقط coupling را پشت network پنهان کردهایم. schema ownership با topology یکی نیست.
شاهد قابل قبول باید نشان دهد foreign write حذف شده و تغییر از owner write path عبور میکند. اگر فقط diagram جدید داریم و مسیر write واقعی باقی مانده، boundary هنوز باز است.
11. Consumer Projection و دادهی تکراری
گاهی consumer برای استقلال read به projection محلی نیاز دارد. در این حالت owner منبع حقیقت domain fact است، اما consumer مالک projection و lifecycle محلی خودش است. consumer نباید projection را به owner برگرداند یا آن را مسیر write مرجع کند.
در Contract برای هر projection سه چیز را ثبت کن: منبع update، freshness/consistency expectation و translation rule. این کار جلوی تبدیل cache/read model به «منبع حقیقت دوم» را میگیرد.
12. Evidence table را ردیفی بساز
برای هر boundary یک ردیف ثبت کن: Module، Owned Data، Owned Invariant، Shared Identity، Allowed Read، Allowed Command، Event Contract، Forbidden Direct Write، Translation Rule، Evidence و Open Boundary Risk. نتیجه باید از شواهد محلی یا authority مشخص پشتیبانی شود، نه از احساس معماری.
اگر نمیدانی یک write path واقعاً حذف شده است، وضعیت را OPEN نگه دار. سبز بودن build یا test عمومی بهتنهایی اثبات ownership نیست.
13. Repair متناسب: کمترین تغییر معتبر
Repair خوب الزاماً microservice split نیست. گاهی کافی است direct repository reference را با application command جایگزین کنیم. گاهی باید shared entity را به دو context-specific model جدا کنیم. گاهی هم projection محلی از یک published event ساخته میشود.
معیار repair این است که مالک state و invariant واضح شود و consumer فقط از contract مجاز عبور کند؛ نه اینکه معماری پرهزینهتر یا پیچیدهتر به نظر برسد.
14. Lab: قرارداد مالکیت را از fixture استخراج کن
در OPS014-LAB1 هشت fixture مصنوعی وجود دارد. ابتدا owner و invariant را از هر case استخراج کن، سپس sync/async boundary، projection، translation و forbidden write را تعیین کن. starter پاسخ را از قبل پر نکرده است؛ تصمیم باید از case ساخته شود.
یک fixture عمداً فریبنده است: databaseها جدا هستند ولی foreign write وجود دارد. fixture دیگری event را با request عوض میکند و انتظار دارد trade-off را بسنجی، نه اینکه request را خودکار رد کنی. اجرای checks فقط سلامت fixture را ثابت میکند؛ artifact نهایی باید row-level evidence داشته باشد.
15. Transfer: دامنه دوم را بدون کپی نتیجه بازسازی کن
در transfer، نام ماژولها، shared identifiers و interaction style عوض میشوند. از نتیجهی case اول کپی نکن. دوباره از invariant شروع کن، owner را تعیین کن، published language را مشخص کن و consumer projection یا request/event contract را مستقل بساز.
خروجی کامل زمانی قابل دفاع است که نشان دهد shared identity به shared domain model منجر نمیشود، physical separation مالکیت را خودکار حل نمیکند، و همهی writeهای مرجع از owner-controlled path عبور میکنند. هر boundary بدون شاهد باید OPEN بماند.
ارزیابی
پاسخنامهی این درس در بستهی آموزش وجود ندارد؛ خودت جواب بده و صادقانه علامت بزن. «نیاز به مرور» درس را به صف تکرار میفرستد.
Q1 retrieval
What distinguishes shared identity from shared domain meaning?
Q2 diagnosis
Two modules write the same inventory table. What must be established before accepting the boundary?
Q3 application
A consumer needs low-latency reads of owner data. Name a boundary-preserving option.
Q4 tradeoff
When can a synchronous request be valid instead of an event?
Q5 transfer
In a new domain, databases are separate but a consumer has UPDATE access to the owner store. Is ownership proven?
ملاکهای ارزیابی (rubric)
A passing response must:
- assign one explicit owner to each authoritative invariant;
- distinguish shared identity from shared domain meaning;
- reject direct foreign writes even when databases are physically separate;
- choose synchronous request, event, or projection based on the boundary need rather than a universal pattern;
- identify translation responsibility where published language differs from the consumer model;
- keep unsupported ownership or write-path claims OPEN;
- rebuild the transfer case from the changed domain evidence.
منابع و تازگی
مطالعهٔ موارد (case studies)
تصاویر درس
فایلهای آزمایشگاه
This lab uses only deterministic synthetic module metadata. No network, production database, credential, customer record, or external service is required.
For each fixture:
- identify the authoritative aggregate/data owner and invariant owner;
- determine whether an owner-controlled write path exists;
- identify shared identity versus shared domain meaning;
- record the published language and any translation boundary;
- classify consumer projection, synchronous boundary, asynchronous boundary, and schema ownership;
- reject foreign direct writes even when stores are physically separate;
- keep unsupported conclusions OPEN.
run_checks.py verifies fixture integrity only. Implement your own reasoning in starter.py and build the contract row by row.
رسانه و اسلایدهای درس
فایلهای آموزشی (دانلود)