توضیحات

هیچ تصمیم آسانی در معماری نرم افزار وجود ندارد. در عوض، بخش‌های سخت بسیاری وجود دارد؛ مشکلات یا مسائل دشواری که بهترین شیوه ای برای انجام ندارند و شما را مجبور می‌کنند تا با انجام سبک سنگین‌های مختلف، یکی را برای موفقیت انتخاب کنید. با کمک کتاب Software Architecture: The Hard Parts (معماری نرم افزار: قسمت‌های سخت)، شما یاد خواهید گرفت که چگونه به طور انتقادی در مورد سبک سنگین‌های مربوط به معماری‌های توزیع شده فکر کنید.
پیشکسوتان معماری و مشاوران مجرب، نیل فورد، مارک ریچاردز، پرامود سادالاژ و ژامک دهقانی، درباره راهبردهای انتخاب یک معماری مناسب بحث می‌کنند. نویسندگان با سر هم کردن داستانی درباره یک گروه خیالی از متخصصان فناوری - جوخه Sysops - همه چیز را از نحوه تعیین جزئیات سرویس، مدیریت گردش کار و هماهنگ سازی، مدیریت و جداسازی قراردادها و مدیریت تراکنش‌های توزیع شده تا نحوه بهینه سازی ویژگی‌های عملیاتی، مانند مقیاس پذیری، کشش و عملکرد را مورد بررسی قرار می‌دهند.
این کتاب با تمرکز بر سوالات متداول، تکنیک‌هایی را ارائه می‌کند که به شما کمک می‌کند تا هنگام مواجهه با مسائلی که به عنوان یک معمار با آن مواجه هستید، سبک سنگین‌ها را انجام دهید و بررسی کنید.

نظر

کتابی مفید در زمینه معماری نرم‌افزار که به‌جای ارائه راه‌حل‌های کلیشه‌ای، به چالش‌های واقعی و سخت معماری می‌پردازد. نویسندگان با استفاده از مثال‌های عملی و داستانی جذاب، مفاهیم پیچیده‌ای مانند کوپلینگ، تصمیم‌گیری معماری، و مدیریت workflow در سیستم‌های توزیع‌شده را به شکلی قابل فهم توضیح می‌دهند. این کتاب برای معماران نرم‌افزار و توسعه‌دهندگانی که می‌خواهند درک عمیق‌تری از مسائل معماری داشته باشند، بسیار توصیه می‌شود.

نظر

  • امتیاز : 09/10
  • به دیگران توصیه می‌کنم : بله
  • دوباره می‌خوانم : بله
  • ایده برجسته : تصمیم‌گیری معماری همیشه شامل سبک‌سنگین‌های پیچیده است و هیچ راه‌حل واحدی وجود ندارد.
  • تاثیر در من : دانستن اینکه معماری نرم‌افزار بیشتر از یک علم، هنر تصمیم‌گیری است، دید من را نسبت به نقش معمار نرم‌افزار تغییر داد.
  • نکات مثبت : تمرکز بر چالش ها و مسائل واقعی
  • نکات منفی : بعضی بخش‌ها ممکن است برای مبتدیان کمی پیچیده باشد.

مشخصات

  • نویسنده : Mark Richards, Neal Ford, Pramod Sadalage, Zhamak Dehghani
  • انتشارات : O’Reilly Media

بخش‌هایی از کتاب

فصل اول کتاب “Software Architecture: The Hard Parts”

آنچه رخ می‌دهد وقتی “بهترین روش” وجود ندارد

چرا معماری نرم‌افزار کار سختی است؟

در این فصل، نویسندگان با یک حقیقت اساسی شروع می‌کنند: در معماری نرم‌افزار، اغلب با مسائلی روبرو هستیم که راه‌حل قطعی و جهانی برای آنها وجود ندارد. مفاهیمی مثل “best practice” فقط تا حدی (و در بعضی فضاها) کاربرد دارند. علت آن:

  • پروژه‌ها و چالش‌ها بسیار متنوع‌اند.
  • انتخاب‌ها همواره وابسته به نیازهای تجاری، محدودیت‌های فنی و شرایط سازمانی است.
  • راه‌حل‌هایی که در یک زمینه جواب می‌دهند، ممکن است در جای دیگر مشکل‌ساز شوند.

هدف فصل

هدف این فصل، تعریف درست مسئله و دادن نگرش درست به مخاطب است که قرار نیست برای هر چالش معماری، یک جواب واحد وجود داشته باشد. معماری، کار تصمیم‌گیری میان گزینه‌های مختلف و یافتن بهترین راه بر اساس شرایط است.

چرایی بخش‌های سخت معماری

Ambiguity و عدم قطعیت: خیلی وقت‌ها اطلاعات ناقص یا مبهم است؛ باید با فرضیات گام برداریم و تصمیمات را مطابق تغییرات اصلاح کنیم. – Trade-off: هر راه‌حلی مزایا و معایبی دارد. لازم است مصالحه (trade-off) را بشناسیم و مدیریت کنیم. – تعاملات انسانی: معماری فقط تکنولوژی نیست؛ شامل سازمان، ذینفعان، تیم‌ها و فرهنگ هم هست.

داده‌ها و اهمیت آنها در معماری

بخشی از فصل به نقش محوری داده می‌پردازد:

  • معماری خوب باید از همان ابتدا به سوال “داده‌ها کجا هستند؟ چه کسی صاحب آن‌هاست؟ چگونه اعمال تغییرات و پردازش اطلاعات انجام می‌شود؟” پاسخ بدهد.
  • در دوران معماری‌های توزیع‌شده (مثل میکروسرویس یا کلود)، داده‌ها تبدیل به کلیدی‌ترین چالش‌ها شده‌اند چون مالکیت داده بین سرویس‌ها تقسیم می‌شود و مسائل consistency، امنیت و کارایی اهمیت مضاعف پیدا می‌‌کند.

سند تصمیم معماری (ADR)

نویسندگان تاکید دارند که معماری یعنی “تصمیم‌گیری”. بنابراین مستندسازی تصمیمات معماری با شفافیت دلایل، الزامی است. معماری باید “قابل توضیح” و “قابل دفاع” باشد تا بتوان به تیم، ذینفعان یا حتی معماران بعدی چرایی انتخاب‌ها را منتقل کرد.

سند “Architectural Decision Record” یا مخفف آن “ADR” برای همین هدف است: ثبت موضوع تصمیم، گزینه‌های قابل بررسی، دلایل انتخاب و رد هر گزینه و اثرات تصمیم. اگر این فرآیند شفاف و مدون شود، هم یادگیری تسهیل می‌شود، هم خطاهای قبلی تکرار نمی‌شود.

تمرین: فکر کن اگر در یک پروژه واقعی، سرویس پرداخت دارید و باید تصمیم بگیرید که اعتبارسنجی کارت بانکی را در همان سرویس انجام دهید یا یک سرویس مستقل ایجاد کنید، چه گزینه‌هایی دارید؟ هر کدام چه مزایا و معایبی دارند؟ دلیل انتخاب نهایی چه خواهد بود؟

Fitness Function در معماری

ایده “Fitness Function” یا توابع سنجش کارایی/سلامت معماری، از یادگیری ماشین گرفته شده است. مشابه زمانی‌که یک معیار (function) داریم تا بفهمیم مدل یادگیری چقدر خوب کار می‌کند، در معماری هم باید معیارهایی داشته باشیم که به‌صورت اتوماتیک یا دستی، کیفیت تصمیمات و ساختار را ارزیابی کند.

مثال‌ها:

  • هیچ سرویسی نباید به بیش از سه سرویس دیگر وابسته باشد.
  • نرخ خطای ارتباطات سرویس‌ها باید زیر 0.5٪ بماند.
  • دیتابیس‌ها باید فقط توسط سرویس‌های مشخص خودش قابل دسترسی باشند.

سوال: آیا تا به حال قانونی (تست یا مانیتورینگ) برای سلامت معماری تعریف کرده‌ای؟ مثلاً در مانیتورینگ‌هایتان چه چیزی را به عنوان “نشانه معماری سالم” می‌شناسید؟

تفاوت معماری و طراحی

این بخش تمایز بین معماری (تصمیمات ساختاری سطح بالا، غیرقابل تغییر یا خیلی سخت قابل تغییر) و طراحی (جزئیات پیاده‌سازی، الگوها و ترکیب‌ها) را شرح می‌دهد.

  • معماری چیزی است که اگر اشتباه شود، هزینه اصلاح آن بالا است.
  • طراحی معمولاً جزئی‌تر است و اصلاحش کم‌هزینه‌تر.

معرفی مطالعه موردی: Sysops Squad

نویسندگان برای ملموس کردن مباحث از مثال یک پروژه تخیلی به نام “Sysops Squad” استفاده می‌کنند و تصمیمات معماری آن را طی فصل‌ها بررسی می‌کنند تا مفاهیم نه صرفاً تئوری بلکه عملی و واقعی تدریس شوند.

خلاصه فصل اول (برای تثبیت)

  • در معماری نرم‌افزار، جواب قطعی وجود ندارد؛ تصمیمات همیشه trade-off دارند.
  • داده‌ها و مالکیت آنها مرکز ثقل معماری توزیع‌شده هستند.
  • مستندسازی تصمیمات با ADR الزامی است.
  • تعریف Fitness Function برای سلامت معماری اهمیت دارد.
  • مرز واضح بین معماری و طراحی را باید بشناسیم.
  • مثال پروژه واقعی، مسیر یادگیری را عملی‌تر می‌کند.

بررسی درک

  • سئوال: معمار نرم‌افزار چه زمانی به سراغ سند ADR می‌رود و قرار است چه موضوعاتی را در آن ثبت کند؟
  • اگر دوست داری بیشتر با ADR یا Fitness Function کار کنیم یا مثالی برای هرکدام بزنم، بگو تا با هم تمرین کنیم.

توضیح عمیق و مثالی از ADR (Architectural Decision Record)

تعریف کامل‌تر

ADR ابزاری ساختاریافته برای ثبت و تحلیل تصمیمات معماری است. این سند، نه تنها انتخاب انجام‌شده، بلکه دلایل، شرایط، گزینه‌های جایگزین و پیامدها را (کامل و با صراحت) مستند می‌کند. کاربردش این است که هر معمار، مدیر یا دولوپری در آینده بتواند منطق تصمیمات را درک کند و اگر نیاز به تغییر بود، آن را آگاهانه و اصولی انجام دهد.

ساختار استاندارد ADR

معمولاً هر ADR شامل چهار بخش کلیدی است:

  1. عنوان (Title): موضوع تصمیم معماری، به‌صورت خلاصه.
  2. زمینه (Context): توضیح شرایط و مشکل/فرصت، به‌همراه گزینه‌های جایگزین.
  3. تصمیم (Decision): شرح تصمیم گرفته‌شده و علت ارجحیت آن.
  4. پیامدها (Consequences): اثرات مثبت/منفی تصمیم و trade-offها.

مثال واقعی ۱: ADR برای سرویس پرداخت

عنوان: جداسازی اعتبارسنجی کارت بانکی به سرویس مستقل

زمینه:
پرداخت کاربران باید بلافاصله تأیید شود تا تجربه کاربری مناسب باشد. دو گزینه وجود دارد:

  • انجام اعتبارسنجی در همان سرویس خرید
  • جدا کردن اعتبارسنجی به یک سرویس مستقل در صورت استقلال، مزیت دیپلوی مستقل و تحمل خطا داریم، اما لَتِنسی و پیچیدگی افزایش می‌یابد.

تصمیم:
اعتبارسنجی کارت بانکی به‌‌عنوان یک سرویس مستقل پیاده‌سازی می‌شود. این سرویس نسخه‌بندی‌شده و مستقل از چک‌آوت عمل می‌کند.

پیامدها:

  • مزیت: دیپلوی مجزا، تحمل بالاتر به خطاهای بانکی، تسهیل تغییر بانک‌های ارائه‌دهنده API.
  • عیب: افزایش لَتِنسی (رفت‌وبرگشت شبکه)، احتمال خطا در ارتباطات، پیچیدگی بیشتر مانیتورینگ مسیر سرویس‌ها.

مثال واقعی ۲: ADR از متن کتاب (Sysops Squad)

عنوان: یکپارچگی سرویس مدیریت مشتری

زمینه:
کاربر برای ثبت‌نام باید اطلاعات پروفایل، کارت، پسورد و محصولات را درج کند. سه گزینه:

  • تجمیع همه قابلیت‌ها در یک سرویس (یکپارچگی)
  • جداسازی بر اساس نوع داده (حساس/غیرحساس)
  • جداسازی هر قابلیت به سرویس مستقل

تصمیم:
یک سرویس یکپارچه ساخته می‌شود تا ضمن حفظ تراکنش اتمیک و ACID، انجام عملیات ثبت‌نام سریع و پایدار باشد. امنیت داده‌های حساس از طریق لایه امنیتی ابزار Tortoise و Service Mesh انجام می‌شود.

پیامدها:

  • مزیت: انجام عملیات ثبت‌نام در یک transaction، سهولت اطمینان از یکپارچگی داده.
  • عیب: در صورت ضعف امنیتی، داده‌های حساس آسیب‌پذیرتر می‌شود؛ وابستگی بیشتر کدها.

توضیح عمیق‌تر درباره Fitness Function

فلسفه Fitness Function

در معماری و DevOps هدف این است که سلامت و تطابق ساختار معماری با اهداف تجاری یا تکنیکی به شیوه‌ای قابل سنجش و اتوماتیک بررسی شود. Fitness Function قاعده‌ای نرم‌افزاری (کد، تست، هشدار، پابنده، یا مانیتورینگ) است که به صورت پیوسته یکی از ویژگی‌های اصلی معماری را اندازه‌گیری می‌کند و اگر سازگاری مختل شد، هشدار یا خطا می‌دهد.

دسته‌بندی

  • اتمی: بررسی صرفاً یک معیار (مثلاً عدم وابستگی سیکلی بین ماژول‌ها)
  • کل‌نگر: ترکیب چند ویژگی (مثلاً حفظ تعادل بین امنیت و کارایی)

مثال Fitness Function اتمی (کد جاوا-سی‌شارپ)

بررسی ضد-وابستگی سیکلی فرض کنیــد می‌خواهیم مطمئن شویم که هیچ cyclic dependency بین ماژول‌ها وجود ندارد. می‌توان از ابزارهایی مثل [NDepend] برای #C یا [JDepend] برای جاوا استفاده کرد.

// با استفاده از NDepend در دات‌نت:
warnif count > 0 from t in Application.Types
where t.IsUsing(t) // چک کردن dependency بر روی خودش یا دیگر ماژول‌های یکسان
select t

این خط کد، گزارشی می‌دهد تا هر circular dependency دیده شد، تیم dev هشدار بگیرد.

مثال Fitness Function کاربردی (سفارشی برای سلامت ارتباطات میکروسرویس‌ها)

سلامت تعداد وابستگی‌های سرویس در سازمانی توافق شده هیچ سرویسی نباید به بیش از ۳ سرویس دیگر وابسته باشد (health of independence):

# pseudo-code:
for each service in all_services:
    if service.dependencies.count > 3:
        alert("Service {service.name} has too many dependencies")

این اسکریپت می‌تواند در pipeline CI/CD یا مانیتورینگ اجرا شود.

مثال پیشرفته: تست performance دوره‌ای (holistic)

فرض کنید تیم شما باید تضمین کند که response time برای endpoint خاص، همیشه زیر ۵۰۰ms باشد.

  • یک تست performance اتوماتیک در pipeline اجرا می‌کند اگر:
    • میانگین پاسخ بالاتر از عدد threshold باشد، build fail شود.
    • (در تست) مصرف منابع کل cluster را در بازه شلوغی هم بررسی کند.

مرور و نکاتی عملی:

  • ADR: هر زمان تصمیم معماری جدی می‌گیرید یا الگوی جدیدی پیاده می‌کنید، همین ساختار را مستند کنید؛ حتی اگر اول کار ساده‌تر بنویسید و بعداً کامل‌تر کنید.
  • Fitness Function: سعی کن برای هر اصل معماری که برات اهمیت حیاتی دارد، یک تست اتوماتیک یا مانیتورینگ قرار دهی تا هر نوع deviation بلافاصله قابل تشخیص باشد.


فصل دوم: درک کوپلینگ (Discerning Coupling in Software Architecture)

در این فصل، تمرکز روی “کوپلینگ” یا وابستگی‌های درونی بخش‌های مختلف یک سیستم نرم‌افزاری است. یکی از سخت‌ترین وظایف معمار، تشخیص و مدیریت همین وابستگی‌هاست. بسیاری از مشکلات پیچیده معماری (مخصوصاً در معماری توزیع‌شده مثل مایکروسرویس‌ها) ناشی از درهم‌تنیدگی غیرشفاف این وابستگی‌هاست.

۱. چرا بحث کوپلینگ مهم است؟

همه جا می‌شنویم: سیستم شُل‌-وابسته (loosely coupled) خوب است!
اما واقعیت ساده‌تر و عمیق‌تر است: بعضی کوپلینگ‌ها اجتناب‌ناپذیر و حتی ضروری‌اند. اگر همه اجزای سیستم کاملاً از هم جدا باشند، چگونه باید همکاری کنند؟ معمار موفق کسی است که بفهمد چه وقت، کجا، و چقدر کوپلینگ لازم و مفید است.

کوپلینگ یعنی: اگر تغییر در یک بخش سیستم، بخش دیگر را مجبور به تغییر کند، آن دو قسمت کوپل شده‌اند.

۲. قدم‌های تحلیل پیچیدگی معماری

نویسندگان یک مسیر سه‌مرحله‌ای معرفی کرده‌اند:

  1. شناسایی بخش‌های به‌هم‌ گره خورده (Entangled Parts)
    باید بفهمیم دقیقا کدام بخش‌ها به هم وابسته‌اند.
  2. تحلیل نوع و شدت کوپلینگ
    کوپلینگ فقط یک معنی ندارد! نوع و سطح آن (ایستا/پویا) اهمیت دارد.
  3. تحلیل تِرِید-آف (Trade-Off)ها
    تغییر چه اثری روی بخش‌های دیگر می‌گذارد؟ هزینه تغییر چیست؟ ممکن است علاوه بر مشکلات کد و فنی، اثرات سازمانی و تجاری هم داشته باشد.

۳. تعریف “کوآنتوم معماری” (Architecture Quantum)

اینجا نویسندگان یک مفهوم بکر معرفی می‌کنند:
“کوآنتوم معماری” یعنی: کوچکترین واحد مستقل از نگاه پیاده‌سازی و دیپلوی که کارایی بالا، کوپلینگ ایستا و کوپلینگ پویا دارد.

  • مثال: یک میکروسرویس کامل که هسته یک بیزینس ‌دومِین (Bounded Context) را پیاده‌سازی می‌کند.
  • هر کوآنتوم ویژگی‌های زیر را باید داشته باشد:
    • دیپلوی مستقل (شما می‌توانید آن را جدا توسعه و دیپلوی کنید)
    • همبستگی داخلی بالا (functional cohesion بالا؛ یعنی اجزای داخلی آن به هم بشدت مرتبط‌اند ولی بیرونش کمترین ارتباط را دارد)
    • کوپلینگ ایستا (Static Coupling) بالا: اجزای درون کوآنتوم از لحاظ کد، کانفیگ، دیتابیس و … به هم وابسته‌اند.
    • کوپلینگ پویا (Dynamic Coupling): اجزای مختلف کوآنتوم در زمان اجرا با هم ارتباط دارند (معمولاً به صورت سینکرون).

پرسش:

  • آیا تجربه کردی که بخواهی بخشی از برنامه را جدا دیپلوی کنی، اما چون دیتابیس مشترک بود، عملاً نتوانستی مستقل این کار را انجام دهی؟
  • نتیجه: کوآنتوم عملی نشده و هنوز کوپلینگ بین دو بخش باقی مانده است.

۴. انواع کوپلینگ: ایستا و پویا

الف) کوپلینگ ایستا (Static Coupling)

این همان وابستگی‌هایی است که در زمان ساخت و Build (نه اجرا) دیده می‌شود: مثلاً

  • Dependency به یک پکیج یا لایبرری خارجی
  • داشتن یک دیتابیس مشترک یا کامپوننت مشترک
  • قراردادهای سخت کد (مثل Strong Typing، یا شِرینگ مدل‌های داده بین سرویس‌ها)

ویژگی: اگر بخواهید فلان سرویس را بدون این وابستگی دیپلوی کنید، شکست می‌خورید.
پس هر عنصر خارجی که برای اجرا لازمه، بخشی از کوپلینگ ایستا است.

ب) کوپلینگ پویا (Dynamic Coupling)

ارتباطاتی که فقط در زمان اجرا ایجاد می‌شود:

  • فراخوانی API بین سرویس‌ها در زمان اجرای برنامه
  • ارسال پیام از طریق پیام‌برها (Message Broker) یا صف‌ها
  • تعامل سرویس‌ها با یکدیگر در طول یک Workflow

ویژگی: بدون وجود ارتباط در لحظه، بخش‌ها می‌توانند “مستقل اجرا شوند” اما workflow به صورت runtime بهم وصل می‌شود.

۵. گیج‌کننده‌ترین چالش معماری: Granularity صحیح سرویس‌ها

سؤال قدیمی: “سرویس‌های من چندتا باشند؟ هرکدام چقدر بزرگ؟”

  • سرویس خیلی کوچک: مشکل تراکنش، اورکستراسیون و اجرای هماهنگ Workflow
  • سرویس خیلی بزرگ: inherit مشکلات مانولیتی؛ scale و توزیع سخت می‌شود

اینجا تحلیل کوپلینگ تمرکز اصلی است؛ باید بفهمیم هر سرویس (یا کوآنتوم) چقدر از سایر سرویس‌ها مستقل است.

۶. مثال‌ها و سناریوها

مثال ۱:

یک سیستم نوبت‌دهی کلینیک داریم:

  • مودل مانولیتی: همه ماژول‌ها (ویزیت، صندوق، پیامک، پرداخت) یکجا دیپلوی می‌شوند و دیتابیس مشترک دارند. فقط یک کوآنتوم تشکیل می‌شود، هرچقدر هم بخش‌بندی کد کنیم.
  • سناریوی توزیع: اگر “پرداخت” را واقعاً جدا کنیم (با دیتابیس مختص و API جدا)، و همه وابستگی‌های Deployment را قطع کنیم، تبدیل به یک کوآنتوم مستقل می‌شود.

مثال ۲ (از فصل):

در یک معماری event-driven، اگر همه سرویس‌ها از یک message broker و یک دیتابیس استفاده کنند (مثلاً Kafka و یک RDBMS مشترک)، باز هم کوآنتوم یکی است، چون هیچ‌کدام بدون آن منابع مشترک کار نمی‌کنند.

ولی، اگر چند سرویس، دیتابیس و message broker جدا داشته باشند، هرکدام کوآنتوم مستقل خواهند بود.

تمرین:

  • در پروژه فعلی‌ات، اگر یک سرویس جدید بسازی:
    • آیا کاملاً جدا دیپلوی می‌شود؟
    • آیا دیتابیس و منابع زیرساخت خودش را دارد؟
    • آیا در زمان اجرایش نیازی به روشن بودن سایر سرویس‌ها نیست؟ اگر به همه بله دادی، تو یک کوآنتوم مستقل داری.

۷. اشکال و الگوهای رایج معماری و وضعیت کوانتوم آنها

  • معماری مانولیتی و نیو-مانولیت (Monolithic): فقط یک کوآنتوم
  • Service-Based: اگر دیتابیس مشترک باشد، باز فقط یک کوآنتوم
  • Microservices واقعی: هر سرویس (با دیتابیس، پیاده‌سازی، و منابع جدا) کوآنتوم جدا.
  • Micro-frontends: هر بخش از UI که توسط یک میکروسرویس جدا ران می‌شود، خودش یک کوآنتوم مجزاست.
  • Shared DB: هر جا هنوز دیتابیس مشترک وجود دارد، حتی اگر سرویس‌ها مستقل باشند، فقط یک کوآنتوم واقعی خواهد بود و جداسازی ناقص است.

۸. ابزار عملی برای تحلیل کوپلینگ

معیار ساده:

“اگر این بخش را در محیط جدید بدون هیچ چیز دیگر بالا بیاوریم و کار کند، کوآنتوم مستقل است.”

ابزارهای کمکی:

  • دیاگرام‌های مدرن وابستگی (Dependency graphs)
  • ابزارهای تحلیل استاتیک کد (مثل SonarQube، NDepend)
  • جداسازی زیرساخت‌های مورد نیاز (Databases، Message Brokers، …)
  • تحلیل کانفیگ و مدیریت اسرار (Secrets Management)

جمعبندی این فصل (کد به ذهن بسپار):

  • کوپلینگ الزاماً بد نیست؛ میزان و محل آن مهم است.
  • کوآنتوم معماری = کمترین واحد واقعا مستقل اجرایی و ساختاری
  • کوپلینگ ایستا = وابستگی‌های ساختاری/قبل اجرای برنامه
  • کوپلینگ پویا = وابستگی‌های ارتباطی/در زمان اجرا
  • هر منبع اشتراکی (حتی UI یا DB) عامل یکی‌کردن کوآنتوم‌هاست
  • در فاز مهاجرت مانولیت به میکروسرویس، تحلیل دقیق و عملی کوپلینگ ضروری است

سؤالات یادگیری و تأمل:

  1. چگونه می‌توانی بفهمی یک کوآنتوم واقعی داری، نه یک شبه-میکروسرویس؟
  2. نمونه‌ای در پروژه‌ات داری که کوپلینگ مانع جدایی کامل یک سرویس شده باشد؟
  3. آیا دیاگرامی از Dependency ها در پروژه رسم کرده‌ای؟ چه چیزهایی را آشکار می‌کند؟

بررسی کوپلینگ و کوآنتوم وقتی Broker و Database زیرساخت مرکزی دارند

۱. سرویس مرکزی Kafka، ولی Topic جدا برای هر سرویس

سناریو:
فرض ‌کن همه سرویس‌ها برای ارسال و دریافت پیام از یک Kafka مرکزی استفاده می‌کنند، اما هر سرویس فقط داخل topic خودش پیام می‌فرستد و مصرف می‌کند.

تحلیل کوپلینگ:

  • زیرساخت مرکزی («Shared Infrastructure»):
    همه سرویس‌ها برای عملکردشان وابسته به Kafka هستند؛ اگر Kafka قطع شود، کل سیستم دچار اختلال خواهد شد.
  • Topic جدا:
    از نظر “business logic”، مستقل‌اند (یعنی داده و پیام‌های هر سرویس با دیگری ترکیب نمی‌شود)، اما از نظر اجرا وابسته به Kafka مرکزی.

نتیجه:

  • این معماری کوآنتوم واقعی نمی‌سازد، چون اگر مثلا بخواهی فقط یکی از سرویس‌ها را در محیطی بدون Kafka بالا بیاوری، کار نمی‌کند.
  • اگر Kafka به طور موازی و مستقل (برای هر سرویس یک instance جدا) داشته باشی، تازه هر سرویس کاملاً مستقل می‌شود و کوآنتوم واقعی شکل می‌گیرد.
  • فقط جدا بودن Topic کافی نیست: dependency روی Kafka مرکزی یعنی کوپلینگ ایستا مشترک، حتی اگر coupling پویا (ارتباطات داده‌ای) جدا باشد.

۲. دیتابیس مرکزی، اما دیتابیس‌های جدا روی آن

سناریو:

  • یک سرور SQL یا RDBMS مرکزی داریم، اما برای هر سرویس schema و دیتابیس جدا تعریف شده است. هیچ سرویسی جدول‌های سرویس دیگر را نمی‌خواند یا نمی‌نویسد.

تحلیل کوپلینگ:

  • زیرساخت مرکزی:
    همه سرویس‌ها نیازمند در دسترس بودن سرور دیتابیس مرکزی هستند. اگر سرور DB قطع شود، تمام سرویس‌ها آسیب می‌بینند.
  • دیتابیس جدا:
    “مرز داده‌ها” رعایت شده، هر سرویس صاحب data و schema خودش است، پس coupling در سطح business logic پایین آمده است.

نتیجه:

  • این هم کوآنتوم کامل نیست، مگر اینکه بتوانی هر سرویس را با دیتابیس خودش (روی instance جدا یا حتی روی سرور مستقل) بالا بیاوری.
  • وقتی دیتابیس مشترک، هرچند با schema جدا، وجود دارد dependency زیرساختی یکجا می‌کند؛ تمام سرویس‌ها به روشن بودن سرور وابسته‌اند.
  • از نظر تغییرپذیری: آپدیت امنیت یا patch سرور روی همه اثر دارد؛ اگر باید یکی را مستقل به روزرسانی کنی و بقیه وابسته باشند، وابستگی باقی مانده است.

برای مقایسه

حالت دیتابیس حالت واقعی
دیتابیس server جدا برای هر سرویس هر سرویس کوآنتوم واقعی/دیپلوی/تغییر مستقل
دیتابیس مشترک، ولی schema جدا کوپلینگ زیرساختی دارد، استقلال ناقص
دیتابیس/جدول مشترک کوپلینگ کامل، فقط یک کوآنتوم
حالت Kafka/Message Broker حالت واقعی
Broker جدا برای هر سرویس کوآنتوم واقعی/سرویس کاملاً منفصل
Broker مشترک، Topic جدا استقلال ناقص، dependency زیرساختی باقی است
Broker و Topic مشترک کوپلینگ کامل، تنها یک کوآنتوم

نکته مهم

در معماری‌های enterprise معمولاً اجتناب کامل از shared infrastructure عملی نیست (به دلایل هزینه، اجرا، DevOps). هدف معماری ایده‌آل این است که لایه زیرساخت طوری مدیریت شود که قطع یا تغییر یک سرویس، سایر سرویس‌ها را مختل نکند و دیپلوی سرویس‌ها صددرصد مستقل باشد. کوآنتوم کامل، این استقلال را ممکن می‌کند.

سوال برای تمرین:

در پروژه فعلی‌ات (یا پروژه‌ای که ساختارش را می‌شناسی)، آیا سرویس‌ها واقعا مستقل‌اند؟ اگر زیرساخت دیتابیس یا Message Broker را قطع کنی، کدام سرویس‌ها واقعا زنده می‌مانند؟
این تست ساده، میزان کوپلینگ را خیلی خوب نشان می‌دهد.

اگر دوست داشتی با هم درباره الگوهای حذف این کوپلینگ (مثل استفاده از دیتابیس‌های آسان-مستقل، Brokerهای Micro-Broker یا containerized) بیشتر کار کنیم، بگو تا راهکار معماری حرفه‌ای را تحلیل کنیم.
آیا دوست داری سناریو و دیاگرام پیشنهادی برای یک کوآنتوم واقعی با ابزارهای دنیای .NET و Kafka عملی‌تر بررسی کنیم؟



ادامه فصل دوم: کوپلینگ پویا (Dynamic Coupling) و Workflow در معماری سرویس‌گرا

در بخش قبلی، کوپلینگ ایستا (static coupling) و اثر آن بر “کوآنتوم معماری” را به دقت تحلیل کردیم. اما یک بخش کلیدی دیگر باقی مانده: کوپلینگ پویا و نقش آن در runtime و workflowهای معماری‌های نوین.

کوپلینگ پویا (Dynamic Coupling) چیست؟

کوپلینگ پویا به ارتباطات اجزای معماری در زمان اجرا اشاره دارد. این یعنی: سرویس‌ها، ماژول‌ها یا اجزای یک سیستم، هنگام انجام عملیات تجاری چگونه با هم حرف می‌زنند و همکاری می‌کنند.

  • اگر ارتباطات حتما باید به صورت هم‌زمان (synchronous) و با شِرط موفقیت فوری باشد، کوپلینگ پویا “سخت‌تر” است.
  • اگر تعاملات به‌صورت غیرهم‌زمان (asynchronous) انجام شود (مثلا Event/Message مدل)، سطح کوپلینگ کمتر خواهد بود ولی مدیریتِ خطا و چرخه داده پیچیده‌تر می‌شود.

مثال:

  • در ارتباط sync: سرویس پرداخت درخواست را به سرویس سفارش می‌دهد و منتظر پاسخ می‌ماند (اگر قطع شود، کل جریان متوقف می‌شود).
  • در ارتباط async: سرویس پرداخت پیام را روی یک صف یا broker ارسال می‌کند و دیگر منتظر پاسخ سرویس سفارش نمی‌ماند؛ هر وقت جواب آماده شد، سرویس دیگر واکنش نشان می‌دهد.

سه بُعد مهم در کوپلینگ پویا

نویسندگان کتاب می‌گویند هر معماری توزیع‌شده، باید این سه محور را در تعاملاتش شفاف کند:

  1. Communication (ارتباط):
    • Synchronous (همزمان)
    • Asynchronous (غیرهمزمان)
  2. Consistency (تراکنش‌پذیری):
    • Atomic (همراه با تضمین اتمی بودن عملیات)
    • Eventual consistency (هر بخش به مرور به حالت سازگار می‌رسد، اما تضمین آنی نیست)
  3. Coordination (هماهنگی):
    • Orchestration (یک سرویس مرکزی فرآیند را مدیریت می‌کند)
    • Choreography (هر خدمت فقط خودش را می‌شناسد و به پیام‌های event-driven پاسخ می‌دهد)

این سه بعد به صورت ماتریس با eight الگوی مختلف ترکیب می‌شوند (مثلا Epic Saga, Fairy Tale, Phone Tag و غیره) که هرکدام برای نیازهای خاصی مناسب‌اند.

اهمیت انتخاب الگوی مناسب

انتخاب اشتباه جفت‌های ارتباط/تراکنش/هماهنگی باعث موارد زیر می‌شود:

  • افزایش latency و بروز race condition
  • تحمل‌پذیری پایین و کاهش availability
  • مدیریت مشکل خطاها و خطاهای تراکنشی

مثال:

  • اگر هم orchestration و هم تضمین atomicity داشته باشیم، عملاً یک سرویس مرکزی داریم که باید کل عملیات را rollback کند (مثل نمونه Epic Saga).
  • اگر choreography و eventual consistency باشد، هر سرویس با پیام event فقط بخش خودش را انجام می‌دهد حتی اگر بقیه سرویس‌ها شکست بخورند، با سیستم eventual recovery مشکل حل می‌شود (نمونه Fairy Tale).

دیاگرام‌های تصمیم و کوپلینگ

کتاب مثال‌هایی تصویری دارد:

  • Mediated EDA (Event-Driven با Central Mediator): هر سرویس برای تعامل با سایر سرویس‌ها یا منابع مشترکی مثل دیتابیس، از یک mediator یا broker مرکزی استفاده می‌کند. این کوپلینگ را تقویت می‌کند و معمولا فقط یک کوآنتوم ایجاد می‌کند.
  • Brokered EDA with Multiple Data Stores: اگر هر مجموعه‌ای از سرویس‌ها دیتابیس و message broker مجزا داشته باشند، کوآنتوم‌های مستقل شکل می‌گیرند.

ضرب‌آهنگ تصمیم‌سازی معمار

  • باید ابتدا مرز کوآنتوم‌ها را مشخص کنی (هم از نظر دیپلوی و هم runtime)
  • برای هر workflow، سطح مورد نیاز coupling و consistency و coordination را بازبینی کنی
  • ابزارهایی مثل تست یکپارچگی (Integration Tests)، مانیتورینگ distributed tracing و event schema validation را استفاده کنی تا از سلامت runtime coupling مطمئن شوی

جمع‌بندی بخش کوپلینگ پویا:

  • کوپلینگ ایستا و پویا دو بُعد مکمل‌اند.
  • مدیریت ارتباطات sync/async و atomic/eventual در قلب معماری مدرن قرار دارد.
  • معمار خوب قبل از هر اقدام، الگوی مناسب برای workflowها را به صراحت انتخاب و مستند می‌کند.


الگوهای مدیریت Workflow در معماری توزیع‌شده: Choreography و Orchestration

بخش بعدی فصل به بررسی دو الگوی عمده برای مدیریت جریان‌های کاری (workflow) در سامانه‌های توزیع‌شده می‌پردازد. انتخاب بین این الگوها، یکی از تصمیمات سرنوشت‌ساز معماری است چون روی کوپلینگ، مقیاس‌پذیری و قابلیت اطمینان سیستم تاثیر کلیدی دارد.

۱. Orchestration

در این الگو، یک سرویس مرکزی (Orchestrator یا Mediator) کل جریان را مدیریت می‌کند. این سرویس مرکزی مسئول:

  • تعیین توالی انجام کارها
  • فراخوانی سرویس‌های مختلف
  • مدیریت وضعیت جریان و هندل‌کردن errorها است

ویژگی‌ها:

  • کنترلی متمرکز بر کل جریان.
  • مدیریت راحت‌تر خطا و rollback.
  • وضعیت workflow همیشه قابل ردیابی کامل است.
  • اما: یکی از نقاط کوپلینگ مرکزی در سیستم تلقی می‌شود؛ کاهش دهنده flexibility و scale است.

مثال:
در یک سیستم سفارش آنلاین، Order Orchestrator ترتیب را مشخص می‌کند:
ابتدا ثبت سفارش → فراخوانی سرویس پرداخت → تایید پرداخت → ارسال به Fulfillment → ابلاغ به سرویس ایمیل.

۲. Choreography

در این الگو هیچ سرویس مرکزی وجود ندارد؛ هر سرویس خودش با مشاهده eventها و پیام‌ها تصمیم می‌گیرد چه کاری انجام دهد. گردش کار به صورت غیرمتمرکز بین سرویس‌ها پخش می‌شود.

ویژگی‌ها:

  • هر سرویس فقط خود و eventهای مرتبط را می‌شناسد و action خودش را انجام می‌دهد.
  • مقیاس‌پذیری و انعطاف‌پذیری بالاتر.
  • خطا و exception handling سخت‌تر و پراکنده‌تر است.
  • ردیابی وضعیت کل workflow به شکل متمرکز دشوار است.

مثال:
در همان سیستم سفارش، سرویس Order پس از ایجاد سفارش، event ایجاد سفارش رامنتشر می‌کند. Payment با شنیدن این پیام، اقدام به پرداخت می‌کند و پس از موفقیت، نتیجه را به صورت event اعلام می‌کند و Fulfillment با شنیدن نتیجه، سفارش را ارسال می‌کند.

۳. مقایسه و Trade-off

  Orchestration Choreography
کنترل مرکزی کار دارد ندارد
مقیاس‌پذیری کمتر بیشتر
قابلیت ردیابی وضعیت راحت دشوار
مدیریت خطا متمرکز پراکنده
پیچیدگی پیاده‌سازی پایین‌تر بالاتر در سناریوهای پیچیده
تاثیر بر کوپلینگ بالاتر پایین‌تر

۴. مثال کاربردی: workflow تیکت پشتیبانی

فرض کن سه سرویس Ticket Management, Assignment, Notification داری:

  • در Orchestration، یک سرویس Workflow Manager همه گام‌ها را سرویس به سرویس فراخوانی و نتیجه را پیگیری می‌کند.
  • در Choreography، Ticket Management پس از ایجاد تیکت، event مربوطه را publish می‌کند. Assignment و Notification بر اساس این event تصمیم‌گیری و اجرا می‌کنند؛ اگر گامی fail شود، event مرتبط با خطا منتشر می‌شود و سایر سرویس‌ها واکنش نشان می‌دهند.

۵. توصیه‌های کلیدی

  • اگر به traceability، مدیریت متمرکز خطا یا انعطاف در تغییر گام‌ها نیاز داری، Orchestration انتخاب بهتری است.
  • اگر scale و استقلال تیم‌ها هدف اصلی باشد، یا workflow ساده‌تر یا تغییرپذیر است، Choreography گزینه مناسب‌تری است.
  • هر دو الگو در واقع complement هم هستند و می‌توانند ترکیبی در پروژه‌های بزرگ داشته باشند.

تراکنش جبرانی (Compensating Transaction) چیست؟

تراکنش جبرانی یک الگو برای مدیریت خطا و بازگرداندن وضعیت در سیستم‌های توزیع‌شده است، زمانی که امکان استفاده از تراکنش‌های کلاسیک (ACID) وجود ندارد. چون در معماری microservices یا event-driven هر سرویس داده یا عمل خاص خودش را انجام می‌دهد، گاهی بخشی از عملیات کامل شده ولی یکی از مراحل آخر کار fail می‌شود. در این صورت دیگر نمی‌توان مثل تراکنش پایگاه داده “همه چیز را به عقب برگرداند”.

تعریف دقیق

تراکنش جبرانی یا Compensating Transaction عملی است که اثر عملیات قبلی را که موفق بوده‌اند، خنثی (یا معکوس) می‌کند تا سیستم به وضعیت سازگار بازگردد. این کار معمولاً به صورت یک روال نرم‌افزاری مجزا برای هر مرحله‌ی موفق قبلی طراحی می‌شود.

مثال کاربردی

فرض کن در یک سیستم رزرواسیون سفر، فرآیند خرید شامل مراحل زیر است:

  1. صدور بلیت پرواز
  2. رزرو هتل
  3. پرداخت نهایی

اگر پس از موفقیت در مرحله 1 و 2، پرداخت نهایی شکست خورد، باید:

  • بلیت پرواز لغو شود (compensating transaction)
  • رزرو هتل آزاد یا کنسل شود (compensating transaction) این لغو عملیات معادل “rollback” در تراکنش‌های متمرکز بانک اطلاعاتی است، با این تفاوت که هر مرحله rollback جداگانه‌ای دارد و با تاخیر اجرا می‌شود.

نحوه پیاده‌سازی

  • در معماری‌های مبتنی بر Saga، برای هر action یک compensating action تعریف می‌شود.
  • این عملیات معمولاً به صورت پیام (event) به سرویس مسئول ارسال می‌شود.
  • ممکن است عملیات جبرانی دقیقاً معکوس عمل اصلی نباشد، چون وضعیت سیستم یا داده‌ها تغییر کرده است (مثلاً اگر بلیت صادر شده refund می‌شود و جریمه‌ای لحاظ می‌گردد).

نکات کلیدی

  • همیشه برای هر گام اصلی که وضعیت سیستم را تغییر می‌دهد، باید امکان compensating action در نظر گرفته شود.
  • تراکنش جبرانی تضمین می‌کند حتی در شکست‌های جزئی، داده سیستم به وضعیت سازگار نزدیک بماند، هرچند ممکن است مثل ACID کاملاً بی‌اثر نباشد.
  • این روش trade-off اصلی را بین availability و consistency مدیریت می‌کند.

اگر نیاز داشتی یک مثال واقعی یا pseudo code اجرای compensating transaction در .NET یا یک نمونه event-driven را بررسی کنیم، بنویس تا دقیق‌تر توضیح دهم.



درس جدید: الگوها و انواع Saga Pattern در معماری توزیع‌شده

Saga Pattern یکی از مهم‌ترین راهکارهای مدیریت تراکنش و یکپارچگی داده (Consistency) در معماری‌های مدرن است. استفاده درست از Saga به شما اجازه می‌دهد تراکنش‌هایی را که میان چند سرویس یا دیتابیس توزیع‌شده باید رخ دهند، بدون نیاز به 2PC و با قابلیت مدیریت خطا (و بازگشت وضعیت) طراحی کنید.

معرفی: Saga چیست؟

Saga در اصل فرایندی است که از چند “تراکنش محلی” متوالی (Local Transaction) تشکیل شده است. هر قدم تراکنشی را روی یک سرویس یا زیرسیستم اجرا می‌کند. اگر در وسط کار یکی از این مراحل fail شود، بجای انجام “rollback” کامل مانند بانک اطلاعاتی سنتی، یک یا چند عملیات جبرانی (compensating transactions) اجرا می‌شود تا اثر مراحل قبلی را خنثی یا معکوس کند.

شکل ساده:

  1. اقدام A اجرا شود (مثلا محول‌کردن وظیفه)
  2. اقدام B اجرا شود (مثلا پرداخت)
  3. اگر مرحله بعدی fail شد (مثلا ارسال کالا)، مراحل پیشین با جبران‌کننده معکوس (cancel/reverse) شوند

انواع Saga و ماتریس این الگو

در کتاب، Saga بر اساس سه بعد اصلی طبقه‌بندی می‌شود:

  • Communication: Synchronous یا Asynchronous
  • Consistency: Atomic یا Eventual
  • Coordination: Orchestrated (با orchestrator) یا Choreographed (سرویس‌ها خودمختار)

این سه ویژگی باعث ایجاد 8 حالت یا Pattern اصلی می‌شود (Epic Saga، Fairy Tale، Anthology، …). هر کدام مزایا و معایب خود را دارند و باید متناسب با نیاز سیستم انتخاب شوند.

نام الگو نوع ارتباط Consistency هماهنگی توضیح مختصر
Epic Saga Sync Atomic Orchestrated الگوی کلاسیک مثل monolithic ACID
Phone Tag Sync Atomic Choreographed فرانت کنترلر، atomic اما بدون mediator
Fairy Tale Sync Eventual Orchestrated orchestrator اما با eventual
Time Travel Sync Eventual Choreographed بدون orchestrator و با eventual
Fantasy Fiction Async Atomic Orchestrated پیچیده و کم کاربرد
Horror Story Async Atomic Choreographed بدترین حالت؛ atomic, async, بی‌هماهنگی
Parallel Async Eventual Orchestrated mediator + async + eventual
Anthology Async Eventual Choreographed کمترین coupling، بیشترین آسانی scale

مثال عملی از یک Saga مدیریت تیکت پشتیبانی

  1. START: ثبت تیکت توسط کاربر (در Ticket Service).
  2. CREATED: سرویس ذی‌ربط، تیکت را assign می‌کند.
  3. ASSIGNED: تیکت به موبایل متخصص ارسال می‌شود.
  4. ACCEPTED: متخصص تائید می‌کند.
  5. COMPLETED/REASSIGN: یا مشکل رفع و وضعیت بسته می‌شود، یا به متخصص دیگر می‌رود.

اگر در هر مرحله شکست رخ دهد، یک transaction جبرانی برای معکوس کردن عملیات قبلی اجرا می‌شود (مثلا اگر assign موفق شود اما ارسال پیام شکست بخورد → assign لغو شود).

نکات کلیدی انتخاب Saga Pattern

  • الگوهای orchestration، مدیریت خطا و وضعیت را ساده‌تر می‌کنند و برای workflowهای پیچیده مناسب‌اند اما ممکن است scale را کاهش دهند.
  • Choreography، ساده‌تر، مقیاس‌پذیرتر و decoupled تر است اما مدیریت error و وضعیت workflow سخت‌تر می‌شود.
  • در الگوهای “Eventual Consistency” تاخیر و اختلاف لحظه‌ای داده بین سرویس‌ها طبیعی است.
  • پیاده‌سازی “Compensating Transaction” ضروری است و هر عملیات عملی باید قابلیت معکوس‌سازی داشته باشد.

تمرین و استفاده حرفه‌ای

  • برای هر معماری distributed، ببین کدام نقطه از ماتریس Saga برای کاربرد تو مناسب‌تر است (مثلا سامانه سفارش ساده = Anthology، سامانه مالی حساس = Epic یا Fairy Tale).
  • ترسیم state machine کارکرد Saga با نمودار (یا code) کمک به شفافیت روند می‌کند.
  • در هر گام باید logging و dead-letter queue طراحی شود برای بازیابی خودکار یا بررسی دستی خطاها.

برای هر پترن، سه‌تا محور رو یادت نگه دار:
Communication (Sync/Async)، Consistency (Atomic/Eventual)، Coordination (Orchestrated/Choreographed). الان هر ۸ تا رو با تمرکز روی کاربرد، مزایا/معایب و جاهایی که به درد می‌خورن باز می‌کنیم.

Epic Saga (sao) – کلاسیک، شبیه Monolith

  • ویژگی‌ها: Sync + Atomic + Orchestrated.
  • شکل ذهنی: یک Orchestrator مرکزی داری که با چند سرویس صحبت می‌کند و همه باید در یک تراکنش “همه یا هیچ” موفق شوند.

مزایا

  • بیشترین شباهت به دنیای Monolith و ACID؛ برای تیم‌های عادت‌کرده به RDBMS راحت‌تر است.
  • یک نقطه واضح برای کنترل workflow و error handling (Orchestrator).

معایب

  • Coupling بسیار زیاد (هر سه بعد شدید): هر مشکلی در یک سرویس، کل saga را می‌خواباند.
  • پیاده‌سازی distributed atomicity (با compensating transactions و امثالهم) پر از failure mode و پیچیدگی است؛ scale و performance ضعیف می‌شود.

کِی استفاده؟

  • فرآیندهای خیلی حساس که فعلاً سازمان “اتمی بودن” را رها نمی‌کند (بانکی/مالی internal).
  • وقتی هنوز به‌نوعی در حال transition از monolith هستی و نخواستی ذهن بیزینس را با eventual consistency درگیر کنی.

Phone Tag (sac) – Front Controller بدون Orchestrator

  • ویژگی‌ها: Sync + Atomic + Choreographed.
  • شکل ذهنی: هیچ Orchestrator رسمی نیست؛ اولین سرویسی که call می‌شود نقش Front Controller را دارد و بعد خودش سرویس بعدی را call می‌کند، و همین‌طور زنجیروار.

مزایا

  • یک bottleneck مرکزی (orchestrator) نداری، پس کمی scale بهتر نسبت به Epic.
  • happy path می‌تواند سریع‌تر باشد چون آخرین سرویس مستقیم جواب را برمی‌گرداند.

معایب

  • هنوز atomic است، پس پیچیدگی transactional باقی است.
  • هر سرویس باید هم منطق دامین خودش را بداند هم منطق workflow و error chain را، که complexity را بالا می‌برد.

کِی استفاده؟

  • Workflow ساده، تعداد گام کم، و نیاز به atomic بودن؛ ولی نمی‌خواهی Orchestrator مرکزی بسازی.
  • به‌محض اینکه workflow پیچیده شود، عملاً مجبورت می‌کند برگردی به Orchestrator.

Fairy Tale (seo) – خوش‌خیم‌ترین حالت پرکاربرد

  • ویژگی‌ها: Sync + Eventual + Orchestrated.
  • شکل ذهنی: Orchestrator داری، ولی به‌جای atomic بودن، هر سرویس تراکنش محلی خودش را انجام می‌دهد و کل داستان با eventual consistency تنظیم می‌شود.

مزایا

  • constraint سخت Atomic حذف می‌شود؛ هر سرویس transaction خودش را مدیریت می‌کند.
  • Orchestrator مدیریت workflow و error handling را ساده می‌کند؛ پیچیدگی ذهنی نسبتاً پایین است.
  • scale نسبت به Epic خیلی بهتر است، چون locking سراسری و ۲PC نداری.

معایب

  • Coupling هنوز بالاست (sync + orchestrator).
  • باید بیزینس را قانع کنی که “تاخیری کوتاه” در هماهنگ شدن داده‌ها طبیعی است.

کِی استفاده؟

  • اکثر سیستم‌های business-critical با نیاز به traceability و مدیریت خطای متمرکز ولی قابل‌قبول بودن eventual consistency.
  • جای خیلی خوبی برای مهاجرت از Epic به سمت الگوهای شُل‌تر.

Time Travel (sec) – chain sync با eventual

  • ویژگی‌ها: Sync + Eventual + Choreographed.
  • شکل ذهنی: سرویس‌ها chain می‌سازند (مثل Pipes and Filters یا Chain of Responsibility)، هرکدام تراکنش خودش را انجام می‌دهد و نتیجه را به بعدی پاس می‌دهد؛ orchestrator وجود ندارد.

مزایا

  • coupling نسبت به Fairy Tale کمتر است چون Orchestrator حذف شده.
  • برای workflowهای خطی با throughput بالا (import داده، پردازش batch، pipeline های ساده) بسیار خوب است.

معایب

  • برای workflowهای پیچیده، debugging و error handling سخت می‌شود.
  • هر سرویس باید context بیشتری از workflow بداند (state + خطا).

کِی استفاده؟

  • pipelineهای نسبتاً ساده با مراحل زیاد ولی منطق ساده (ETL، پردازش log، پردازش سندها).
  • جایی که eventual consistency اوکی است و نیاز به orchestrator مرکزی نداری.

Fantasy Fiction (aao) – Async + Atomic + Orchestrated

  • ویژگی‌ها: Async + Atomic + Orchestrated.
  • شکل ذهنی: می‌خواهی هم atomic باشی، هم orchestrator داشته باشی، هم async باشی؛ ظاهراً “خوبه”، اما…

مزایا

  • از نظر تئوری، می‌خواهد performance را با async بالا ببرد ولی transactional باقی بماند.

معایب

  • atomicity در دنیای async یعنی orchestrator باید state همه تراکنش‌های درحال اجرا (pending) را نگه‌ دارد، با race condition و deadlock و ترتیب رسیدن پیام‌ها سر و کله بزند.
  • پیاده‌سازی و دیباگ بسیار سخت؛ scale هم در عمل خوب نمی‌شود چون atomic بودن گلوگاه است.

کِی استفاده؟

  • خیلی کم؛ معمولاً باید یا atomic را رها کنی (برو Parallel)، یا async را؛ این پترن در کتاب تقریباً به‌عنوان “چیزی که بهتر است نروی سمتش مگر مجبور باشی” مطرح شده.

Horror Story (aac) – ضد‌الگوی واقعی

  • ویژگی‌ها: Async + Atomic + Choreographed.
  • شکل ذهنی: هیچ Orchestrator ای نداری، همه چیز async است، ولی می‌خواهی atomic هم باشی. باید از هر جایی بتوانی state همه تراکنش‌های توزیع‌شده را در زمان‌های مختلف، out-of-order، rollback کنی.

مزایا

  • تقریباً مزیت جدی‌ای ندارد نسبت به الگوهای بهتر! coupling در دو بعد کم شده، ولی atomic همه چیز را خراب می‌کند.

معایب

  • پیاده‌سازی و reasoning تقریباً کابوس؛ هر سرویس باید undo چند تراکنش درحال اجرا را بداند، با ترتیب‌های مختلف و پیام‌های out-of-order.
  • error handling به‌شدت پیچیده؛ debugging و operation آن در محیط واقعی بسیار سخت.

کِی استفاده؟

  • عملاً نباید انتخاب شود؛ بیشتر به‌عنوان anti-pattern مطرح است و هشدار: «اگر از Epic شروع کردی و برای performance فقط async و choreography اضافه کردی، داری توی Horror Story می‌افتی».

Parallel (aeo) – Orchestrator + Async + Eventual

  • ویژگی‌ها: Async + Eventual + Orchestrated.
  • شکل ذهنی: orchestrator داری، اما callها async هستند و هر سرویس تراکنش خودش را دارد؛ orchestrator بیشتر state و هماهنگی و error recovery را مدیریت می‌کند.

مزایا

  • throughput و responsiveness بالا به خاطر async.
  • eventual consistency complexity atomic را کاهش می‌دهد؛ تراکنش‌های محلی ساده‌ترند.
  • مناسب workflowهای پیچیده که هنوز می‌خواهی “مرکز کنترل” داشته باشی.

معایب

  • orchestrator همچنان یکی از گلوگاه‌های معماری است (از نظر scale و availability).
  • مدیریت state async و error handling پیچیده‌تر از Fairy Tale است.

کِی استفاده؟

  • workflowهای پیچیده با نیاز به throughput بالا و کنترل متمرکز (مثلاً orchestration سفارش در سیستم‌های بزرگ).
  • وقتی از Fairy Tale شروع کردی و بعد نیاز به async برای performance پیدا شد.

Anthology (aec) – کم‌کوپل‌ترین، بیشترین scale

  • ویژگی‌ها: Async + Eventual + Choreographed.
  • شکل ذهنی: هیچ orchestrator ای نیست؛ سرویس‌ها با event/message و async کار می‌کنند، هرکدام state و تراکنش خودش را دارد؛ نهایت decoupling.

مزایا

  • کمترین coupling بین همه پترن‌ها؛ بهترین scale و elasticity.
  • عالی برای throughput بالا، پردازش‌های event-driven، pipes & filters با حجم زیاد.

معایب

  • complexity بالاست: هیچ مرکز کنترل واحدی نیست، برای workflow پیچیده باید state را در خود پیام‌ها یا state machineها (با تکنیک‌هایی مثل stamp coupling) حمل کنی.
  • debug و trace کل workflow نیاز به observability قوی (tracing, correlation id, log) دارد.

کِی استفاده؟

  • سیستم‌های event-driven بزرگ، ingestion و پردازش انبوه داده، microservices mature با تیم‌های مستقل و observability قوی.
  • وقتی SLA ها بیشتر روی throughput و availability است تا “فوری atomic بودن”.

جمع‌بندی مقایسه‌ای

جدول مقایسه‌ای سریع

Pattern Comm Consistency Coord Coupling Complexity Scale نمونه استفاده خوب
Epic Sync Atomic Orchestrated خیلی زیاد کم تا متوسط خیلی کم شبیه monolith، مالی حساس
Phone Tag Sync Atomic Choreograph. زیاد زیاد کم workflow ساده، بدون orchestrator
Fairy Tale Sync Eventual Orchestrated زیاد خیلی کم خوب میکروسرویس های تجاری متعارف
Time Travel Sync Eventual Choreograph. متوسط کم خوب pipelines ساده با throughput بالا
Fantasy Fiction Async Atomic Orchestrated زیاد زیاد کم نادر، بهتر است از Parallel استفاده شود
Horror Story Async Atomic Choreograph. متوسط خیلی زیاد متوسط anti-pattern، ازش فرار کن
Parallel Async Eventual Orchestrated کم کم خیلی خوب workflow پیچیده + throughput بالا
Anthology Async Eventual Choreograph. خیلی کم زیاد عالی event-driven با خطای کم/ساده


بخش بعدی فصل ۲: استفاده عملی از کوآنتوم‌ها برای تصمیم‌گیری معماری

در این بخش نویسنده از حالت تئوری خارج می‌شود و می‌گوید:
«حالا که فهمیدی کوآنتوم چیه و کوپلینگ ایستا/پویا چطور کار می‌کنن، در عمل با این مفهوم چه کار می‌کنی؟»

فلسفه ساده است:

  • به‌جای این‌که بپرسی: «مایکروسرویس بهتر است یا مانولیت؟»،
  • بپرس: «کوآنتوم‌های مناسب سیستم من چی هستند و به چه دلیلی باید از هم جدا یا در یک واحد نگه داشته شوند؟»

سه قدم اصلی:

1) شناسایی کوآنتوم‌های فعلی (Current Architecture Quantums) 2) تحلیل کوپلینگ‌ها و ریسک‌ها 3) تصمیم برای ادغام یا شکستن کوآنتوم‌ها (Refactor Architecture)

بیاییم هر کدام را خلاصه و مفهومی بگوییم.

۱) شناسایی کوآنتوم‌های فعلی

نویسنده پیشنهاد می‌کند ابتدا بدون تعصب، وضعیت فعلی را شفاف ببینی:

  • چه چیزهایی فقط با هم دیپلوی می‌شوند و نمی‌توانی جداشان کنی؟
  • چه چیزهایی دیتابیس، message broker، یا runtime مشترک دارند؟
  • اگر یک سرویس را روی محیط جدا بالا بیاوری، دقیقاً چه چیزهایی را باید همراهش بیاوری تا کار کند؟

این‌ها کوآنتوم واقعی یا «کاندیدا»ی کوآنتوم هستند.

برای کمک، معمولاً:

  • dependency graph از سرویس‌ها، دیتابیس‌ها، queueها، external systems رسم می‌کنی.
  • برای هر نود، resourceهای لازم برای اجرای مستقل را لیست می‌کنی.

سؤال کلیدی: اگر این نود را در یک cluster دیگر deploy کنم، چه چیزهایی را باید با آن ببرم؟
جواب، مرز کوآنتوم را نشان می‌دهد.

۲) تحلیل کوپلینگ‌ها و ریسک‌ها

بعد از شناسایی کوآنتوم‌ها، باید ببینی:

  • کدام کوآنتوم‌ها بیشترین وابستگی را دارند؟
  • کدام dependencyها پرخطرترند؟ (از نظر performance, consistency, تیمی، تغییرپذیری)

این‌جا معماری فقط تکنیکال نیست:

  • شاید از نظر تکنیکال بتوانی دو سرویس را جدا کنی، ولی از نظر سازمانی دو تیم مختلف مسئولشان هستند و هماهنگی سخت می‌شود.
  • یا برعکس، از نظر فنی جدا نداده‌اند ولی تیم‌های مختلف روی بخش‌های مختلف کار می‌کنند و تغییر مشترک کابوس است.

در کتاب تأکید می‌شود:

  • «مکانیزم تغییر» و «مکانیزم دیپلوی» را هم معیاری برای مرز کوآنتوم ببین.
    • هر چیزی که اغلب با هم تغییر می‌کند، کاندید است با هم در یک کوآنتوم باشد.
    • هر چیزی که نیاز دارد مستقل scale شود یا مستقل release شود، کاندید است که کوآنتوم جدا شود.

۳) تصمیم برای ادغام یا شکستن کوآنتوم‌ها

وقتی تصویر کوآنتوم‌ها را دیدی، تازه وقت تصمیم است:

  • کجا کوآنتوم‌ها را یکی کنیم؟
  • کجا واقعاً لازم است کوآنتوم را بشکنیم؟

این‌جا همان «Hard Parts» شروع می‌شود. چون:

  • ادغام (Merge) کوآنتوم‌ها:
    • معمولاً وقتی external coupling شدید است (مثلاً سه سرویس هر بار برای یک use case باید پشت‌هم call شوند)
    • یا تغییرات همیشه مشترک و هم‌زمان است
    • یا مدیریت consistency خیلی سخت شده و در عمل داری با compensating transaction خودت را خفه می‌کنی. نتیجه: گاهی بهتر است آن‌ها را در یک کوآنتوم (و حتی در یک سرویس / مانولیت) قرار دهی.
  • شکستن (Split) کوآنتوم‌ها:
    • وقتی bottleneck performance یا scale داری.
    • وقتی تیم‌ها مستقل شده‌اند و release مشترک برایشان زجر است.
    • وقتی SLAهای متفاوت داری (مثلاً پرداخت باید ۹۹.۹۹٪ باشد ولی گزارش‌گیری می‌تواند پایین‌تر باشد).

نکته مهم کتاب:

  • «Microservices as default» اشتباه است.
  • کوآنتوم را با microservice یکی نبین؛ ممکن است یک کوآنتوم، چند پروسه/سرویس داخلی داشته باشد ولی از دید دیپلوی و تغییر، یک واحد باشد.

مثال: سیستم سفارش (Order Management)

فرض کن چهار سرویس داری:

  • Order API
  • Payment
  • Inventory
  • Notification

سناریو:

  • Order و Payment هربار با هم تغییر می‌کنند، مدل داده‌ای مشابه دارند، تیمشان هم مشترک است، و همیشه در یک release deploy می‌شوند.
  • Inventory و Notification هم توسط دو تیم دیگر مدیریت می‌شوند و load مختلفی دارند.

تحلیل:

  • Order + Payment در عمل یک کوآنتوم هستند (حتی اگر دو سرویس جدا باشند).
  • Inventory و Notification کوآنتوم‌های جدا هستند.

تصمیم:

  • شاید به‌جای اصرار به جدا بودن Order و Payment، آن‌ها را در یک سرویس/کوآنتوم ادغام کنی (مثلاً OrderService که پرداخت را هم هندل می‌کند)، اما Inventory و Notification را جدا نگه داری.

این دقیقاً همان چیزی است که کتاب می‌خواهد بگوید:
«از دید کوآنتوم به خانه نگاه کن، نه از دید شعار میکروسرویس.»



- Decomposition و Modularity – ادامه مستقیم بحث کتاب-

ایده‌ی اصلی این بخش:
معمار چطور باید سیستم را تکه‌تکه (decompose) کند تا:

  • توسعه‌پذیر باشد،
  • قابل‌فهم و قابل‌دیپلوی باشد،
  • و از پیچیدگیِ بی‌دلیل (مثلاً “میکروسرویس فقط چون مد است”) دور بماند.

کتاب از همین‌جاست کم‌کم وارد فصل بعدی و بحث‌های ماژولار شدن سیستم می‌شود.

۱) چرا “خُرد کردن به میکروسرویس” جواب مسئله نیست؟-

نویسنده‌ها خیلی شفاف می‌گویند:

  • این‌که یک سیستم را به ۱۰ یا ۵۰ سرویس تقسیم کنیم، به خودیِ خود به این معنی نیست که معماری خوب شده است.
  • معیار کلیدی:

    «ماژول‌های درست، آن بخش‌هایی هستند که:

    • با هم تغییر می‌کنند،
    • و معمولاً با هم دیپلوی می‌شوند.»

اگر دو سرویس:

  • هر بار با هم تغییر می‌کنند،
  • دیتابیسشان به‌شدت در هم تنیده است،
  • تیم‌ها مجبورند همیشه هماهنگ release کنند،

حتی اگر روی کاغذ “microservice” باشند، در واقع یک ماژول/کوآنتوم منطقی‌اند.
کتاب عملاً می‌گوید: برچسب “microservice” چیزی را درست نمی‌کند؛ مرز درست، مهم‌تر از فرم است. - ۲) سه زاویه‌ی اصلی برای Decomposition-

کتاب برای شکستن سیستم به بخش‌های درست، سه زاویه‌ی اصلی را پررنگ می‌کند؛ چیزی فراتر از فقط “bounded context” روی وایت‌برد:

۱) زاویه‌ی تغییر (Change)

  • بپرس: کدام قسمت‌های سیستم عموماً با هم تغییر می‌کنند؟
  • اگر دو ماژول تقریباً همیشه در یک commit تغییر می‌کنند، احتمالاً باید نزدیک هم باشند (در یک ماژول/کوآنتوم).
  • اگر یک بخش خیلی کم تغییر می‌کند و بخشی دیگر دائماً در حال تغییر است، بهتر است جدا باشند تا بخش پایدار برای هر تغییر کوچک ضربه نخورد.

۲) زاویه‌ی تیم (Organization / Team)

  • مرز تیم‌ها کجاست؟ چه کسی مسئول کدام قسمت است؟
  • اگر یک ماژول بین چند تیم مشترک است، هر تغییری نیاز به هماهنگی بین چند تیم دارد؛ friction بالا می‌رود.
  • ماژولی که ownership روشن و یک‌تیمی دارد، معمولاً از نظر معماری هم مرز واضح‌تری پیدا می‌کند.

۳) زاویه‌ی داده (Data & Consistency)

  • هر domain باید داده‌ی خودش را مالک باشد؛
  • foreign key بین domainها، view مشترک، trigger‌هایی که روی چند domain کار می‌کنند، همه نشانه‌ی مرزبندی ضعیف‌اند.
  • هر جا نیاز به distributed transaction داری، احتمالاً مرزهایت را طوری کشیده‌ای که consistency را سخت کرده‌ای.

نویسنده‌ها در واقع می‌گویند:
«ماژول خوب جایی است که تغییر، تیم و داده تا حد خوبی هم‌راستا باشند.» - ۳) ضدالگوی مهم: Shared Everything-

چیزهایی که کتاب خیلی رویشان حساس است:

  • دیتابیس مشترک بین چند سرویس، به‌خصوص با:
    • join بین جدول‌های چند domain،
    • foreign key cross-domain،
    • triggerهایی که چند domain را درگیر می‌کنند.
  • کتابخانه‌های business logic مشترک که در چند سرویس استفاده‌ی سنگین دارند (نه صرفاً shared kernel خیلی کوچک و پایدار).
  • مدل‌های مشترک (Entity / DTO) که بین سرویس‌ها کپی/اشتراک داده شده‌اند و هر تغییری در یک جا، سرویس‌های دیگر را می‌شکند.

مشکل این‌ها چیست؟

  • coupling پنهان ایجاد می‌کنند.
  • هر تغییری در یک domain، ممکن است چند سرویس دیگر را از کار بیندازد.
  • تست isolated سخت می‌شود، چون ماژول‌ها واقعاً مستقل نیستند، فقط ظاهراً جدا هستند.

کتاب تأکید دارد:
shared DB و shared library باید تصمیم معماریِ آگاهانه و محدود باشد، نه راه‌حل تنبلی و سریع. - ۴) Decomposition باید بر اساس رفتار واقعی سیستم باشد، نه فقط دیاگرام-

نویسنده‌ها هشدار مهمی می‌دهند:

  • اگر فقط با مدل مفهومی و دیاگرام روی وایت‌برد (مثلاً DDD context map) تصمیم بگیری سیستم را چطور بشکنی،
  • اما رفتار واقعی سیستم را نبینی (در runtime و در history کد)، احتمال خطای بالا داری.

آن‌ها پیشنهاد می‌کنند از شواهد واقعی کمک بگیری:

  • لاگ و tracing: کدام سرویس‌ها در یک request chain، با هم بارها درگیر می‌شوند؟
  • تاریخچه‌ی گیت: کدام فایل‌ها تقریباً همیشه در یک commit با هم تغییر می‌کنند؟
  • dependency graph واقعی (چه پکیج‌ها/ماژول‌هایی از هم استفاده می‌کنند؟)

با این شواهد، مرزهای واقعی را کشف می‌کنی، بعد با مدل مفهومی (bounded context و غیره) تطبیق می‌دهی و refine می‌کنی.

۵) نکته‌ی بالغ: ماژولاریتی ثابت نیست-

یک نکته خیلی مهم در کتاب:

  • ساختار ایده‌آل امروز، ممکن است شش ماه دیگر اشتباه باشد، چون:
    • تیم‌ها تغییر کرده‌اند،
    • نیازمندی‌ها عوض شده،
    • نقاط فشار (traffic / SLA / KPI) تغییر کرده.

پس:

  • Decomposition یک تصمیمِ یک‌بار برای همیشه نیست.
  • معماری باید قابل تکامل باشد؛
  • هر از چندگاهی باید مرزها را بازبینی کنی:
    • کدام سرویس‌ها را باید merge کنیم؟
    • کدام domain بزرگ شده و باید split شود؟
    • sharedها را باید کم‌تر کنیم یا جابجا؟

کتاب از این دید، معماری را فرآیند مستمر تصمیم‌گیری می‌داند، نه طراحی یک نقشه‌ی «همیشه درست».

۶) برداشت عملی برای تو به‌عنوان TL-

پیامی که این بخش برای یک TL/معمار مثل تو دارد:

  • “Service = Project” در solution را فراموش کن؛ این فقط یک جزئیات فنی است.
  • “Bounded Context” را هم خشک و کتابی پیاده نکن؛ با داده، تیم و رفتار واقعی validate کن.
  • وقتی shared DB یا shared library راه می‌اندازی، بدان که داری روی مرز کوآنتوم و استقلال سیستم قمار می‌کنی؛ آگاهانه و با trade-off تصمیم بگیر، نه از روی راحتی.


– ادامه: تفکیک بر اساس Change, Team, Data در عمل–

در ادامه همین بحث، نویسنده‌ها از حالت نظری فاصله می‌گیرن و می‌رن سراغ این که:

«حالا با همین سه محور تغییر، تیم و داده، واقعا یک سیستم رو چطور تکه‌تکه کنیم؟»

ایده اینه که:

  • این سه محور رو جداجدا نگاه نکن،
  • جایی خوب عمل کردی که این سه تا بیشترین هم‌پوشانی رو پیدا کنن.

بیایم هر محور رو عملی‌تر و نزدیک به فضای کاری خودت ببینیم.

۱) محور Change – از گیت شروع کن، نه از کلاس‌ها

کتاب روی این خیلی تأکید داره:

  • اگر می‌خوای بفهمی ماژول‌ها کجا باید جدا یا ادغام بشن، به این نگاه کن که «چه چیزهایی با هم تغییر می‌کنند»، نه این‌که «الان پروژه‌ها چطوری تقسیم شده‌اند».

این یعنی:

  • تاریخچه گیت (یا هر VCS) بهترین داده‌ی واقعی توست.
  • اگر فایل‌ها/ماژول‌های A و B مرتباً در یک commit دستکاری می‌شن:
    • یعنی از نظر تغییر، به هم چسبیده‌اند.
    • این یک سیگنال قوی است که یا:
      • باید در یک ماژول/کوآنتوم قرار بگیرند،
      • یا API و مرزشون درست طراحی نشده (همیشه مجبوری دست ببری تو هر دو سمت).

برعکس:

  • اگر یک زیرسیستم بسیار کم تغییر می‌کنه (مثلا یک ماژول محاسبه مالیات با قواعد ثابت)،
  • و ماژول‌های دیگر مرتبا تغییر می‌کنند،
  • احتمالاً خوبه اون بخش پایدار از بقیه جدا بشه تا هر release بزرگ، اون را هم بی‌دلیل درگیر نکنه.

نویسنده‌ها عملاً می‌گن:
«change coupling» رو جدی بگیر؛ این یکی از قوی‌ترین سیگنال‌ها برای طراحی یا اصلاح مرزهای معماریه.

۲) محور Team – Conway’s Law به شکل عملی

کتاب به‌طور مستقیم (یا غیرمستقیم) به قانون کانوی اشاره داره:
ساختار سیستم‌ها بازتاب ساختار تیم‌هاست.

ترجمه عملی:

  • اگر برای یک بخش مهم سیستم، چند تیم مختلف باید هماهنگ شن تا تغییری اعمال شه، معماری تو با سازمانت هم‌راستا نیست.
  • مرزهای معماری ایدئال اون‌هایی هستن که:
    • یک domain / ماژول / سرویس، یک مالک اصلی (owner) واضح داشته باشه.
    • تیم‌ها بتونن روی ماژول خودشون کاور end-to-end داشته باشن، نه فقط یک لایه‌ی نازک UI یا DAL.

بنابراین:

  • وقتی می‌خوای سیستم رو بشکنی:
    • حواست به ساختار تیم‌ها و مالکیت‌ها باشه،
    • ماژولای معماری رو طوری انتخاب کن که تیم‌ها تا حد ممکن مستقل بتونن روی ماژول خودشون تحلیل، توسعه، تست و دیپلوی کنن.

اگر برعکس عمل کنی:

  • معماری روی کاغذ قشنگه، ولی هر تغییر کوچک می‌شه جلسه و هماهنگی و dependency جهنمی بین تیم‌ها.

۳) محور Data – Ownership و مرزهای Consistency

کتاب بعد برمی‌گرده روی داده، چون قبلاً هم گفته بود «داده مرکز سختی معماریه».

چند اصل کلیدی:

  • هر domain باید data خودش رو مالک باشه:
    • یعنی فقط اون domain حق write به دیتای خودش رو داشته باشه.
  • اگر چند domain به یک schema / جدول / دیتابیس مشترک وصل می‌شن و cross-domain foreign key دارن:
    • تو داری مرز domain رو در دیتابیس می‌شکنی،
    • consistency و migration رو جهنمی می‌کنی.

نویسنده‌ها روی این الگوی ذهنی تاکید دارن:

  • ارتباط بین domainها باید از طریق:
    • API / event / message انجام بشه،
    • نه از طریق join مستقیم در دیتابیس مشترک.

هم‌چنین:

  • هرجا نیاز به تراکنش توزیع‌شده (۲PC یا Saga خیلی پیچیده) حس می‌کنی،
  • این یک زنگ خطره که شاید:
    • مرز داده‌ها درست کشیده نشده،
    • یا domainهایی که باید با هم باشن، به شکل مصنوعی جدا شدن.

۴) هم‌راستا کردن Change + Team + Data

نقطه اوج این بخش کتاب اینه:

  • اگر بتونی جایی پیدا کنی که:
    • data آنجا ownership مشخصی داره،
    • change الگوهای تقریبا مشترکی داره،
    • و یک تیم مشخص مالک اون بخشه،
  • تو به یک «کاندید خوب» برای bounded context / ماژول / کوآنتوم رسیده‌ای.

برعکس:

  • اگر این سه تا از هم گسسته باشن:
    • data مشترک بین چند domain / تیم،
    • change پراکنده،
    • ownership مبهم،
  • معماری‌ات دائم friction تولید می‌کنه.

کتاب می‌خواد معمار رو از این حالت خارج کنه که صرفا به “نام‌گذاری سرویس‌ها” فکر کنه؛ بلکه بره سراغ هم‌راستاکردن این سه محور.

۵) کاربرد عملی برای معمار

نتیجه‌ی طبیعی این بخش برای تو به‌عنوان معمار:

  • اگر الان بخوای سیستم رو ارزیابی کنی:
    • به‌جای این‌که بپرسی «چند تا microservice داریم؟»،
    • بپرس:
      • data کجاها shared و مبهمه؟
      • کجاها تغییرات کد همیشه دو–سه ماژول رو با هم درگیر می‌کنه؟
      • کجاها تیم‌ها برای هر تغییر کوچک باید با تیم‌های دیگر sync بشن؟
  • این سؤال‌ها مستقیم تو را به نقاطی می‌بره که:
    • باید ماژول‌ها رو merge کنی،
    • یا برعکس یک دامنه‌ی خیلی شلوغ رو split کنی.

کتاب این‌جا هنوز نسخه نمی‌ده «دقیقاً این‌طور split کن»،
بیشتر داره ابزار ذهنی می‌ده برای این‌که خودت از دل context واقعی پروژه‌ات مرز درست رو دربیاری.



ایده‌ی بعدی: «کِی مانولیت، کِی سرویس‌محور، کِی میکروسرویس؟»

نویسنده‌ها می‌گویند قبل از انتخاب سبک معماری، باید این سؤال را شفاف جواب بدهی:

«کوآنتوم‌هایت چند تا هستند و چه ویژگی‌ای دارند؟»

۱. معماری مانولیتی (یک کوآنتوم)

وقتی:

  • کل سیستم یک دیتابیس، یک runtime، یک دیپلوی دارد،
  • بیشتر تغییرات cross-cutting است و تیم‌ها روی کل کد کار می‌کنند،
  • نیاز به scale مستقل ماژول‌ها هنوز جدی نیست،

در عمل تنها یک کوآنتوم داری، حتی اگر لایه‌ها را جدا کرده باشی.

کتاب این‌جا «مانولیت خوب» را بد نمی‌داند؛ می‌گوید:

  • اگر مرزهای داخلی‌ات (ماژول‌بندی داخل مانولیت) خوب باشد،
  • و نیاز سازمان scale و استقلال دیپلوی نباشد،
    مانولیت می‌تواند بهترین انتخاب باشد، مخصوصاً در شروع.

۲. Service-Based / Modular Monolith (یک کوآنتوم، مرزهای داخلی بهتر)

مرحله‌ی بعد:

  • هنوز دیپلوی یکی است (یک کوآنتوم)،
    ولی
  • داخلش ماژول‌های واضح، شبیه سرویس، با مرزهای مشخص data و مسئولیت داری.

این حالت:

  • برای تیم‌هایی که می‌خواهند آینده‌اً به microservices فکر کنند،
    ولی فعلاً پیچیدگی توزیع را نمی‌خواهند، مناسب است.
  • اگر بعداً نیاز شد، می‌توان بعضی ماژول‌ها را از درون مانولیت بیرون کشید و کوآنتوم‌های جدید ساخت.

۳. Microservices (چند کوآنتوم مستقل)

وقتی:

  • چند ماژول داری که:
    • دیتابیس و resource مستقل دارند،
    • چرخه تغییر مستقل دارند،
    • تیم‌های جدا مالکشان هستند،
  • و واقعاً می‌توانی آن‌ها را جدا دیپلوی و scale کنی،

آن‌وقت چند کوآنتوم واقعی داری و microservices معنی پیدا می‌کند.

کتاب هشدار می‌دهد:

  • اگر دیتابیس مشترک، shared library سنگین یا runtime واحد داری،
  • احتمالاً هنوز فقط ظاهر microservice داری، ولی کوآنتوم واقعی نه؛
    و مشکلاتت از مانولیت هم بیشتر می‌شود.


ادامه: انتخاب سبک معماری بر اساس «تعداد و نوع کوآنتوم‌ها»

در این بخش کتاب، نویسندگان به‌جای شعار دادن درباره «میکروسرویس خوب است یا بد»، یک چارچوب تصمیم‌گیری می‌سازند:

معماری مناسب = تابعی از تعداد کوآنتوم‌ها و نیازهای تغییر/داده/تیم.

۱. وقتی فقط «یک کوآنتوم» داری چه کن؟

اگر عملاً:

  • یک دیتابیس مرکزی داری،
  • همه چیز با هم دیپلوی می‌شود،
  • برای هر فیچر، چندین ماژول در کل سیستم تغییر می‌کنند،
  • و تیم‌ها روی کل کد دخیل‌اند،

کتاب می‌گوید:

  • تو واقعاً فقط یک کوآنتوم داری.
  • این‌جا تلاش برای میکروسرویس‌سازی تهاجمی معمولاً فقط پیچیدگی و ناکامی می‌آورد.

در این حالت، کتاب توصیه می‌کند:

  • به‌جای تلاش برای جدا کردن سرویس‌ها در سطح زیرساخت،
    روی ماژولار کردن داخل مانولیت تمرکز کن:
    • مرزهای منطقی،
    • جداسازی پکیج‌ها/لایه‌ها،
    • کنترل دسترسی (clear API درون مانولیت).

این می‌شود چیزی که بعدها به آن «modular monolith» خواهیم گفت.

۲. وقتی «چند کوآنتوم» طبیعی داری

اگر بر اساس سه محور (Change, Team, Data) می‌بینی:

  • یک بخش از سیستم:
    • دیتای خودش را دارد،
    • تیم خودش را دارد،
    • الگوی تغییر مستقل خودش را دارد،
  • و می‌خواهی:
    • آن را جدا scale کنی،
    • جدا deploy کنی،
    • SLA متفاوت بدهی،

کتاب می‌گوید:

  • این یک کاندید طبیعی برای کوآنتوم جداست.
  • در این‌جا سرویس مستقل (و در ادامه، شاید microservice) معنی پیدا می‌کند.

ولی باز تأکید:

  • مهم «مستقل بودن دیپلوی و داده» است، نه صرفاً جدا بودن پروژه‌ها.

۳. طیف معماری از مانولیت تا مایکروسرویس

کتاب این را به‌صورت یک طیف می‌بیند، نه دو حالت صفر و یک:

  1. Monolith ساده
    • یک کوآنتوم، ماژولاریتی ضعیف/متوسط.
  2. Modular Monolith / Service-Based
    • هنوز یک کوآنتوم (یک دیپلوی)،
    • ولی مرزهای داخلی خوب، لایه‌های واضح، ماژول‌های domain‌محور.
  3. چند کوآنتوم محدود
    • بعضی بخش‌ها، به‌خاطر نیازهای خاص (مثلاً گزارش‌گیری حجیم، پردازش آسنکرون سنگین، یا ماژول خیلی مستقل)، جدا شده‌اند.
  4. Microservices گسترده
    • تعداد زیادی کوآنتوم مستقل،
    • هر کدام دیتابیس و lifecycle خودشان را دارند،
    • تیم‌های متعدد و مستقل.

نکته کتاب:

  • همیشه لازم نیست تا Extreme طرف microservices بروی.
  • خیلی سیستم‌ها با ۲–۳ کوآنتوم مستقل + یک مانولیت/ماژولار مرکزی به نقطه تعادل خوبی می‌رسند.

۴. عامل «توان تیم و سازمان»

کتاب فقط فنی نگاه نمی‌کند:

  • اگر تیم/سازمان:
    • توان DevOps قوی ندارد،
    • observability و مانیتورینگ بالغ ندارد،
    • تجربه کار با distributed system را ندارد،

پرتاب کردن آن‌ها به دنیای ده‌ها سرویس مستقل، عملاً تبدیل به هرج‌ومرج می‌شود.

پس تصمیم معماری باید:

  • توان تحویل‌دادن و نگه‌داشتن را هم لحاظ کند، نه فقط زیبایی فنی.

۵. معماری به‌عنوان «تصمیم آگاهانه بین گزینه‌ها»

جمع‌بندی این بخش:

  • قبل از اینکه بگویی «می‌رویم میکروسرویس»، باید:
    • تعداد و نوع کوآنتوم‌هایت را بشناسی؛
    • سه محور Change/Team/Data را تحلیل کنی؛
    • توان سازمان و نیازهای عملی (SLA، سرعت تغییر، امنیت،…) را بشناسی.
  • معماری خوب، یعنی:
    • انتخاب شفاف یکی از نقاط این طیف،
    • و مستندسازی دلایل آن (در قالب ADR و…)،
    • تا بعداً هم خودت و هم بقیه بدانند چرا این شکل انتخاب شده است.


نویسنده‌ها می‌گویند:

  • بیشتر تصمیمات معماری، در نهایت به یک چیز برمی‌گردند:
    «داده کجاست، مال کیست، و چطور تغییر می‌کند؟»

پس، حالا که:

  • کوآنتوم‌ها را می‌شناسی،
  • سبک معماری را آگاهانه‌تر می‌بینی،

باید سوال اصلی را روی داده متمرکز کنی.

چند محور کلیدی که کتاب شروع به باز کردن‌شان می‌کند:

۱) مالکیت داده (Data Ownership) جدی‌تر از آنی است که به نظر می‌رسد

ایده‌ی محوری:

  • هر کوآنتوم (هر domain / سرویس / ماژول مهم) باید:
    • “مالک” داده‌ی خودش باشد.
    • یعنی:
      • فقط خودش حق write به این داده را داشته باشد.
      • بقیه اگر نیاز دارند، باید از طریق API / event / contract با او صحبت کنند، نه مستقیماً به DB سر بزنند.

کتاب این‌جا روی این نقطه فشار می‌دهد:

  • shared database که چند سرویس به‌طور مستقیم در آن جدول‌های همدیگر را می‌خوانند/می‌نویسند،
    معماری را به مانولیت پنهان تبدیل می‌کند، حتی اگر سرویس‌ها جدا deploy شوند.

در مقابل، «مالکیت روشن» یعنی:

  • جدول X فقط زیر domain/سرویس A است.
  • سرویس B اگر چیزی درباره X می‌خواهد:
    • یا از A می‌پرسد (API call / async message)،
    • یا یک نسخه read مدل خودش را نگه می‌دارد (replicated / denormalized data).

۲) تغییر دید از مدل داده‌ی مشترک به مدل‌های داده‌ی محلی

کتاب در این بخش شروع می‌کند به شکستن یک عادت بد:

  • این که یک «مدل داده‌ی مشترک» برای کل سیستم تعریف کنیم (مثلاً یک schema عظیم یا یک مجموعه entity مشترک)
    و همه‌جا از آن استفاده کنیم.

مشکل این ذهنیت:

  • هر تغییری در مدل مشترک، موجی از تغییر در همه‌جا ایجاد می‌کند → coupling شدید.
  • domainهای مختلف نیازهای متفاوتی از داده دارند، و اگر همه را در یک مدل واحد زور بدهی، مدل پیچیده و شکننده می‌شود.

در عوض، نویسنده‌ها این الگو را پیشنهاد می‌کنند:

  • هر domain / سرویس، مدل داده‌ی خودش را دارد (حتی اگر مفهومی مشترک مثل Customer در چند جا وجود داشته باشد).
  • شاید «Customer» در billing یک نمای متفاوت (فقط id و billing info) داشته باشد تا در marketing (id + preferences).
  • اشتراک، در سطح مفهوم است، نه در سطح schema یا entity class واحد.

۳) هم‌زمانی (Concurrency) و Consistency در داده توزیع‌شده

این‌جا کتاب دوباره برمی‌گردد به بحث ACID vs BASE، ولی از زاویه داده:

  • در یک مانولیت با DB واحد:
    • تراکنش‌های ACID با یک transaction manager کار را انجام می‌دهد.
  • در معماری توزیع‌شده،
    داده در چند کوآنتوم / سرویس پراکنده است؛
    دیگر نمی‌توانی “یک تراکنش جهانی” ساده داشته باشی.

پس مجبور می‌شوی:

  • یا بعضی جاها atomicity را رها کنی و eventual consistency را قبول کنی؛
  • یا با الگوهایی مثل Saga و compensating transaction این توزیع‌شدگی را مدیریت کنی.

کتاب تاکید می‌کند که:

  • هر تصمیم درباره decomposition سرویس‌ها و انتخاب سبک معماری، برای داده و consistency هزینه دارد.
  • معماری یعنی فهمیدن و پذیرفتن همین trade-off، نه فرار از آن.

۴) Data as a First-Class Architectural Concern

نویسنده‌ها می‌گویند:

  • خیلی از تیم‌ها معماری را فقط حول سرویس‌ها و APIهایشان می‌کشند،
  • و داده را فقط به‌عنوان “implementation detail” DB می‌بینند؛
  • این خطرناک است.

در دید کتاب:

  • data model و data ownership،
    باید از اول و در همان سطح تصمیمات معماری (مثل سبک معماری، کوآنتوم‌ها، deployment) لحاظ شوند.
  • یعنی:
    • در ADRها باید «تصمیمات درباره مالکیت داده و نحوه دسترسی دیگران» ثبت شوند.
    • Fitness Functionها برای بررسی نقض این مرزها (مثلاً سرویس‌های غیرمجاز که به schema دیگران وصل می‌شوند) تعریف شوند.


۱. معماری به‌عنوان تکامل، نه پروژه بازنویسی

نویسنده‌ها تأکید می‌کنند:

  • بازنویسی کامل سیستم (Big Bang Rewrite) تقریباً همیشه پرریسک، پرهزینه و در عمل شکست‌خورده است.
  • معماری باید «تکاملی» (evolutionary) باشد:
    • یعنی ساختار سیستم در طول زمان، با گام‌های کنترل‌شده عوض شود.
    • هر گام کوچک، یک بهبود معماری و یک تصمیم قابل‌ردیابی است، نه انقلاب کامل.

این نگاه مستقیم به همان ایده‌ی ADR برمی‌گردد:
هر گام بزرگ معماری = یک سری تصمیم مستند، نه یک‌باره همه‌چیز را عوض کردن.

۲. نقطه شروع مهاجرت: فهم کوآنتوم فعلی

قبل از اینکه چیزی را «بیرون بکشی»، باید واقعاً بفهمی:

  • الآن چند کوآنتوم داری؟
  • مرزهای واقعی‌شان کجاست؟
  • coupling ایستا (کد، دیتابیس، زیرساخت) و پویا (کال‌ها، eventها) چه شکلی است؟

کتاب تأکید می‌کند:

  • اگر ندانی الآن چه داری، هر گونه plan برای مهاجرت، حدسی و خطرناک است.
  • این‌جا تحلیل:
    • dependency code
    • دیتابیس (schemaها، foreign keyها، joinها)
    • و جریان‌های اصلی بیزینس (workflowهای مهم) بخش ضروری کار است.

۳. معیار انتخاب «اولین کاندید جداسازی»

کتاب برای اینکه مهاجرت عملی شود، پیشنهاد می‌کند:

  • به‌جای این‌که بگویی “همه‌چیز را میکروسرویس می‌کنیم”، یک یا چند کاندید انتخاب کن که:
    • مرز دامین نسبتاً واضحی دارند،
    • Data Ownership قابل تعریف است،
    • به‌لحاظ بیزینسی، جدا شدنشان ارزش دارد (مثلاً:
      • یا شدیداً تغییرپذیرند،
      • یا SLA/scale متفاوت دارند،
      • یا نیاز به تکنولوژی متفاوت دارند).

این بخش روی یک اصل می‌چرخد:

«اول ماژولی را جدا کن که هم ریسک پایین‌تری دارد، هم سود ملموس‌تری.»

۴. دو نوع حرکت: جداسازی دامین یا جداسازی قابلیت فنی

نویسنده‌ها دو سناریو را از هم تفکیک می‌کنند:

1) جداسازی بر اساس domain (مثلاً ماژول پرداخت، ماژول گزارش، ماژول کاتالوگ محصول):

  • این‌جا اصل، مرز دامین و داده است.
  • بعد از جدا شدن، این دامین باید دیتابیس و API خودش را داشته باشد.

2) جداسازی بر اساس concern فنی (مثلاً authentication، logging، گزارش‌گیری سنگین):

  • این‌جا ممکن است دامین‌های مختلف از این قابلیت استفاده کنند،
  • اما خود این بخش می‌تواند یک کوآنتوم مستقل شود (مثلاً سرویس گزارش‌گیری read-only که از snapshot داده تغذیه می‌شود).

نکته مهم کتاب:

  • هر دو نوع جداسازی ممکن است مفید باشند، اما باید از نظر کوپلینگ و داده با دقت طراحی شوند که دوباره shared monster نسازند.

۵. الگوی کلی گام‌های مهاجرت

بدون رفتن به جزئیات پیاده‌سازی (که در فصل‌های بعدی و مثال‌ها باز می‌شود)، این بخش روی این الگوی ذهنی تأکید دارد:

۱) فهم سیستم فعلی (کوآنتوم‌ها، coupling، داده، تیم‌ها).
۲) انتخاب یک بخش با مرز نسبتاً واضح و ارزش جدا شدن.
۳) تعریف target state برای آن بخش:

  • data ownership
  • API / قراردادها
  • نیازهای SLA/scale
    ۴) طراحی گام‌های کوچک برای رسیدن به آن:
  • مثلاً:
    • اول لایه API دامین را تمیز کن،
    • بعد دیتای آن را از بقیه جدا کن (schema یا DB جدا)،
    • بعد dependencyهای کد را کاهش بده،
    • و در نهایت آن را به کوآنتوم واقعی تبدیل کن (دیپلوی مستقل).
      ۵) در هر گام:
  • تصمیمات معماری را در ADR ثبت کن،
  • و fitness functionهایی برای کنترل سلامت معماری تعریف کن (مثلاً:
    • «بعد از گام X، هیچ سرویس دیگری نباید مستقیم جدول Y را بخواند»).

۶. چک‌پوینت ذهنی معمار

پیامی که این بخش می‌خواهد در ذهن معمار بنشاند این است که:

  • «معماری خوب» نه فقط انتخاب سبک و ترسیم دیاگرام، بلکه طراحی مسیر تکامل است.
  • معمار باید:
    • وضعیت فعلی را بشناسد،
    • نقطه‌ی هدف را بداند،
    • و مسیر را به گام‌های کوچک و قابل برگشت تقسیم کند.


ورود به فصل «معماری داده‌محور» (Data-Centric Architecture Decisions)

حالا که کتاب نشان داده معماری بدون فهم کوآنتوم و سبک کلی ناقص است، تمرکز را می‌گذارد روی سخت‌ترین بخش: تصمیمات معماری درباره داده.

در این بخش، چند محور اصلی مطرح می‌شود:

۱. تصمیمات مهمِ معماریِ داده

نویسنده‌ها چند نوع تصمیم «سخت و معماری‌سطح» را برای داده فهرست می‌کنند؛ چیزهایی که اگر اشتباه گرفته شوند، بعداً اصلاحشان بسیار گران است:

  1. الگوی کلی داده
    • دیتابیس متمرکز یا توزیع‌شده؟
    • relational، NoSQL، ترکیبی؟
    • event-sourced یا state-based؟
  2. مرزبندی domainهای داده
    • هر داده زیر کدام bounded context / سرویس است؟
    • چه کسی مالک write است؟ چه کسانی فقط read دارند؟
  3. استراتژی اشتراک داده بین کوآنتوم‌ها
    • API synchronous
    • پیام / event asynchronous
    • replication / caching / viewهای read-only
  4. الگوی consistency
    • کجا ACID (محلی، درون domain)
    • کجا eventual consistency (بین domainها)

این‌ها از نظر نویسندگان، تصمیمات «معماری» هستند، نه صرفاً «طراحی دیتابیس».

۲. دسته‌بندی الگوهای تعامل داده بین سرویس‌ها

کتاب چند الگوی کلی برای اینکه سرویس‌ها چطور به داده همدیگر دسترسی پیدا کنند معرفی می‌کند. (ایده‌ها را ساده‌سازی می‌کنم، اسم‌گذاری ممکن است در فصل‌های بعد دقیق‌تر شود):

  1. Shared Database
    • چند سرویس مستقیماً به یک دیتابیس/schema مشترک وصل می‌شوند.
    • مزیت: ساده در شروع.
    • عیب: coupling شدید، سختی migration، نقض ownership.
  2. Database per Service (با API / Event)
    • هر سرویس دیتای خودش را دارد.
    • سرویس‌های دیگر فقط از طریق API یا پیام‌ها، داده او را می‌گیرند.
    • مزیت: استقلال، مقیاس‌پذیری.
    • عیب: پیچیدگی consistency و replication.
  3. Data Domain Shared، ولی با قرارداد سخت
    • چند سرویس به یک data domain دسترسی دارند،
    • اما یک لایه‌ی domain API یا data-access رسمی جلوی آن است، نه اتصال مستقیم همه به DB.
    • این حالتی بین shared DB خام و DB per service است.

کتاب می‌خواهد تو را از shared DB بی‌قانون، به سمت مدل‌های ۲ و ۳ (با مرز و قرارداد) هل بدهد.

۳. چه زمانی Shared Database هنوز «قابل قبول» است؟

نویسنده‌ها واقع‌گرا هستند:

  • گاهی در سیستم‌های legacy یا تیم‌های کوچک، shared DB اجتناب‌ناپذیر است.
  • اما:

    • باید آن را به‌عنوان «بدهی معماری» ببینی،
    • برایش قواعد صریح تعریف کنی، مثلاً:
      • فقط یک سرویس حق write به فلان جدول را دارد،
      • سایر سرویس‌ها فقط read، آن هم از طریق view یا API،
      • هیچ foreign key cross-domain جدیدی اضافه نمی‌شود.
  • و در ADR ثبت کنی که:
    • چرا فعلاً shared مانده،
    • و برنامه‌ی خروج از آن چیست (در صورت نیاز).

۴. داده‌ی «منبع حقیقت» (Source of Truth) در هر domain

کتاب تأکید می‌کند روی مفهوم:

Source of Truth (SoT) برای هر بخش داده:

  • برای هر مفهوم مهم (مثلاً Customer, Order, Inventory)
    باید دقیقاً معلوم باشد:

    • «سرویس/دومِینی که منبع حقیقت این داده است، کدام است؟»
    • سایر سرویس‌ها اگر نسخه‌ای دارند، نسخه‌ی مشتق شده (cache, projection, read model) است.

پیام مهم:

  • اگر دو جا هم‌زمان «فکر می‌کنند» مالک داده‌ی واحدی هستند،
    معماری داده‌ات دیر یا زود مشکل consistency جدی می‌خورد.

۵. نقش Event در اشتراک داده

کتاب در ادامه وارد این جهت‌گیری کلی می‌شود:

  • برای اشتراک داده بین domainها،
    Event و پیام ابزار بسیار مهمی هستند:

    • domain مالک، هنگام تغییر داده، event منتشر می‌کند،
    • سرویس‌های دیگر نسخه‌ی خودشان را به‌روزرسانی یا واکنش مناسب را اعمال می‌کنند.

اما:

  • این الگو eventual consistency را وارد بازی می‌کند؛
    باید بپذیری:
    • بین زمان تغییر در SoT و زمان به‌روزرسانی در consume‌کننده، تأخیر وجود دارد،
    • گاهی باید compensating logic بنویسی.

این بخش هنوز وارد جزئیات event modeling نشده، فقط ذهن را آماده می‌کند که:

«اشتراک داده بین کوآنتوم‌ها = احتمالاً event-driven و eventual».

۶. نتیجه ذهنی این بخش برای معمار

چیزی که نویسنده‌ها می‌خواهند در ذهن تو تثبیت کنند:

  • تصمیم‌های اصلی معماری داده را از الان واضح کن:
    • کجا shared DB داریم و چرا؟
    • کجا هر سرویس data store خودش را دارد؟
    • مالکیت هر domain داده با کیست؟
    • SoT هر داده مهم کجاست؟
    • الگوی اشتراک داده چیست (API, Sync vs Async, Events, Replication)؟
  • و این‌ها را تصادفی رها نکن،
    بلکه به‌صورت تصمیمات ثبت‌شده و قابل دفاع.


ادامه: استراتژی‌های اشتراک داده بین سرویس‌ها (در خود کتاب)

بعد از تعریف مالکیت داده و Source of Truth، کتاب وارد این می‌شود که:

«وقتی یک سرویس به داده سرویس دیگر نیاز دارد، چه راه‌هایی داریم و trade-off هرکدام چیست؟»

۱. خواندن مستقیم از سرویس مالک (Sync API Call)

الگو:

  • سرویس A مالک داده است.
  • سرویس B هر وقت نیاز دارد، یک call همزمان (REST/gRPC و …) به A می‌زند و داده را می‌گیرد.

مزایا:

  • داده همیشه تازه است.
  • مالکیت روشن می‌ماند؛ A تنها منبع حقیقت است.

معایب:

  • coupling پویا: availability و latency سرویس B وابسته به A است.
  • اگر chain طولانی شود، پایدار کردن کل workflow سخت می‌شود.

نویسنده‌ها این را برای:

  • اطلاعات حیاتی و کم‌حجم،
  • و سناریوهایی که latency و availability قابل تحمل است، مناسب می‌دانند.

۲. نگه داشتن کپی محلی از داده (Replication / Cache / Read Model)

الگو:

  • سرویس A مالک است و event (یا پیام) انتشار می‌دهد وقتی داده تغییر می‌کند.
  • سرویس B نسخه‌ی read-only خودش را نگه می‌دارد (projection / cache / read model).

مزایا:

  • B برای خواندن به A وابسته نیست؛ availability و performance بهتر می‌شود.
  • chain‌های طولانی sync کاهش می‌یابند.

معایب:

  • eventual consistency: داده‌ی B ممکن است لحظاتی از A عقب‌تر باشد.
  • نیاز به logic برای sync، جبران خطا، و پاک‌سازی cache.

کتاب می‌گوید این الگو:

  • برای سناریوهای read-heavy و حساس به latency، بسیار مهم است،
  • اما باید آگاهانه طراحی شود (نه random caching).

۳. انتقال مسئولیت داده (Refactor Ownership)

الگو:

  • گاهی چند سرویس از یک داده استفاده می‌کنند چون ownership اشتباه است.
  • کتاب پیشنهاد می‌کند به‌جای اضافه کردن call یا replication بی‌پایان،
    مالکیت را عوض کنی:
    • مثلاً بعضی فیلدها اصلاً باید به دامین/سرویس دیگر منتقل شوند.
    • یا یک مفهوم داده (Entity) باید split شود و هر بخش به یک domain منتقل شود.

این، همان «تکامل مدل داده» است که اگر انجام نشود،
الگوهای قبلی (API و replication) فقط مسکن موقت‌اند.



۱. تضاد دائمی: «نرمال‌سازی دیتابیس» در مقابل «نیاز معماری توزیع‌شده»

کتاب می‌گوید:

  • طراحی دیتابیس در دنیا‌ی مانولیت معمولاً حول نرمال‌سازی و حذف افزونگی می‌چرخد.
  • اما در معماری توزیع‌شده:
    • حذف افزونگی همیشه مطلوب نیست؛
    • گاهی عمداً باید داده را تکثیر (denormalize / replicate) کنیم تا:
      • coupling کمتر شود،
      • سرویس‌ها مستقل شوند،
      • latency و availability بهتر شود.

نتیجه:

  • «درست» بودن طراحی دیتابیس دیگر فقط یعنی BCNF و 3NF نیست؛
  • باید بین «پاکی مدل داده» و «استقلال معماری» تعادل برقرار کنی.

۲. دو نوع coupling داده‌ای که باید ببینی

نویسنده‌ها روی دو نوع coupling در سطح داده تأکید می‌کنند:

  1. Schema Coupling
    • وقتی چند سرویس مستقیماً به یک schema/جدول وصل هستند.
    • هر تغییر در schema، چند سرویس را می‌شکند.
    • این، نشانه shared DB بد است.
  2. Semantic Coupling
    • وقتی چند سرویس وابسته به معنی مشترک داده‌اند (مثلاً مفهوم Customer یا Order)،
      اما هرکدام نمای خودشان را دارند.
    • این نوع coupling اجتناب‌ناپذیر است، ولی باید کنترل شود (با قرارداد، event، نسخه‌بندی).

کتاب می‌خواهد تو را از schema coupling به سمت semantic coupling کنترل‌شده ببرد.

۳. تله‌ی کلاسیک: Reporting و Analytics روی دیتابیس عملیاتی

الگوی متداول (و مشکل‌زا):

  • سیستم عملیاتی (OLTP) روی دیتابیس اصلی کار می‌کند،
  • گزارش‌گیری، BI، و Queryهای سنگین هم روی همان دیتابیس انجام می‌شود.

مشکلات:

  • performance کل سیستم تحت فشار قرار می‌گیرد،
  • مدل دیتای عملیاتی برای گزارش‌گیری مناسب نیست (joins پیچیده، schema پیچیده).

کتاب پیشنهاد کلاسیک ولی مهم را یادآوری می‌کند:

  • داده‌ی گزارش‌گیری را به data store مخصوص خودش منتقل کن:
    • data warehouse / data mart / read model جدا،
    • تغذیه شده از eventها، replication یا jobها.

از دید کتاب، این دقیقاً یک مثال از «کوآنتوم داده‌ای جدا» است:

  • سیستم عملیاتی یک کوآنتوم،
  • گزارش‌گیری کوآنتوم دیگر، با مدل داده متناسب خودش.

۴. Cost of Change روی داده

نویسندگان تاکید می‌کنند:

  • تغییر در مدل داده، گران‌ترین نوع تغییر است (از نظر ریسک، migration، downtime).
  • بنابراین:
    • تصمیمات معماری مربوط به داده، باید با نگاه بلندمدت گرفته شود.
    • هر «اتصال جدید» به دیتابیس مشترک، یک بدهی معماری تازه است.

این‌جا کتاب مجدد ADR را یادآوری می‌کند:

  • هر تصمیمی مثل:
    • «این سرویس هم مستقیم به این جدول وصل شود»
    • «این داده را replicate کنیم یا نه»
  • باید به‌عنوان یک Architectural Decision ثبت شود:
    • موضوع، گزینه‌ها، trade-offها، و برنامه‌ی خروج (اگر لازم شد) مشخص باشد.

۵. خلاصه ذهنی این قطعه از کتاب

نویسنده‌ها می‌خواهند این چند نکته در ذهن بماند:

  • طراحی داده در معماری توزیع‌شده = تعادل بین:
    • پاکی و نرمال‌سازی مدل،
    • استقلال و resiliency معماری.
  • schema coupling را باید به حداقل برسانی؛ semantic coupling را با قرارداد و event مدیریت کنی.
  • گزارش‌گیری و خواندن‌های سنگین را از دیتابیس عملیاتی جدا کن؛
    این، یک نمونه‌ی طبیعی از ایجاد یک کوآنتوم داده‌ای جدید است.
  • هر بار که پای shared DB، schema تغییر، یا replicate داده وسط است،
    عملاً داری تصمیم معماری می‌گیری؛ لازم است آگاهانه و مستند باشد.


بخش ۱: Database per Service در عمل – ایده، مزایا، دردسرها

کتاب حالا یکی از اصلی‌ترین شعارهای دنیای میکروسرویس را می‌گیرد زیر ذره‌بین:

“هر سرویس دیتابیس خودش را داشته باشد”

۱. ایده نظری

  • هر سرویس (یا هر کوآنتوم) صاحب data store خودش است.
  • هیچ سرویس دیگری مستقیماً به دیتابیس او وصل نمی‌شود.
  • تعامل بین سرویس‌ها فقط از طریق:
    • API (sync) یا
    • event/message (async) انجام می‌شود.

این دقیقاً همان چیزی است که استقلال کوآنتوم را ممکن می‌کند:

  • deploy مستقل،
  • scale مستقل،
  • schema evolve مستقل.

۲. مزایا از دید معماری

کتاب چند مزیت کلیدی را برجسته می‌کند:

  1. کاهش schema coupling
    • تغییر در schema یک سرویس فقط روی همان سرویس اثر دارد.
    • بقیه فقط قرارداد API/event را می‌بینند، نه جزئیات جداول.
  2. استقلال تغییر و دیپلوی
    • می‌توانی schema و منطق دیتایی یک سرویس را تغییر دهی،
      بدون این‌که نگران شکستن queryهای مستقیم سرویس‌های دیگر باشی.
  3. انتخاب تکنولوژی متناسب
    • یک سرویس می‌تواند SQL باشد،
    • دیگری document store،
    • دیگری time-series؛
      همه بر اساس نیاز خودش، نه تصمیم واحد سازمان.

از دید نقش Software Architect، این یعنی:

  • می‌توانی برای هر دامین، stack بهینه‌تری انتخاب کنی،
  • بدون این‌که درگیر «یک دیتابیس برای همه» باشی.

۳. دردسرها و trade-offهای جدی

کتاب این‌جا گلایه‌های واقعی را مطرح می‌کند:

  1. پیچیدگی consistency بین سرویس‌ها
    • joinهای cross-service دیگر وجود ندارند؛
    • باید با:
      • API callها،
      • read modelها،
      • eventها،
      • و گاهی سادگی بیزینس
        این نیازها را از نو طراحی کنی.
  2. گزارش‌گیری پیچیده‌تر
    • اگر قبلاً یک گزارش با چند join روی چند جدول در یک DB می‌گرفتی،
      حالا داده‌ات در چند سرویس پخش شده.
    • باید:
      • یا data pipeline به یک data store تحلیلی بسازی،
      • یا read model/aggregation مركزی (برای reporting) طراحی کنی.
  3. Migration از legacy shared DB
    • در سیستم موجود، به‌ندرت از صفر Database per Service داری.
    • باید تدریجاً:
      • schemaها را تکه کنی،
      • ownership را جابجا کنی،
      • و shared access را کم‌کم قطع کنی.
        کتاب تأکید می‌کند این فرایند زمان‌بر و نیازمند plan و ADR است.

برای تصمیم‌گیری در سیستم فعلی‌ات، باید دقیقاً این trade-offها را وزن کنی:

  • کجا ارزش این استقلال را دارد،
  • کجا فعلاً shared DB با قراردادهای سخت بهتر است.

بخش ۲: شکستن دیتابیس متمرکز به چند Data Domain – رویکرد تدریجی

حالا کتاب می‌رسد به یکی از سخت‌ترین و مهم‌ترین موضوعات برای تو:
«یک دیتابیس بزرگ متمرکز را چطور تبدیل به چند data domain / دیتابیس سرویس‌محور کنیم؟»

نویسنده‌ها یک رویکرد گام‌به‌گام پیشنهاد می‌کنند (مفهومی، نه با جزئیات ابزاری):

گام ۱: شناسایی Domainهای داده (Data Domains)

  • از دید بیزینس و معماری، جداول را خوشه‌بندی کن:
    • Customer / Identity
    • Order / Sales
    • Inventory / Product
    • Billing / Payments
    • و …

این کار بر اساس:

  • مفهوم بیزینسی،
  • تیم مالک،
  • و الگوی تغییر جداول انجام می‌شود.

نکته کتاب:

  • mapping ابتدایی‌ات کامل نخواهد بود،
    اما باید جایی شروع کنی.

گام ۲: جدا کردن Schema منطقی

در دیتابیس فعلی:

  • ابتدا مرز منطقی را با جدا کردن schemaها نزدیک به domainها مشخص کن:
    • مثلاً: Customer.*, Order.*, Inventory.*
  • Foreign Keyها و viewها را که cross-domain هستند شناسایی کن؛
    این‌ها couplingهای واضح‌اند.

این گام:

  • هنوز در همان DB physical هستی،
  • ولی مرز منطقی domainها را در سطح schema روشن‌تر کرده‌ای.

گام ۳: ایجاد لایه دسترسی رسمی برای هر Domain

کتاب می‌گوید:

  • قبل از جدا کردن فیزیکی،
    دسترسی را کنترل کن:

    • به‌جای اینکه هر سرویس مستقیم به جدول‌ها وصل شود،
      یک لایه data access/domain API برای هر domain تعریف کن.
    • از این به بعد:
      • سرویس‌ها فقط از طریق این لایه با داده domain حرف می‌زنند،
      • نه با اتصال مستقیم به همه جدول‌ها.

این لایه می‌تواند:

  • کتابخانه داخلی،
  • سرویس داخلی،
  • یا حتی module در همان مانولیت باشد.

اما مهم این است:

  • نقطه تماس با داده domain متمرکز شود.

گام ۴: کم‌کردن Cross-Domain FK و Join

حالا نوبت یکی از سخت‌ترین کارهاست:

  • Foreign Keyهایی که بین domainها تعریف شده‌اند را کم‌کم حذف یا شل کن:
    • به‌جای FK مستقیم، identifier ساده نگه دار (مثل CustomerId)
    • validation و consistency به سطح application/domain منتقل می‌شود.

برای گزارش‌گیری:

  • joinهای cross-domain را کم‌کم از OLTP به read models / reporting DB منتقل کن.

کتاب این گام را حیاتی می‌داند:

  • تا وقتی FK و joinهای cross-domain در قلب OLTP داری،
    هرگونه جداسازی فیزیکی پرریسک است.

گام ۵: جدا کردن فیزیکی Domainها به Data Storeهای مستقل

وقتی:

  • schema منطقی domainها جدا شد،
  • لایه دسترسی رسمی تعریف شد،
  • وابستگی‌های cross-domain در دیتابیس تا حد معقول کاهش یافت،

حالا می‌توانی:

  • domainها را به دیتابیس‌های مستقل منتقل کنی:
    • هر domain یک DB / instance جدا،
    • یا حتی نوع DB متفاوت (در صورت نیاز).

و چون:

  • سرویس‌ها دیگر مستقیم به DB وصل نمی‌شوند،
  • و cross-FKها را حذف کرده‌ای،
    این move فیزیکی نسبتاً کنترل‌شده خواهد بود.

گام ۶: تنظیم ارتباط بین Domainها

بعد از جدا شدن فیزیکی:

  • ارتباط بین domainها با:
    • API callها،
    • eventها،
    • یا pipelineهای داده (برای reporting) برقرار می‌شود.

اینجاست که:

  • patternهایی مثل Saga، read model، CQRS و غیره وارد بازی می‌شوند.

کتاب دقت می‌دهد:

  • نباید فکر کنی با جدا کردن DB، کار تمام شده؛
  • تازه complexity consistency و توزیع را وارد کرده‌ای،
  • و باید با الگوها و trade-offها مدیریت‌شان کنی.


در data pipeline تحلیلی، ایده این است:

  • سیستم اصلی (OLTP) فقط برای کار روزمره اپلیکیشن است: ثبت سفارش، پرداخت، لاگین و …
  • برای تحلیل، گزارش‌گیری، BI، داشبورد و کوئری‌های سنگین، داده را به یک data store تحلیلی می‌بری.

یعنی چی «data pipeline به data store تحلیلی»؟

  1. Extract:
    داده از سیستم‌های عملیاتی جمع می‌شود
    (مثلاً از چند دیتابیس سرویس‌ها، لاگ‌ها، eventها).

  2. Transform:
    در یک فرایند میانی، داده:
    • تمیز می‌شود (Cleaning)،
    • به ساختار تحلیلی تبدیل می‌شود (Denormalization، محاسبه ستون‌های مشتق‌شده، تجمیع روزانه/ساعتی)،
    • به هم join می‌شود، اما خارج از OLTP.
  3. Load:
    خروجی به یک محل ذخیره‌سازی تحلیلی منتقل می‌شود، مثل:
    • data warehouse (مثلاً star schema)،
    • data mart،
    • lakehouse،
    • یا حتی یک DB جدا فقط برای گزارش‌گیری.

از این به بعد:

  • گزارش‌ها، داشبوردها، مدل‌های تحلیلی،
    از data store تحلیلی می‌خوانند، نه از دیتابیس عملیاتی.

چرا این کار معماری مهم است؟

  • OLTP را از کوئری‌های سنگین، joinهای پیچیده و اسکن‌های بزرگ جدا می‌کنی → سیستم اصلی پایدارتر و سریع‌تر می‌ماند.
  • مدل داده تحلیلی می‌تواند denormalized و optimized برای read باشد (ستاره‌ای، snapshotهای روزانه و …)، مستقل از نیازهای نرمال‌سازی OLTP.
  • این دقيقاً یک کوآنتوم داده‌ای جدا است:
    • سیستم عملیاتی = یک کوآنتوم،
    • سیستم تحلیلی/گزارشی = کوآنتوم دیگر با چرخه و SLA خودش.


۱. زندگی با Eventual Consistency (وقتی Read Model داری)

کتاب بعد از معرفی Read Model و Database-per-service، روی یک نکته سخت تمرکز می‌کند:

وقتی داده را برای خواندن تکثیر می‌کنی، باید بپذیری که همه‌جا هم‌زمان به‌روز نیست.

این یعنی:

  • بین لحظه‌ای که داده در سیستم write تغییر می‌کند،
  • تا لحظه‌ای که read model / data store تحلیلی به‌روز می‌شود،
    فاصله زمانی وجود دارد.

چالش‌ها از نگاه کتاب

۱) User Experience

  • کاربر عملی انجام می‌دهد (مثلاً ثبت سفارش)،
    بلافاصله به صفحه لیست سفارش‌ها می‌رود،
    اما هنوز سفارش جدید در read model نیست.
  • اگر معماری و UI را درست طراحی نکنی،
    این اختلاف لحظه‌ای، برای کاربر «بگ» به نظر می‌رسد.

۲) Business Invariants

  • برخی قواعد بیزینسی (مثلاً موجودی انبار، محدودیت سقف اعتبار)
    ممکن است به چند سرویس و read model وابسته باشند.
  • Eventual consistency یعنی ممکن است لحظه‌ای داده‌ها «قدیمی» باشند،
    پس باید کوئری و قواعد را طوری طراحی کنی که این رفتار پذیرفته و مدیریت شود.

۳) Debug / پشتیبانی

  • وقتی در محیط واقعی، یک read model عقب می‌افتد،
    تیم پشتیبانی باید بتواند:
    • تشخیص دهد،
    • علت را ببیند (event مصرف نشده، خطای sync و …)،
    • و جبران کند.

کتاب می‌خواهد این واقعیت را معماری‌وار بپذیری، نه اینکه سعی کنی با ترفندهایی pseudo-ACID بسازی.

راهکارهای معماری برای مدیریت این وضعیت

کتاب چند ایده‌ی عمومی را مطرح می‌کند (من با زبان ساده خودم، ولی در چارچوب کتاب می‌گویم):

  • در UI:
    • پس از عمل write، نتیجه را موقتاً از همان منبع write نشان بده (optimistic UI)،
      تا وقتی read model sync شد، کاربر تناقض نبیند.
  • در بیزینس:
    • قواعد حیاتی را تا حد ممکن فقط روی منبع حقیقت enforce کن،
      نه بر پایه read modelهای احتمالی.
  • در سیستم:
    • برای pipeline/eventهایی که read model را به‌روز می‌کنند،
      monitoring و alert داشته باشی (fitness function برای «تأخیر مجاز» read model).

۲. از الگو به تصمیم واقعی: کجا Read Model، کجا Sync Call، کجا Shared DB محدود؟

کتاب در این بخش می‌خواهد تو را از سطح «الگو را می‌شناسم» به سطح «تصمیم‌گیری در سیستم واقعی» ببرد.
چند سناریوی تصمیم به این شکل مطرح می‌شوند (خلاصه مفهومی):

سناریو A: عملیات حیاتی، حجم کم خواندن

  • مثال: تأیید هویت، عملیات مالی حساس.
  • ویژگی:
    • هر خواندن بسیار مهم است و باید تازه باشد،
    • حجم read خیلی بالا نیست.

تصمیم معماری پیشنهادی:

  • Sync API call به سرویس مالک (no replication)،
    یعنی consistency را به دست می‌آوری، به قیمت coupling و latency.

سناریو B: خواندن زیاد، حساس به سرعت، کمی دیرتازه‌بودن قابل قبول

  • مثال: لیست سفارش‌ها، داشبورد، گزارش مدیریتی.
  • ویژگی:
    • query زیاد و سنگین،
    • کمی تأخیر در تازه شدن داده قابل تحمل است.

تصمیم معماری پیشنهادی:

  • Read Model / data store مخصوص خواندن،
    که:
    • از روی SoT تغذیه می‌شود (event / pipeline)،
    • بهینه برای query و UI است،
    • و eventual consistency را آگاهانه می‌پذیرد.

سناریو C: سیستم legacy با shared DB و محدودیت زمانی/سازمانی

  • مثال: سیستم قدیمی که چند سرویس/ماژول به یک DB وصل‌اند، و وقت/توان بازطراحی کامل نداری.
  • ویژگی:
    • coupling شدید،
    • ریسک بالا برای تغییر کرداری زیاد.

تصمیم معماری پیشنهادی کتاب:

  • Shared DB با قواعد سفت‌وسخت به‌عنوان «وضعیت موقت / بدهی معماری»:
    • یک سرویس/ماژول رسمی مالک write به هر جدول؛
    • سایرین فقط از طریق view / API؛
    • مستند کردن این تصمیم در ADR؛
    • و برنامه‌ریزی تدریجی برای شکستن این shared DB در آینده (همان گام‌های قبلی).

۳. این بخش کتاب برای تو چه می‌سازد؟

برای هدفی که گفتی (تصمیم‌گیری در سیستم فعلی + نقش Software Architect)،
این بخش دو چیز می‌دهد:

1) زبان تصمیم

  • بتوانی بگویی:
    • «برای این use case، read model با eventual consistency را انتخاب می‌کنیم»،
    • یا «اینجا فقط sync API call به منبع حقیقت مجاز است»،
    • یا «فعلاً shared DB داریم، اما ownership و برنامه خروج آن این است».

2) هشیاری نسبت به هزینه‌ها

  • read model = نیاز به pipeline، sync logic، monitoring،
  • sync call = coupling و dependency زمانی/availability،
  • shared DB = بدهی معماری که باید دیده، نه پنهان شود.

کتاب می‌خواهد این انتخاب‌ها را از حالت سلیقه‌ای/تصادفی، به حالت تصمیم‌های آگاهانه و مستند تبدیل کند.



  • منبع حقیقت (Write Model) مثلا سرویس Order است.
  • Read Model (یا DB گزارش/لیست) با کمی تأخیر به‌روزرسانی می‌شود.
  • برای جلوگیری از «ناپدید شدن» داده تازه در UI، بعد از write، مدتی نتیجه را از همان منبع write نشان می‌دهی.

ساده‌ترین پیاده‌سازی مفهومی

۱) بعد از موفقیت write (مثلا ایجاد سفارش)، API سرویس write، آبجکت کاملِ سفارش را برمی‌گرداند.
۲) UI آن سفارش را موقتاً در state خودش نگه می‌دارد و در لیست نمایش می‌دهد، حتی اگر هنوز در read model نیامده باشد.
۳) وقتی بعداً لیست را از read model می‌خوانی و آن سفارش در آن ظاهر شد، نسخه read model جایگزین نسخه موقت می‌شود.

به‌عبارت دیگر:

  • تا زمانی که read model «به‌روز نشده»، UI با state محلی (local state) + داده برگشتی از write کاربر را راضی نگه می‌دارد.
  • این دقیقاً مصداق optimistic UI در سطح معماری است:
    فرض می‌کنی write موفق و sync به‌زودی انجام می‌شود، و قبل از sync هم نتیجه را به کاربر نشان می‌دهی.


۱. Event-Driven Architecture به‌عنوان ستون فقرات اشتراک داده

کتاب این‌جا ایده را رسمی‌تر می‌کند:

  • برای این‌که سرویس‌ها و کوآنتوم‌ها داده و رفتار را با هم هماهنگ کنند،
    به‌جای call مستقیمِ زیاد، از رویداد (Event) استفاده می‌کنیم.

تعریف در چارچوب کتاب

  • Event = «یک اتفاق معنی‌دار در دامین» که در گذشته رخ داده
    (مثلاً: OrderPlaced, PaymentCaptured, TicketClosed).
  • سرویس مالک (منبع حقیقت)
    هنگام تغییر وضعیت، event منتشر می‌کند.
  • سایر سرویس‌ها Subscribe می‌کنند و:
    • read model خودشان را به‌روزرسانی می‌کنند،
    • یا Workflows جدیدی را آغاز می‌کنند.

این، ستون فقرات خیلی از چیزهایی است که تا حالا گفتیم:

  • Database-per-service،
  • Read model،
  • CQRS،
  • Saga،
  • و data pipeline تحلیلی.

مزایا (هم‌راستا با کتاب)

  • کاهش coupling زمانی (نیاز کمتر به sync call در لحظه).
  • امکان scale مستقل consumerها.
  • ثبت تاریخچه رخدادها به‌صورت شفاف‌تر.

معایب / هزینه‌ها

  • پیچیدگی در:
    • طراحی event (چه چیزی event است، چه چیزی نیست)،
    • نسخه‌بندی eventها،
    • handling خطا (message از دست‌رفته، مصرف دوباره، ترتیب رسیدن).

کتاب تأکید می‌کند Event-Driven عصای جادویی نیست؛
فقط ابزاری است که اگر با درک trade-off استفاده شود، معماری را قوی می‌کند.

۲. Event Sourcing: داده = دنباله رخدادها، نه فقط آخرین وضعیت

روی شانه‌ی EDA، کتاب مفهوم Event Sourcing را معرفی می‌کند (حداقل در سطح مفهومی):

ایده اصلی

در Event Sourcing:

  • به‌جای این‌که فقط «آخرین وضعیت» یک aggregate (مثلاً Order) را ذخیره کنیم،
  • کل دنباله‌ی رخدادهایی را که آن را به این وضعیت رسانده‌اند ذخیره می‌کنیم:
    • OrderCreated
    • ItemAdded
    • PaymentCaptured
    • OrderShipped
  • وضعیت فعلی، نتیجه‌ی پخش کردن (Replay) این رخدادهاست.

چه فرقی با سیستم معمولی دارد؟

در سیستم معمولی (state-based):

  • جدول Order فقط ستونی مثل Status, TotalAmount, … دارد،
    و هر به‌روزرسانی مقدار را عوض می‌کند.
  • تاریخچه کاملِ تغییرات در DB نیست (مگر با audit logهای جدا).

در Event Sourcing:

  • «حقیقت اصلی» در event store است،
  • و هر projection (جدول وضعیت فعلی، read modelها، گزارش‌ها)
    مشتق از این رخدادهاست.

مزایا از دید کتاب

  • Audit و history کامل:
    می‌توانی هر لحظه از گذشته را بازسازی کنی.
  • هم‌خوانی قوی با CQRS و EDA:
    • eventها، هم برای بازسازی state استفاده می‌شوند،
    • هم برای اطلاع‌رسانی به سرویس‌های دیگر.
  • انعطاف برای ساخت read modelهای جدید بدون تغییر منبع حقیقت:
    • فقط eventها را دوباره پخش می‌کنی و projection جدید می‌سازی.

هزینه‌ها و سختی‌ها

کتاب صریحاً می‌گوید Event Sourcing برای همه‌جا نیست:

  • پیچیدگی مدل‌سازی بالا:
    • باید aggregate و eventها را دقیق طراحی کنی.
  • نیاز به tooling و تجربه:
    • event store، migration eventها، versioning رویدادها.
  • ذهنیت تیم:
    • توسعه‌دهندگان باید به «فکر کردن بر مبنای رخداد» عادت کنند،
      نه فقط CRUD روی جدول.

بنابراین:

  • Event Sourcing معمولاً فقط برای دامین‌هایی که:
    • نیاز به history قوی دارند،
    • منطق تغییرات پیچیده است،
    • یا integration event-driven شدیدی دارند،
      منطقی است؛
      نه برای هر CRUD ساده.


۱. «چه چیز Event است؟» معیار تشخیص

در نگاه کتاب/معماری رویدادمحور، هر چیزی Event نیست.

Event = اتفاق معنادار در دامین، که در گذشته رخ داده و برای بقیه مهم است.

معیارها:

  1. از دید بیزینس مهم است، نه فقط تکنیکال
    • خوب: OrderPlaced, PaymentCaptured, UserRegistered
    • بد: RowUpdated, CacheRefreshed
  2. بیان گذشته است (past tense):
    • OrderShipped نه ShipOrder
    • UserEmailChanged نه ChangeUserEmail
  3. پیامد/اثر دارد، نه صرفاً یک حالت
    • تغییر حالت است که برای دیگر bounded contextها یا همان دامین اهمیت دارد.
  4. به‌تنهایی قابل فهم است (self-contained)
    • شامل حداقل اطلاعات لازم برای استفاده‌کننده‌هاست (Idها، داده کلیدی).

اگر چیزی:

  • فقط برای UI داخلی یک سرویس مهم است → لزوماً event اشتراک‌پذیر نیست.
  • برای چند context یا برای build کردن history اهمیت دارد → کاندید جدی event است.

2. نسخه‌بندی Event (Event Versioning)

در سیستم واقعی، قرارداد event ثابت نمی‌ماند. کتاب‌ها (و تجربه عملی) می‌گویند:

  • eventها immutable اند؛
  • پس اگر schema/معنا را عوض کنی، event جدید منتشر می‌کنی، نه اینکه قدیمی‌ها را تغییر دهی.

الگوهای رایج نسخه‌بندی

۱) EventType جدید (نام جدید)

  • قبلاً: OrderCreated
  • بعداً: OrderCreatedV2 (یا OrderInitialized) با فیلدهای جدید/معنای کمی متفاوت.
  • consumer قدیمی فقط OrderCreated را می‌فهمد؛ جدیدها می‌توانند هر دو را پردازش کنند.

۲) افزودن فیلدهای اختیاری (backward compatible)

  • event جدید فیلد بیشتری دارد،
  • consumerها باید بتوانند بدون آن فیلد هم کار کنند.
  • برای تغییرات کوچک و غیرشکننده.

۳) Upcaster / Mapper در لایه خواندن Event Store

  • هنگام خواندن eventهای قدیمی،
    یک «upcaster» آن‌ها را به شکل جدید تبدیل می‌کند.
  • این رویکرد در پیاده‌سازی Event Sourcing جدی‌تر می‌شود.

نکته معماری:
قرارداد event باید با وسواس طراحی و تغییر کند؛ دقیقاً مثل API versioning، بلکه مهم‌تر.

3. مدیریت خطا در Event-Driven / Event Sourcing

خطا در سه سطح مهم مطرح است:

الف) تولید Event (Publisher)

  • اگر رفتار دامین fail شود، event نباید ثبت/منتشر شود.
  • در Event Sourcing:
    • command ⇒ validate ⇒ اعمال روی aggregate ⇒ تولید event ⇒ ذخیره event به‌صورت اتمیک.
    • اگر ذخیره event fail شود، state هم نباید تغییر کرده باشد.

ب) مصرف Event (Consumer)

سناریوهای خطا:

  • پیام رسیده ولی پردازش consumer fail می‌شود.
  • پیام دو بار رسیده (at-least-once delivery).
  • ترتیب رسیدن چند event به هم می‌ریزد.

الگوهای معماری:

  • idempotency در consumer:
    • مصرف چندباره یک event نباید state را خراب کند.
  • dead-letter queue / parking:
    • eventهایی که پردازششان بارها fail می‌شود، در صف جدا می‌روند تا بعداً دستی بررسی/جبران شوند.
  • retry + backoff:
    • تلاش مجدد با فاصله، بدون خفه‌کردن سیستم.

ج) sync بین write model و read model

  • اگر event از دست برود یا consumer عقب بماند،
    read model ممکن است قدیمی شود.
  • به همین دلیل:
    • مانیتورینگ «تاخیر read model» مهم است (مثلاً حداکثر فاصله زمانی بین آخرین event پردازش‌شده و حال حاضر).
    • در صورت زیاد شدن تاخیر، alert و maybe full rebuild انجام می‌شود.

4. «چطور فقط آخرین وضعیت را ذخیره نکنیم؟» Event Store در برابر State Store

در Event Sourcing:

  • به‌جای اینکه جدول Order را به‌صورت:
    Id, Status, TotalAmount, ...
    مرتب overwrite کنی،
  • دنباله eventها را ذخیره می‌کنی، مثلاً:
OrderId Seq EventType Data
123 1 OrderCreated {CustomerId=10, …}
123 2 ItemAdded {ProductId=5, Qty=2}
123 3 ItemAdded {ProductId=7, Qty=1}
123 4 PaymentCaptured {Amount=…}
123 5 OrderShipped {ShipmentId=…}

وضعیت فعلی Order 123 =
نتیجه replay این eventها روی aggregate.

برای سرعت:

  • معمولاً snapshot هم نگه می‌داری:
    هر N رویداد، یک snapshot از state فعلی؛
    در replay بعدی، از آخرین snapshot شروع می‌کنی.

5. فرق Event Sourcing با Audit Log ساده

ظاهراً شبیه به‌نظر می‌رسند، ولی تفاوت عمیق دارند:

Audit Log

  • سیستم اصلی state-based است (جدول وضعیت فعلی).
  • Audit log:
    • کنار آن، log عملیات/تغییرات را صرفاً برای ردیابی و ممیزی ثبت می‌کند.
  • state اصلی سیستم از روی جدول «وضعیت فعلی» خوانده می‌شود؛
    audit log برای چک کردن است، نه برای محاسبه state.

ویژگی‌ها:

  • ممکن است ناقص باشد (فقط بعضی عملیات log شوند).
  • ساختارش الزاماً domain-centric نیست.
  • اگر audit خراب شود، سیستم می‌تواند به کارش ادامه دهد.

Event Sourcing

  • «منبع حقیقت» خود eventها هستند.
  • state فعلی = projection روی eventها.
  • اگر projection/جدول وضعیت فعلی پاک شود،
    می‌توانی از روی eventها آن را دوباره بسازی.

ویژگی‌ها:

  • eventها domain-centric هستند (OrderCreated, …)،
    نه فقط «row X update شد».
  • هر تغییر معنادار باید به‌صورت event ثبت شود؛
    این پیش‌فرض طراحی است، نه افزونه.
  • event store بخش حیاتی سیستم است، نه فقط log کنار آن.

خلاصه:

  • Audit Log = تاریخچه کناری، state اصلی جای دیگر.
  • Event Sourcing = تاریخچه = منبع state؛ projection فقط نمای راحت‌تر است.


در Event Sourcing، snapshot فقط یک «میان‌بُر برای لود state» است؛ منبع حقیقت هنوز خود eventها هستند. بیاییم این را با یک مثال کامل و مرحله‌ای جا بیندازیم.

۱. سناریوی نمونه: کیف پول (Wallet) با Event Sourcing + Snapshot

فرض کن یک aggregate به نام Wallet داریم:

  • رویدادها:
    • WalletOpened
    • MoneyDeposited
    • MoneyWithdrawn
  • state نهایی:
    • Balance
    • Status (مثلاً Active/Closed)
    • لیست آخرین تراکنش‌ها (اختیاری)

بدون snapshot

برای به‌دست آوردن وضعیت فعلی کیف پول W-123:

  1. همه eventهای stream wallet-W-123 را از event store می‌خوانی:
    WalletOpenedMoneyDepositedMoneyWithdrawn → … (مثلاً ۲۰۰۰ رخداد).
  2. آن‌ها را به‌ترتیب روی aggregate اعمال می‌کنی:
    • Balance را به‌روز می‌کنی،
    • Status را تنظیم می‌کنی و …
  3. بعد از ۲۰۰۰ replay، به state فعلی می‌رسی.

اگر کیف پول قدیمی و پر تراکنش باشد، هر بار ۲۰۰۰ event لود کردن، گران است.

۲. snapshot دقیقاً چیست؟

snapshot = یک آبجکت state از aggregate در یک نقطه خاص + شماره آخرین eventی که در آن state لحاظ شده.

مثلاً:

Snapshot(Wallet):
  WalletId = "W-123"
  Version = 2000          // یعنی تا event شماره ۲۰۰۰ لحاظ شده
  State = {
    Balance = 1_250_000
    Status = "Active"
    LastTransactionAt = 2025-01-01T10:05:00
    // هرچقدر state لازم داری
  }

این snapshot می‌تواند:

  • در یک جدول/stream جدا (snapshot-wallet-W-123) باشد،
  • یا در همان stream اصلی به‌عنوان یک event خاص (مثلاً نوع WalletSnapshotted).

۳. فرآیند لود state با snapshot

وقتی می‌خواهی Wallet W-123 را لود کنی:

  1. آخرین snapshot را می‌خوانی (اگر هست):
    • Snapshot با Version = ۲۰۰۰.
  2. aggregate را با همان state snapshot مقداردهی می‌کنی.
  3. از event store فقط eventهای بعد از version ۲۰۰۰ را می‌خوانی:
    • مثلاً MoneyDeposited event #۲۰۰۱
  4. همین چند event را روی state snapshot اعمال می‌کنی.
  5. حالا state فعلی را داری با replay خیلی کمتر.

اگر snapshot نبود:

  • همه eventها از اول خوانده و replay می‌شوند (رفتار پایه Event Sourcing).

۴. چه موقع snapshot می‌گیریم؟

سیاست ساده رایج:

  • هر N event (مثلاً ۱۰۰ یا ۲۰۰) یا
  • بر اساس domain event خاص (مثلاً «پایان شیفت»، «بستن روز مالی»).

مثلاً:

  • بعد از هر ۵۰۰امین event کیف پول،
    یک snapshot جدید می‌سازیم:
New Snapshot:
  WalletId = "W-123"
  Version = 2500
  State = {...}

و snapshot قدیمی ۲۰۰۰ دیگر لازم نیست (می‌توانی نگه داری یا پاک کنی).

۵. فرق snapshot با audit log و state عادی

  • State عادی (در سیستم CRUD کلاسیک):
    • فقط آخرین وضعیت در جدول است (مثلاً Balance=۱,۲۵۰,۰۰۰).
    • تاریخچه تغییرات در DB نیست (اگر audit نداشته باشی).
  • Audit log:
    • مستقل از دیتا، تغییرات را برای «دیدن/ممیزی» log می‌کند،
    • اما state اصلی سیستم همچنان همان جدول وضعیت است،
    • اگر audit پاک شود، سیستم کار می‌کند.
  • Event Sourcing + snapshot:
    • eventها منبع حقیقت‌اند (نه snapshot),
    • snapshot فقط shortcut برای لود state است،
    • اگر snapshot پاک شود:
      • می‌توانی از روی eventها snapshot جدید بسازی،
      • state را از ابتدا replay کنی.

پس:

  • snapshot = state کش شده بر اساس eventها، قابل بازتولید؛
  • audit log = تاریخچه کنار state اصلی، نه پشتوانه اصلی state.

۶. نسخه‌بندی event در همین مثال کیف پول

فرض کن event اولیه MoneyDeposited فقط این را داشت:

MoneyDeposited v1:
  WalletId
  Amount

بعداً بیزینس می‌خواهد نوع تراکنش (Source مثل ATM, Online, Branch) را بداند:

  • برای شکستن قرارداد، نمی‌توانی eventهای قبلی را تغییر دهی،
  • راه‌های رایج:

۱) اضافه‌کردن فیلد اختیاری به data (backward compatible)

  • Source به event اضافه می‌شود،
  • consumerهای قدیمی که Source را نمی‌خوانند، نمی‌شکنند.

۲) event جدید

  • MoneyDepositedV2 با data بیشتر (مثلاً شامل currency, Source)،
  • aggregate هنگام replay:
    • اگر V1 دید، با defaultها کار می‌کند،
    • اگر V2 دید، اطلاعات کامل‌تر را لحاظ می‌کند.

در هر دو صورت:

  • eventهای قبلی دست‌نخورده می‌مانند،
  • snapshotها می‌توانند state کامل‌تر را منتج شوند،
  • و اگر snapshot از بین برود، replay با logic version-aware state را دوباره می‌سازد.

۷. مدیریت خطا روی snapshot و event

در همین مثال کیف پول:

  • اگر هنگام ساخت snapshot خطا شود:
    • eventها همچنان ذخیره شده‌اند؛
    • فقط snapshot این نوبت را از دست داده‌ای (در بدترین حالت، replay طولانی‌تر می‌شود).
  • اگر snapshot خراب شود:
    • می‌توانی آن را نادیده بگیری و از روی eventها snapshot جدید بسازی.

چون:

  • اکیداً event store منبع حقیقت است،
  • snapshot فقط optimization و cache است.


۱. aggregate در Event Sourcing چی هست؟

از نگاه کتاب/معماری دامین:

  • aggregate = ریشه‌ی یک خوشه‌ی داده و رفتار در دامین (مثلاً Order, Wallet, Ticket).
  • در Event Sourcing:
    • هیچ‌وقت مستقیماً state جدول را نمی‌خوانی؛
    • aggregate از روی eventها ساخته می‌شود.

پس aggregate یک آبجکت در حافظه است که:

  • state داخلی دارد (Properties/Fields)،
  • مجموعه‌ای از متدها دارد که:
    • بر اساس eventها state را تغییر می‌دهند،
    • و روی commandها event جدید می‌سازند.

۲. جریان کامل: از event store تا aggregate در حافظه

مرحله ۱: دریافت eventها

برای aggregate با Id مثلاً Wallet-123:

  1. از event store، stream wallet-123 را می‌خوانی:
    • یا از اول (اگر snapshot نداری)،
    • یا از آخرین snapshot-version به بعد.

فرض کنیم eventهای زیر را گرفتی (به ترتیب):

  1. WalletOpened
  2. MoneyDeposited
  3. MoneyWithdrawn
  4. MoneyDeposited

مرحله ۲: ساخت aggregate خالی

در کد/ذهن:

  • یک instance جدید از WalletAggregate می‌سازی، بدون state (یا در حالت پیش‌فرض):
var wallet = new WalletAggregate();  // Balance = 0, Status = null...

مرحله ۳: replay (اعمال eventها روی aggregate)

برای هر event به‌ترتیب:

  • متدی روی aggregate صدا می‌زنی که آن event را «apply» کند:
foreach (var event in events)
    wallet.Apply(event);

و داخل Aggregate:

class WalletAggregate {
    decimal Balance;
    string Status;

    void Apply(WalletOpened e) {
        this.Status = "Active";
        this.Balance = 0;
    }

    void Apply(MoneyDeposited e) {
        this.Balance += e.Amount;
    }

    void Apply(MoneyWithdrawn e) {
        this.Balance -= e.Amount;
    }
}

نتیجه:

  • بعد از replay همه eventها،
    wallet.Balance و wallet.Status همان وضعیت فعلی هستند.

این «Apply»ها همان منطق تغییر state بر اساس گذشته‌اند.

۳. ارتباط replay با snapshot

اگر snapshot داشته باشی:

  • snapshot یک state آماده است + نسخه event آخر:
var snapshot = snapshotStore.GetLatest("wallet-123");
// snapshot.State: Balance=1000, Status=Active
// snapshot.Version: 200

var wallet = new WalletAggregate(snapshot.State);

var events = eventStore.ReadFrom("wallet-123", fromVersion: snapshot.Version+1);

foreach (var e in events)
    wallet.Apply(e);

پس:

  • aggregate از snapshot شروع می‌کند،
  • فقط eventهای بعدی را replay می‌کند.

۴. Command جدید چطور روی همین aggregate کار می‌کند؟

بعد از لود aggregate، وقتی Command جدید می‌آید (مثلاً WithdrawMoney(200)):

  1. Command روی aggregate صدا می‌خورد:
wallet.Handle(new WithdrawMoney(200));
  1. Inside Handle:
  • قوانین دامین را چک می‌کنی (مثلاً: Balance کافی هست یا نه).
  • اگر OK بود، event جدید تولید می‌کنی:
class WalletAggregate {
    List<DomainEvent> pendingEvents = new();

    void Handle(WithdrawMoney cmd) {
        if (this.Balance < cmd.Amount)
            throw new DomainException("Insufficient funds");

        var @event = new MoneyWithdrawn {
            WalletId = this.Id,
            Amount = cmd.Amount,
            OccurredAt = Now
        };

        Apply(@event);          // state را همین‌جا به‌روز کن
        pendingEvents.Add(@event);
    }
}
  1. پس از موفقیت، pendingEvents به event store ذخیره می‌شوند.
    در بار بعدی لود، همان event جدید هم در replay لحاظ خواهد شد.

نکته مهم:

  • state فقط از طریق Apply(event) تغییر می‌کند؛
  • هم در replay گذشته، هم در پاسخ به command جدید.

۵. چرا این فرق دارد با CRUD + audit log؟

در CRUD معمولی:

  • state را از جدول می‌خوانی (SELECT * FROM Wallet WHERE Id=...
  • مستقیماً فیلدها را تغییر می‌دهی (UPDATE ...).

audit log:

  • ممکن است صرفاً قبل/بعد را log کند، ولی state اصلی همانی است که در جدول است.

در Event Sourcing:

  • state اصلی از replay eventها ساخته می‌شود؛
  • جدول وضعیت نهایی اگر باشد، projection است (read model)، نه منبع حقیقت.

aggregate:

  • همیشه با eventها زندگی می‌کند:
    • گذشته را از event store می‌گیرد،
    • آینده را با تولید event ادامه می‌دهد.

۶. ترکیب این با نسخه‌بندی event

اگر بعداً MoneyDeposited نسخه جدیدی آوردی (V2)، داخل Apply می‌توانی:

void Apply(MoneyDepositedV1 e) {
    this.Balance += e.Amount;
}

void Apply(MoneyDepositedV2 e) {
    this.Balance += e.Amount;
    this.LastSource = e.Source;
}

هنگام replay:

  • eventهایی که قدیمی‌اند، با منطق قدیمی (یا با default) اعمال می‌شوند؛
  • eventهای جدید، state کامل‌تر را می‌سازند.

snapshot هم state نهایی را در یک نقطه منعکس می‌کند؛
اگر پاک شود، می‌توانی replay را از اول انجام دهی.



۱. Event Sourcing؛ ابزار سنگین، نه پیش‌فرض

کتاب تأکید می‌کند:

  • Event Sourcing یک گزینه معماری سنگین است:
    • مدل‌سازی دشوارتر،
    • نیاز به tooling و تجربه،
    • پیچیدگی در testing، debugging، migration.
  • پس نباید تبدیل شود به:
    • «هرجا event-driven داریم = Event Sourcing»،
      بلکه برعکس:
    • اول ببین مشکل/نیاز چیست، بعد ببین Event Sourcing مناسب است یا نه.

کجا Event Sourcing منطقی است؟

معمولاً وقتی همه یا بعضی این‌ها صادق‌اند:

  1. تاریخچه کامل رفتار مهم است
    • برای ممیزی، مالی، حقوقی، investigation (مثلاً حساب، سفارش مالی، تغییرات قرارداد).
  2. منطق تغییر state پیچیده و متکی بر گذشته است
    • تصمیم فعلی وابسته به توالی و نوع رخدادهای قبلی است،
      نه فقط به وضعیت فعلی یک جدول.
  3. نیاز به چندین projection مختلف از داده داری
    • مثلاً state فعلی، گزارش‌های تحلیلی، log مالی، snapshotهای خاص…
    • Event Sourcing اجازه می‌دهد projectionهای جدید را بدون تغییر منبع حقیقت بسازی (فقط eventها را دوباره پخش می‌کنی).
  4. هم‌زمان می‌خواهی event-driven integration قوی داشته باشی
    • چون eventها از اول طراحی شده‌اند، به کار integration و read modelها هم می‌آیند.

کجا نباید Event Sourcing به‌کار ببری؟

  • CRUD ساده با منطق کم:
    • مثل جدول تنظیمات، lookupها، یا چیزهایی که «آخرین وضعیت» کافی است.
  • domainهایی که:
    • نیاز به history کامل ندارند,
    • یا حجم event خیلی زیاد می‌شود و value تاریخچه پایین است.
  • تیم/سازمان هنوز در پایه‌های distributed systems و event-driven immature است.

۲. ریسک‌ها و هزینه‌ها از دید معمار

کتاب (و تجربه عملی) روی چند ریسک مهم دست می‌گذارد؛ تو به‌عنوان Software Architect باید آن‌ها را آگاهانه قبول کنی:

ریسک ۱: تغییر مدل و migration

  • اگر بعد از مدتی بفهمی مدل eventها کامل/درست نبوده:
    • اصلاحِ گذشته سخت‌تر از state-based است،
    • باید:
      • یا upcaster بسازی،
      • یا migration eventها را انجام دهی (که عملیات سنگینی است).

ریسک ۲: پیچیدگی تیمی و ذهنی

  • توسعه‌دهندگان باید:
    • به‌جای فکر کردن «UPDATE جدول»،
      به «رویدادها» فکر کنند:
      • چه eventی تولید شود،
      • چه Applyی بنویسند،
      • چطور command → event → state را طراحی کنند.
  • تست‌ها باید:
    • روی توالی eventها نوشته شوند
      (Given این eventها، When این command، Then این event/این state).

اگر تیم فقط به CRUD عادت دارد، این یک جهش ذهنی جدی است.

ریسک ۳: tooling و operability

  • نیاز به:
    • event store مناسب و مطمئن،
    • ابزار دیدن/جستجوی eventها،
    • مانیتورینگ جریان event،
    • پشتیبانی از snapshot و replay در محیط واقعی.
  • بدون این‌ها، debug و پشتیبانی واقعاً سخت می‌شود.

۳. استراتژی «هسته Event Sourced، بقیه state-based»

کتاب ضمنی پیشنهاد می‌کند (و بسیاری جاها همین کار می‌شود):

  • لازم نیست کل سیستم را Event Sourcing کنی؛
  • می‌توانی:

    • هسته دامین حساس (مثلاً حساب مالی، تغییر مالکیت دارایی، lifecycle سفارش)
      را با Event Sourcing پیاده کنی،
    • بقیه قسمت‌ها (UI, تنظیمات، بخش‌های ساده CRUD) state-based معمولی باشند.

این طراحی:

  • Event Sourcing را در جایی به‌کار می‌برد که ارزشش را دارد،
  • ولی complexity را به کل سیستم تعمیم نمی‌دهد.

۴. تصمیم معماری: «EDA بدون Event Sourcing» vs «EDA با Event Sourcing»

کتاب می‌خواهد تفکیک روشن باشد:

  • Event-Driven Architecture (EDA):
    • می‌توانی فقط از event برای integration و read model استفاده کنی،
    • ولی منبع stateِ اصلی سیستم، همچنان جدول وضعیت فعلی باشد.
  • Event Sourcing:
    • eventها منبع حقیقت state هستند.

پس:

  • EDA بدون Event Sourcing برای بسیاری سیستم‌ها کافی و مطلوب است؛
  • Event Sourcing فقط وقتی وارد بازی شود که واقعاً مزیتش (history، پیچیدگی دامین، چند projection) نیاز است.


۱. چرا اصلاً upcaster لازم می‌شود؟

در Event Sourcing:

  • eventها immutable هستند؛ بعد از ذخیره نباید تغییرشان دهی.
  • اما دامین تغییر می‌کند:
    • فیلد جدید اضافه می‌کنی،
    • معنای event کامل‌تر می‌شود،
    • شکل JSON/contract عوض می‌شود.

مشکل:
eventهای قدیمی هنوز در event store هستند؛ کد جدیدت با شکل جدید event کار می‌کند.

راه‌حل:
قبل از اینکه event قدیمی را به aggregate یا projection بدهی،
آن را به شکل جدید «بالا می‌کشی» (upcast → تبدیل از نسخه قدیمی به نسخه جدید).

۲. upcaster دقیقاً چه می‌کند؟

در جریان لود aggregate:

  1. از event store، event خام (قدیمی) خوانده می‌شود؛
  2. قبل از این‌که به Apply() برود، از upcaster عبور می‌کند؛
  3. upcaster:
    • event قدیمی را می‌گیرد،
    • اگر لازم باشد:
      • فیلد جدید اضافه می‌کند (default می‌گذارد)،
      • نام type را map می‌کند به type جدید،
    • و یک event «جدید» در حافظه پس می‌دهد؛
  4. aggregate فقط با نسخه جدید کار می‌کند.

event روی دیسک دست نخورده می‌ماند؛
upcaster فقط در مسیر read آن را transform می‌کند.

شِمای مفهومی:

Raw Event (old shape)  ──>  Upcaster  ──>  New Event object  ──>  Aggregate.Apply(...)

۳. مثال ساده

فرض کن قبلاً event UserRegistered فقط داشت:

{
  "UserId": 10,
  "Email": "a@b.com"
}

بعداً تصمیم می‌گیری DisplayName هم داخل event باشد.

کد جدیدت انتظار دارد:

{
  "UserId": 10,
  "Email": "a@b.com",
  "DisplayName": "..."
}

اما eventهای قدیمی این فیلد را ندارند.

upcaster:

Event Upcast(Event raw) {
    if (raw.Type == "UserRegistered" && raw.Data.DisplayName is null) {
        raw.Data.DisplayName = raw.Data.Email.Split("@")[0]; // یا یک default
    }
    return raw;
}

از دید aggregate:

  • همیشه eventی می‌بیند که DisplayName دارد (یا با default).

۴. فرق upcaster با “رفتن و عوض کردن eventها در DB”

دو راه کلی برای تکامل eventها:

  1. upcaster:
    • eventها روی دیسک immutable می‌مانند؛
    • هنگام خواندن، در حافظه transform می‌کنی؛
    • مزیت:
      • تاریخچه واقعی حفظ می‌شود؛
    • عیب:
      • chain upcasterها می‌تواند زیاد و پیچیده شود، باید همیشه نگه‌شان داری.
  2. copy & replace (refactor event stream):
    • کل stream را می‌خوانی،
      روی دیسک در stream جدید، eventها را با شکل جدید می‌نویسی؛
    • بعد از migration، upcaster را حذف می‌کنی؛
    • عیب:
      • immutability شکسته می‌شود (نسخه جدیدی از تاریخ می‌سازی)،
      • عملیات سنگین و حساس است.

کتاب معمولاً رویکرد upcaster را به‌عنوان ابزار اصلی پیشنهاد می‌کند، مگر در refactorهای بزرگ.



۱. در Event Sourcing دقیقاً چه مشکل داریم؟

  • قبلاً eventهایی مثل OrderPlaced داشتی که این فیلدها را داشتند:
    • OrderId
    • CustomerId
    • TotalAmount
  • حالا می‌خواهی فیلد جدیدی اضافه کنی، مثلاً:
    • Channel (Web, Mobile, POS)

ولی در event store کلی OrderPlaced قدیمی بدون Channel ذخیره شده.
نمی‌توانی آن‌ها را تغییر بدهی (اصل immutability).

سوال: aggregate جدید که انتظار Channel را دارد، با eventهای قدیمی چه کند؟

۲. گزینه‌های اصلی هنگام افزودن فیلد جدید

گزینه ۱: اضافه‌کردن فیلد اختیاری (ساده‌ترین حالت)

  • event type را عوض نمی‌کنی، فقط schemaش را گسترش می‌دهی:
    • Channel می‌شود فیلد optional.
  • در کد:
    • اگر event قدیمی بود، Channel مقدار null یا default می‌گیرد.
    • aggregate و projectionها باید این را مدیریت کنند (مثلاً اگر null بود، رفتار قبلی را داشته باشند).

این روش موقعی خوب است که:

  • معنی فیلد جدید فقط «اطلاعات بیشتر» است،
  • نبودنش برای eventهای گذشته مشکلی ایجاد نمی‌کند.

گزینه ۲: تعریف event type جدید (وقتی معنی عوض شده)

اگر اضافه‌کردن فیلد، در واقع تغییر معنی event است، بهتر است:

  • OrderPlaced قدیمی را نگه داری،
  • event جدید تعریف کنی: OrderPlacedV2 یا OrderInitialized با schema جدید.

Aggregate:

  • Apply(OrderPlaced) قدیمی را دارد (بدون Channel),
  • Apply(OrderPlacedV2) را هم اضافه می‌کند (با Channel).

Command جدید فقط OrderPlacedV2 تولید می‌کند؛
eventهای قدیمی روی aggregate با منطق قدیمی / default اعمال می‌شوند.

این روش مناسب است وقتی:

  • تغییر معنادار است،
  • می‌خواهی Distinguish بین قبل و بعد داشته باشی.

گزینه ۳: upcaster (وقتی نمی‌خواهی همه جا if بنویسی)

اگر نمی‌خواهی در هر Apply یا projection شرط نسخه بگذاری:

  • upcaster را وسط می‌گذاری (در مسیر read):

جریان:

  1. event خام (قدیمی) از store خوانده می‌شود.
  2. از upcaster عبور می‌کند.
  3. upcaster اگر دید event قدیمی است:
    • فیلد جدید را با default یا logic تولید می‌کند.
  4. aggregate و projection فقط event «جدید» را می‌بینند.

در نتیجه:

  • کد domain فقط یک شکل event را می‌شناسد،
  • منطق backward-compatibility در upcaster متمرکز می‌شود.

این روش زمانی به‌صرفه است که:

  • تاریخچه زیاد است،
  • فیلد جدید برای replay و projection مهم است،
  • نمی‌خواهی تغییرات نسخه‌ای را در همه Applyها پخش کنی.

۳. چه کار نباید بکنی؟

در Event Sourcing:

  • نباید eventهای قدیمی را در DB «ویرایش» کنی تا فیلد جدید بگیرند، مگر در یک migration خیلی کنترل‌شده و با طراحی خاص.
  • نباید schema event store را طوری تغییر بدهی که eventهای قبلی دیگر قابل parse نباشند.

اصل:
event ذخیره‌شده، تاریخ است؛ تاریخ را نمی‌نویسی، تعبیرش را تغییر می‌دهی (با upcaster، default، event جدید).

۴. Aggregate و Read Model چه تغییری می‌کنند؟

وقتی فیلد جدید اضافه می‌شود:

  • Aggregate:
    • در Apply، از فیلد جدید استفاده می‌کند اگر باشد؛
    • اگر نبود، رفتار قبلی (با default) را انجام می‌دهد.
  • Read Model:
    • اگر هم‌اکنون هم بدون این فیلد کار می‌کرد، می‌تواند به همین حالت ادامه دهد؛
    • اگر نیاز دارد از فیلد جدید استفاده کند،
      باید بداند در eventهای قدیمی مقدارش چگونه تعیین شود (default, inference, …).

۵. اگر سیستم state-based + audit log باشد؟

اگر سیستم Event Sourcing نیست، بلکه:

  • state در جدول است،
  • audit log جدا داری،

آن‌وقت:

  • جدول اصلی را مثل همیشه alter می‌کنی (فیلد جدید اضافه می‌کنی).
  • audit log:
    • اگر event-style log است، همین الگوهای بالا صدق می‌کند (فیلد optional، نسخه جدید event).
    • اگر فقط text log/old-value/new-value است،
      باید قرارداد logging را update کنی، ولی گذشته همان‌طور باقی می‌ماند.


۱. حداقل ستون‌های معمول برای هر Event

برای هر رویداد (در یک ردیف):

  • EventId (کلید یکتا)
  • AggregateId (مثلاً WalletId یا OrderId)
  • Sequence / Version (شماره ترتیبی event برای آن aggregate)
  • EventType (نام رویداد: OrderPlaced, MoneyDeposited, …)
  • OccurredAt (زمان وقوع)
  • Data (payload)، این معمولاً JSON است.

پس جواب مستقیم به سوالت:

  • نه «هر مقدار یک ستون جدا» برای کل بادی event،
  • نه «فقط یک JSON گنده بدون متادیتا».

مدل رایج: ترکیبی

  • متادیتا (Id, AggregateId, Version, Type, Time) ستون جدا
  • محتوای رویداد (Data) به‌صورت JSON (یا Binary/JSONB و …).

۲. چرا payload معمولاً JSON است؟

چون:

  • schema event در طول زمان تغییر می‌کند،
  • انواع eventها مختلف‌اند (فیلدهای متفاوت)،
  • JSON (یا معادلش) انعطاف می‌دهد بدون اینکه هر نوع event جدول جدا بخواهد.

اگر هر فیلد event را ستون جدا بزنی:

  • باید برای هر نوع رویداد اسکیمای DB جدا بسازی/عوض کنی،
  • نسخه‌بندی و تکامل بسیار سخت‌تر می‌شود.

۳. استثنا: projectionها و read modelها

  • برای read modelها و projectionها، معمولاً schema relational / ستونی کامل‌تر می‌سازی (هر فیلد در ستون جدا) چون آن‌جا روی query و گزارش تمرکز داری.
  • Event Store اما روی «ذخیره دنباله event» و replay متمرکز است، نه query تحلیلی ریزفیلدی.

پس:

  • Event Store → مدل ترکیبی (ستون‌های متادیتا + payload JSON)
  • Read DB / Reporting → schema ستونی و کاملاً نرمال/denormal بر اساس نیاز query.


۱. ماتریس تصمیم برای مدل داده (State-based vs Event-based)

برای هر دامین/aggregate، معمار باید این سؤال را جواب بدهد:

این بخش را state-based نگه دارم یا event-based (Event Sourcing)، یا ترکیبی؟

کتاب عملاً چند معیار (غیررسمی) می‌دهد:

الف) اهمیت تاریخچه

  • اگر فقط آخرین وضعیت مهم است:
    • CRUD معمولی + شاید audit log کافی است.
  • اگر سیر تغییرات مهم است (مالی، حقوقی، dispute، تحلیل رفتار):
    • event-driven + logging جدی،
    • و شاید Event Sourcing برای هسته.

ب) پیچیدگی منطق

  • اگر business rule ساده است (Create/Update/Delete با validation‌های معمولی):
    • state-based ساده‌تر و ارزان‌تر است.
  • اگر تصمیم‌گیری شدیداً وابسته به «ترتیب و نوع رخدادهای قبلی» است:
    • Event Sourcing طبیعی‌تر می‌شود (aggregate تاریخ خودش را می‌فهمد).

ج) تعداد و تنوع Projection / Read Model

  • اگر فقط یک نمای ساده از داده لازم داری (فرم‌ها و چند گزارش عادی):
    • state-based + چند read model کافی است.
  • اگر می‌دانی چندین projection مختلف، گزارش، تحلیل، UI خاص و… روی همان دامین خواهی ساخت:
    • Event Sourcing (با یک event stream) اجازه می‌دهد projectionهای مختلف را از یک منبع بسازی.

د) بلوغ تیم و سازمان

  • اگر تیم هنوز:
    • روی مفاهیم basic distributed systems در حال رشد است،
    • tooling Event Store و observability ندارد،
    • و زمان یادگیری محدوده، → Event Sourcing احتمالاً جای بدی برای شروع است.
  • اگر تیم:
    • روی event-driven و messaging مسلط است،
    • کف کد تمیز و تست‌محور دارد،
    • و دامین مناسبی پیدا شده، → می‌توانی هسته‌ای کوچک را Event Sourcing کنی.

۲. ترکیب رویکردها در یک سیستم

کتاب به‌وضوح می‌گوید:

  • معماری خوب یعنی ترکیب رویکردها بر اساس نیاز، نه تعصب روی یکی.

مثلاً در یک سیستم:

  • از Event Sourcing برای:
    • کیف پول/حساب مالی،
    • lifecycle قرارداد یا سفارش حساس،
  • از CRUD ساده + audit log برای:
    • پروفایل کاربر، تنظیمات، کاتالوگ ساده،
  • از CQRS + read model برای:
    • داشبوردها، صفحات لیست و گزارش‌ها،
  • از event-driven (بدون Event Sourcing) برای:
    • همگام‌سازی داده بین سرویس‌ها، ارسال ایمیل، نوتیفیکیشن، integration.

به‌عنوان معمار، وظیفه‌ات این است که برای هر بخش سیستم پاسخ بدهی:
«این‌جا کدام الگو با trade-offهایش بهینه است؟»

و آن را در ADR ثبت کنی:

  • Context: چه دامین/بخشی، چه نیازهایی؟
  • Options: CRUD، CQRS+Read Model، Event-Driven ساده، Event Sourcing.
  • Decision و Consequences: چرا یکی را انتخاب کردی، هزینه‌هایش چیست.


۱. Integration بین سرویس‌ها: Sync در برابر Async

کتاب، بعد از این‌همه بحث روی داده و event، دوباره روی این سؤال ساده اما مهم برمی‌گردد:

«وقتی دو سرویس باید با هم حرف بزنند، چه زمانی sync و چه زمانی async؟»

Sync Call (مثلاً HTTP/gRPC)

ویژگی‌ها:

  • در لحظه جواب می‌خواهی.
  • اگر سرویس مقابل down یا کند باشد، caller هم گیر می‌کند.
  • Circuit breaker, timeout, retry خیلی مهم می‌شوند.

خوب برای:

  • عملیات حیاتی که باید بلافاصله نتیجه‌شان معلوم شود
    (مثلاً authorize کردن پرداخت، چک کردن credential).

بد برای:

  • chainهای طولانی (چند سرویس پشت هم)،
  • جایی که availability خیلی مهم است و قطعیِ یک سرویس نباید کل سیستم را بخواباند.

Async Messaging / Event

ویژگی‌ها:

  • caller فقط پیام می‌فرستد و منتظر جواب مستقیم نیست.
  • queue/broker بین‌شان است؛ مصرف‌کننده هر وقت توانست پیام را پردازش می‌کند.
  • Coupling زمانی کمتر است.

خوب برای:

  • کارهایی که:
    • می‌توانند «با تأخیر کم» انجام شوند (ارسال ایمیل، به‌روزرسانی read model، sync با سیستم خارجی)،
    • و نیاز به scale / resiliency بالاتر دارند.

بد برای:

  • جاهایی که UX/بیزینس توقع پاسخ فوری دارد و اطراف آن را نتوانی خوب طراحی کنی.

کتاب می‌خواهد تو به‌عنوان معمار، برای هر رابطه سرویس–سرویس، آگاهانه بگویی:

  • این‌جا sync چون…
  • آن‌جا async چون…

۲. الگوهای Integration: Orchestration vs Choreography (جمع‌بندی عملی)

قبلاً مفهومی گفتیم؛ حالا از دید تصمیم عملی:

Orchestration (سرویس هماهنگ‌کننده مرکزی)

  • یک سرویس/workflow engine:
    • ترتیب callها را مدیریت می‌کند،
    • خطاها را handle می‌کند،
    • state کل workflow را می‌داند.

مزیت:

  • یک نقطه واضح برای فهمیدن «الان کجای فرآیندیم؟»،
  • مدیریت خطا و compensating transaction ساده‌تر.

عیب:

  • coupling به orchestrator؛
  • ممکن است به bottleneck تبدیل شود.

Choreography (هر سرویس فقط eventها را می‌شناسد)

  • هیچ مرکز هماهنگی واحدی نیست؛
  • سرویس‌ها بر اساس eventها واکنش نشان می‌دهند؛
  • جریان کار به صورت غیرمتمرکز ساخته می‌شود.

مزیت:

  • کم‌کوپل‌تر، مقیاس‌پذیری بهتر، alignment با EDA.

عیب:

  • سخت‌تر برای debug، tracing، فهم تصویر بزرگ workflow.

کتاب می‌گوید:

  • اغلب سیستم‌های بزرگ، ترکیبی از این دو هستند:
    • برخی workflowهای حساس با orchestrator،
    • بقیه تعاملات با choreography ساده (event-driven).

۳. تصمیم معماری Integration برای سیستم تو

پیام این بخش برای تو:

  • برای هر use case مهم (مثل: ثبت سفارش، بستن تیکت، ثبت پرداخت)،
    باید صریحاً بگویی:
    • زنجیره سرویس‌ها چطور اجرا می‌شود؟
    • کجا sync، کجا async؟
    • orchestrator داریم یا choreography؟

و این را در قالب:

  • sequence/state diagram (ذهنی/مدل)،
  • و در نهایت ADR ثبت کنی.

این تصمیم‌ها مستقیم روی:

  • latency،
  • availability،
  • خطاپذیری (resilience)،
  • و پیچیدگی تیمی/کدی اثر می‌گذارند.


تصمیمات استقرار که معماری را می‌سازند (یا می‌شکنند)

کتاب تأکید می‌کند:

  • معماری فقط دیاگرام کلاس و سرویس نیست؛
  • نحوه‌ی استقرار روی زیرساخت (سرورها، کانتینرها، شبکه، دیتابیس‌ها)
    می‌تواند کوپلینگ و مرز کوآنتوم‌ها را تقویت یا نابود کند.

۱. Shared Infrastructure = Shared Fate

وقتی چند سرویس:

  • روی یک دیتابیس فیزیکی،
  • یک message broker،
  • یک runtime/کلستر مشترک،

متکی‌اند، از دید availability و عملیات:

  • یک failure در آن زیرساخت، همۀ آن سرویس‌ها را می‌خواباند؛
  • یعنی از نظر عملیاتی آن‌ها یک کوآنتوم مشترک می‌شوند،
    حتی اگر کدشان جدا باشد.

نمونه‌ها:

  • چند سرویس، یک SQL Server یا یک Mongo instance؛
  • چند سرویس، یک Kafka/RabbitMQ؛
  • چند سرویس، یک monolithic app server.

کتاب می‌گوید:

  • اگر shared زیرساخت داری، حواست باشد که:
    • SLA واقعی همه آن سرویس‌ها به SLA زیرساخت مشترک گره خورده،
    • و در طراحی availability و failure mode باید آن را لحاظ کنی.

۲. استقرار مستقل = کوآنتوم مستقل (از نظر عملیات)

اگر می‌گویی:

  • «این سرویس/ماژول یک کوآنتوم مستقل است»،
    باید بتوانی آن را:

    • جدا deploy کنی،
    • جدا scale کنی،
    • و failure آن بقیه را نخواباند.

از دید زیرساخت:

  • یعنی:
    • دیتابیس خودش،
    • کانتینر/سرویس خودش،
    • queue/broker خودش (یا حداقل کانفیگ و topic جدا)،
    • config و secrets خودش.

کتاب می‌خواهد این را به ذهن تو بیاورد که:

  • وقتی درباره کوآنتوم و استقلال صحبت می‌کنی،
    باید استقرار و زیرساخت را هم جزو طراحی معماری بدانی، نه لایه‌ی جدا.


۱. تفاوت مرز منطقی (کد) و مرز عملیاتی (استقرار)

کتاب می‌گوید:

  • ممکن است در کد، ماژول‌ها و سرویس‌ها را خیلی قشنگ جدا کرده باشی،
  • اما اگر در استقرار:
    • همه روی یک پروسه،
    • یک کانتینر،
    • یا یک سرور دیتابیس باشند،
      آن‌ها از نظر فالت، SLA و عملیات هنوز به‌هم چسبیده‌اند.

مثال ساده

  • سه سرویس: Order, Inventory, Notification
  • در کد:
    • پروژه‌های جدا،
    • ماژول‌های جدا،
    • حتی DB schema جدا.
  • در استقرار:
    • هر سه در یک container یا یک process اجرا می‌شوند،
    • یا همه به یک DB سرور وابسته‌اند.

نتیجه:

  • اگر آن process یا آن DB سرور down شود، هر سه با هم down می‌شوند.
  • از دید کوآنتوم عملیاتی، این‌ها هنوز یک واحدند.

پیام کتاب:
وقتی می‌گویی «سرویس مستقل»، باید فکر کنی:

  • مستقل در کد؟
  • مستقل در استقرار؟
  • مستقل در هر دو؟

۲. الگوهای معمول استقرار و اثرشان روی معماری

کتاب چند حالت تیپیک را غیرمستقیم مطرح می‌کند (من خلاصه‌شان می‌کنم):

حالت ۱: Monolith Deployment

  • همه ماژول‌ها در یک process/deployable واحد.
  • یک DB (شاید با چند schema).
  • ساده‌ترین از نظر عملیات، بیشترین coupling عملیاتی.

مناسب:

  • تیم کوچک،
  • سیستم نه‌چندان پیچیده،
  • جایی که سادگی عملیاتی مهم‌تر از استقلال است.

حالت ۲: Modular Monolith روی یک Deployment

  • از بیرون یک executable / container،
  • از داخل ماژول‌بندی قوی (پکیج‌های دامین، API داخلی واضح)،
  • هنوز یک کوآنتوم عملیاتی.

مناسب:

  • جایی که می‌خواهی برای آینده‌ی سرویس‌محور آماده شوی،
  • ولی complexity استقرار distributed را فعلاً نمی‌خواهی.

حالت ۳: چند سرویس، shared زیرساخت

  • چند process/container،
  • ولی:
    • یک DB سرور مشترک،
    • یک broker مشترک،
    • config و runtime مشترک.

نتیجه:

  • استقلال نسبی در release/cycle،
  • ولی failure زیرساختی مشترک.

خطر:

  • راحت است که فکر کنی «ما microservice داریم» درحالی‌که از نظر availability هنوز نزدیک به یک کوآنتوم بزرگ هستی.

حالت ۴: چند کوآنتوم واقعاً مستقل

  • هر کوآنتوم:
    • دیتابیس خودش،
    • runtime و scale خودش،
    • pipeline/queue خودش (یا partition/cluster جدا)،
  • اشتراک فقط در لایه شبکه (و شاید چند سرویس زیرساختی بسیار پایدار).

اینجا:

  • واقعا می‌توانی بگویی failure یک کوآنتوم بقیه را از کار نمی‌اندازد،
  • SLA و scale هر بخش را جدا طراحی می‌کنی.


استقرار و «SLA / Non‑Functional Requirements» به‌عنوان محرک معماری

کتاب می‌گوید قبل از این‌که شکل استقرار و تعداد کوآنتوم‌ها را انتخاب کنی، باید صریح بدانی:

  • SLAها و کیفیت‌های غیرمنابعی (NFRها) چی هستند؟

چند مورد کلیدی:

  1. Availability (دردسترس‌بودن)
    • اگر یک زیرسیستم باید ۹۹.۹۹٪ باشد ولی بقیه نه،
      باید او را در کوآنتوم/استقرار جدا بگذاری،
      تا failure بقیه نگیردش پایین.
  2. Scalability (مقیاس‌پذیری)
    • بخش‌هایی با بار بالا (مثلاً جستجو، ثبت سفارش)
      باید بتوانند مستقل scale شوند؛
    • shared DB یا shared process این امکان را محدود می‌کند.
  3. Performance / Latency
    • chainهای sync طولانی و shared resourceها latency را بالا می‌برند؛
    • برای مسیرهای بحرانی، شاید لازم باشد:
      • کوتاه شوند،
      • یا به async + read model تبدیل شوند.
  4. Fault Isolation
    • اگر بخش «گزارش‌گیری» سنگین باشد اما با crash آن، عملیات اصلی نباید بخوابد،
      پس باید در کوآنتوم جدا deploy شود (DB جدا، process جدا).

کتاب نتیجه می‌گیرد:

  • طراحی استقرار = ترجمه‌ی NFRها به:
    • تعداد و نوع کوآنتوم‌ها،
    • نحوه استفاده/عدم‌استفاده از shared resourceها،
    • الگوی ارتباط (sync/async).

این یعنی تو، به‌عنوان Software Architect:

  • قبل از تصمیم «چند سرویس» و «چطور deploy کنیم»،
    باید با بیزینس و تیم روی SLA/NFRها شفاف شوی،
    و بعد معماری و استقرار را بر همان اساس شکل دهی.


Fitness Function برای معماری (به‌ویژه داده، کوپلینگ، استقرار)

ایده کتاب:

  • معماری فقط تصمیم روی کاغذ نیست؛
  • باید قابل‌چک‌کردن خودکار باشد که سیستم واقعاً طبق آن تصمیم پیش می‌رود یا نه.

این چک خودکار = Fitness Function.

۱. Fitness Function یعنی چه؟

  • یک آزمون/چک خودکار (یا نیمه‌خودکار) که یک «ویژگی معماری» را می‌سنجد:
    • آیا سرویس‌ها هنوز به DB مشترک ممنوع وصل نشده‌اند؟
    • آیا latency یک use case در حد SLA هست؟
    • آیا dependency بین ماژول‌ها از حدی بیشتر نشده؟
  • اگر نقض شود، زنگ خطر معماری به صدا در می‌آید.

به زبان ساده:
تست واحد برای تصمیم معماری.

۲. مثال‌های Fitness Function روی چیزهایی که تا حالا گفتیم

کتاب می‌خواهد این جنس چک‌ها را در سیستم بیاوری، مثلاً:

1) Data Ownership / Shared DB

  • تصمیم معماری:
    «هیچ سرویس دیگری حق ندارد مستقیم به جدول‌های domain X وصل شود.»
  • Fitness Function:
    • اسکریپت/ابزار که:
      • connection stringها و queryها را اسکن کند،
      • یا schema و permissionها را چک کند،
    • اگر سرویس دیگری به آن schema/جدول وصل شود، تست fail شود.

2) Coupling بین سرویس‌ها

  • تصمیم:
    «سرویس A فقط از طریق event با سرویس B حرف می‌زند، نه از طریق call مستقیم.»
  • Fitness Function:
    • آنالیز کد/تست integration که call مستقیم B از داخل A را detect کند.
    • اگر اضافه شد، build fail کند.

3) Latency و SLA

  • تصمیم:
    «مسیر ثبت سفارش باید زیر X میلی‌ثانیه بماند.»
  • Fitness Function:
    • تست performance اتوماتیک (در CI یا محیط staging) که این سناریو را بارها اجرا کند،
    • اگر latency از حد بالاتر رفت، هشدار/Fail.

4) Eventual Consistency / Read Model

  • تصمیم:
    «تأخیر بین write و به‌روزرسانی read model حداکثر N ثانیه است.»
  • Fitness Function:
    • تست/مانیتور که:
      • زمان آخرین event پردازش‌شده در read model را با زمان فعلی مقایسه کند،
      • اگر lag بیشتر از N شد، alert.

۳. چرا این مهم است؟

کتاب می‌گوید:

  • معماری بدون Fitness Function، به‌مرور زمان فرسوده می‌شود:
    • یک دولوپر جدید مستقیم به DB مشترک وصل می‌شود،
    • یکی دیگر sync call اضافه می‌کند،
    • الگوی event-driven دور زده می‌شود،
    • و کوپلینگ‌ها برمی‌گردند.

Fitness Function:

  • این «فرسودگی آرام» را خودکار تشخیص می‌دهد؛
  • معمار را از حالت فقط «دیاگرام‌کش» به «طراح سیستم خودکنترل» تبدیل می‌کند.


۱. معماری = جریان مداوم تصمیم‌گیری، نه یک طراحی اولیه

کتاب تأکید می‌کند:

  • معماری نرم‌افزار یک فعالیت مداوم است:
    • هر فیچر بزرگ جدید،
    • هر تغییر در تیم‌ها،
    • هر تغییر در SLA یا حجم ترافیک
      → تصمیم‌های معماری جدید می‌طلبد.

نقش معمار:

  • شناسایی این نقاط تصمیم،
  • روشن کردن گزینه‌ها و trade‑offها،
  • مستندسازی (ADR)،
  • و کمک به اجرا و کنترل (با Fitness Function، review، pairing).

۲. نقش معمار در کنار تیم، نه بالای سر تیم

معمار در نگاه کتاب:

  • «معمار در برج عاج» نیست؛
  • با تیم‌ها کار می‌کند، کد را می‌خواند، گاهی خودش PoC و spike می‌نویسد؛
  • در تصمیم‌های کلیدی:
    • زبان مشترک (مثل همین مفاهیم: کوآنتوم، Saga، CQRS، Event) می‌دهد،
    • و کمک می‌کند تیم خودش تصمیم را بفهمد و حمل کند.

خروجی ملموس:

  • ADRها،
  • نمودارهای به‌روز،
  • Fitness Functionها،
  • و مهم‌تر از همه: درک مشترک تیم از این‌که «چرا معماری این‌طور است».

۳. چگونه از این مفاهیم در سیستم فعلی‌ات استفاده کنی؟

کتاب در نهایت می‌خواهد تو:

  • برای سیستم فعلی‌ات:
    • کوآنتوم‌ها را روی کاغذ/ذهن مشخص کنی،
    • وضعیت داده و مالکیتش را شفاف کنی،
    • تصمیم بگیری کجا sync / async، کجا CQRS/Read Model، کجا فقط CRUD،
    • استقرار و shared resourceها را ببینی،
    • و برای هر بخش مهم، یک یا چند Fitness Function تعریف کنی.

به‌عنوان Software Architect:

  • از این مفاهیم برای:
    • طراحی سیستم جدید،
    • و مهم‌تر، تکامل سیستم فعلی استفاده می‌کنی:
      • اول نقطه‌های درد (coupling بد، shared DB، latency، SLA) را پیدا می‌کنی،
      • بعد تصمیم‌های معماری هدف‌دار می‌گیری،
      • و مسیر migration را مرحله‌ای طراحی می‌کنی.


بخش ۱: مهاجرت از مانولیت به معماری سرویس‌محور – اصول کلی

این بخش کتاب روی یک پیام می‌چرخد:

«مهاجرت = تکامل مرحله‌ای، نه بازنویسی کامل»

۱. چرا Big Bang Rewrite خطرناک است؟

کتاب تأکید می‌کند:

  • بازنویسی کامل سیستم (از صفر، با معماری جدید) تقریباً همیشه:
    • طولانی‌تر از حد انتظار می‌شود،
    • هم‌زمان باید سیستم قدیمی را هم نگه داری،
    • و ریسک شکست/تاخیر بسیار بالاست.

در عوض:

  • باید سیستم را به‌تدریج از وضعیت فعلی به وضعیت جدید ببری:
    • هر بار یک دامین/بخش کوچک،
    • با گام‌های قابل برگشت و قابل پایش.

۲. نقطه‌ی شروع مهاجرت: شناخت مانولیت

قبل از هر کاری:

  • مانولیت را از لحاظ:
    • دامین‌ها،
    • داده‌ها،
    • وابستگی‌ها (کد و دیتابیس)،
    • و الگوهای تغییر (history گیت)
      بررسی می‌کنی.

خروجی این مرحله:

  • «نقشه‌ی معماری فعلی»:
    • کدام قسمت‌ها به‌هم چسبیده‌اند،
    • چه جاهایی سرویس/ماژول واضح‌اند،
    • کدام جدول‌ها shared و خطرناک‌اند.

این همان چیزی است که تو قبلاً با مفاهیم کوآنتوم/Change/Team/Data یاد گرفتی؛ حالا در مهاجرت از آن استفاده می‌شود.

بخش ۲: انتخاب کاندیدهای مناسب برای جداسازی (اولین سرویس‌ها)

کتاب می‌گوید:

«اول، بخش‌هایی را جدا کن که هم ریسک کمتر، هم ارزش بیشتر دارند.»

معیارهای انتخاب:

  1. مرز دامین نسبتاً واضح
    • مثلاً: Billing، Reporting، Catalog، Notification.
    • جایی که با بقیه منطق عمیق درهم‌تنیده ندارد.
  2. Data Ownership قابل تعریف
    • می‌توانی بگویی این سرویس صاحب این بخش از دیتاست؛
    • نه جایی که کل DB هنوز شدیداً مشترک است.
  3. ارزش بیزینسی برای مستقل‌شدن
    • نیاز به scale خاص،
    • SLA متفاوت،
    • یا سرعت تغییر بالاتر نسبت به بقیه.
  4. کمترین وابستگی UI و مسیرهای core
    • برای شروع، بهتر است جایی را جدا کنی که اگر کمی latency اضافه شد یا model کمی تغییر کرد، کل محصول نسوزد.

نتیجه:

  • مهاجرت را از «لبه‌ها» یا «دم بیرونی» سیستم شروع می‌کنی
    (مثلاً گزارش‌گیری، نوتیفیکیشن، یک ماژول خاص)،
    نه از قلب پیچیده‌ترین قسمت.


۱) الگوی Strangler: جدا کردن یک بخش از مانولیت

ایده:
به‌جای این‌که یک‌باره مانولیت را تکه‌تکه کنی، روی یک دامین تمرکز می‌کنی و آن را آرام‌آرام بیرون می‌کشی، در حالی که بقیه سیستم سر جایش است.

مراحل مفهومی Strangler

۱) مسیر ورودی را کنترل کن

  • یک نقطه‌ی ورودی (Gateway / API / Reverse Proxy) جلوی مانولیت می‌گذاری.
  • همه درخواست‌ها اول از آن رد می‌شوند.

۲) یک دامین را هدف بگیر

  • مثلاً بخش Reporting یا Notification.

۳) پیاده‌سازی جدید کنار قدیم

  • یک سرویس/ماژول جدید برای آن دامین می‌سازی.
  • Gateway را طوری تنظیم می‌کنی که:
    • درخواست‌های مربوط به این دامین را به سرویس جدید بفرستد،
    • بقیه درخواست‌ها هنوز به مانولیت بروند.

۴) کم‌کم رفتار/داده را منتقل کن

  • هرچه تعداد use caseهای آن دامین در سرویس جدید بیشتر شود،
    نقش مانولیت در آن حوزه کوچک‌تر می‌شود.

۵) خاموش کردن بخش قدیمی

  • وقتی برای آن دامین دیگر نیازی به کد مانولیت نبود،
    مسیرهای قدیمی را حذف و کد مربوطه را از مانولیت پاک می‌کنی.

این الگو کمک می‌کند:

  • همیشه یک سیستم کارا داشته باشی؛
  • مهاجرت را feature‑به‑feature انجام بدهی؛
  • rollback همیشه ممکن باشد (می‌توانی traffic را برگردانی به مانولیت).

۲) مهاجرت تدریجی دیتابیس مانولیت

کتاب روی این تأکید دارد که شکستن DB سخت‌ترین قسمت مهاجرت است. رویکرد کلی:

گام ۱: مرز منطقی در خود DB

  • جداول را بر اساس domain دسته‌بندی می‌کنی (Customer, Order, Billing, …).
  • schemaهای منطقی/نام‌گذاری را طوری می‌چینی که این مرزها واضح شود.
  • cross‑FK و joinها را پیدا می‌کنی (نقاط کوپلینگ خطرناک).

گام ۲: لایه دسترسی (Anti‑Corruption Layer داده‌ای)

  • به‌جای این‌که همه مستقیم به جداول دست بزنند،
    برای هر domain یک «لایه دسترسی رسمی» تعریف می‌کنی:
    • repository/domain API/سرویس داخلی.
  • از این به بعد:
    • فقط این لایه با جدول‌های domain صحبت می‌کند؛
    • بقیه کد از طریق این لایه.

این کار دو نتیجه دارد:

  • dependencyها متمرکز می‌شوند؛
  • وقتی خواستی domain را جدا کنی، فقط باید این لایه را عوض کنی، نه همه جا.

گام ۳: حذف یا شل‌کردن cross‑FK و join

  • FK بین domainها را کم‌کم برمی‌داری؛
  • به‌جای FK، فقط Id نگه می‌داری؛
  • validation و joinهای پیچیده را:
    • یا در لایه application/domain انجام می‌دهی،
    • یا به read model/reporting DB منتقل می‌کنی.

این گام سخت ولی ضروری است:
تا وقتی DB داخلی‌ات مثل «یک schema عظیم با FKهای everywhere» است،
جدا کردن فیزیکی domainها بسیار پرریسک است.

گام ۴: انتقال فیزیکی domain به DB جدا

وقتی:

  • schema منطقی domain تمیز شد؛
  • لایه دسترسی رسمی داری؛
  • cross‑FKها را کنترل کرده‌ای؛

می‌توانی:

  • برای آن domain یک DB / instance جدا بسازی؛
  • لایه دسترسی را طوری تغییر دهی که به DB جدید وصل شود؛
  • بقیه کد حتی متوجه این جابه‌جایی نشود (چون از آن لایه استفاده می‌کرد).

از این‌جا به بعد:

  • اشتراک داده با بقیه domainها از طریق:
    • API, event, read model است؛ نه join مستقیم.


۱. سبک‌های مختلف نقش معمار (Architect Styles)

کتاب می‌گوید «معمار» یک نقش واحد و ثابت نیست؛ بسته به سازمان و محصول، چند سبک رایج داریم:

الف) Solution / Application Architect

  • نزدیک به یک یا چند سیستم مشخص کار می‌کند.
  • تمرکزش روی:
    • تصمیم‌های روزمره معماری آن محصول،
    • همراهی تیم در طراحی و کدنویسی،
    • ADRها، Diagramها، Fitness Functionهای همان سیستم.
  • در تیم تو (به‌عنوان TL/معمار) عملاً همین نقش را بازی می‌کنی:
    • هم در کد دست داری،
    • هم در تصمیم‌های سرویس‌بندی، داده، استقرار.

ب) Enterprise / Organization Architect

  • روی کل سبد سیستم‌های سازمان نگاه می‌کند:
    • استانداردهای کلان (Security, Integration, Tech stackهای اصلی)،
    • هماهنگی بین تیم‌ها و سیستم‌ها،
    • نقشه راه معماری سازمان (Roadmap).
  • با معماران محصول (Solution Architectها) کار می‌کند تا:
    • سبک‌ها و تصمیم‌ها متناقض نشوند،
    • اشتراک درست (و نه بی‌حساب) بین تیم‌ها تعریف شود.

ج) Architect as Coach / Enabler

  • به‌جای این‌که خودش همه تصمیم‌ها را بگیرد،
    تیم‌ها را در تصمیم‌گیری توانمند می‌کند:
    • مفهوم می‌دهد (مثل همین کوآنتوم، Saga، CQRS…)،
    • همراهی در Design Sessionها،
    • کمک به نوشتن ADR توسط خود تیم‌ها.

کتاب بر این سبک خیلی تأکید دارد:

  • معماری در سازمان‌های مدرن، جمعی است،
  • معمار قرار نیست «پلیس دیزاین» صرف باشد،
    بلکه باید محیطی بسازد که تیم‌ها بتوانند تصمیم‌های معماری خوب بگیرند و نگه دارند.

۲. چطور این مفاهیم را در سازمان جا بیندازی؟

چند نکته‌ی عملی که کتاب (مستقیم یا غیرمستقیم) منتقل می‌کند:

۱) با یک سیستم/تیم شروع کن، نه با «استاندارد کل سازمان»

  • مثلاً:
    • روی سیستم فعلی‌ات:
      • کوآنتوم‌ها را مستند کن،
      • ۲–۳ ADR جدی برای داده / استقرار / CQRS بنویس،
      • ۱–۲ Fitness Function ساده اضافه کن.
  • بعد:
    • این تجربه را با بقیه تیم‌ها به‌اشتراک بگذار،
    • به‌جای دیکته‌کردن، نشان بده که چطور به‌نفع delivery و نگه‌داری است.

2) جلسات معماری را از «بحث سلیقه‌ای» به «تصمیم مستند» تبدیل کن

  • هر بحث معماری مهم:
    • یک Context،
    • چند Option با trade‑off،
    • یک Decision،
    • و عواقب (Consequences) دارد.
  • این فرمت ADR کمک می‌کند:
    • بحث‌ها هدف‌دار شوند،
    • تصمیم‌ها قابل ردگیری باشند،
    • معمار بعدی بفهمد «چرا این‌طور شد».

3) Fitness Function را وارد CI/CD کن

  • به‌مرور:
    • تست‌های معماری (مثلاً منع دسترسی مستقیم به DB دیگر، منع call مستقیم بین دو سرویس خاص، حداکثر latency)
      را وارد pipeline کنی.
  • این کار:
    • فشار مراقبت از معماری را از دوش معمار برمی‌دارد و
    • آن را به یک نیاز فنی/کیفی مشترک تیم تبدیل می‌کند.


۱. ابزارهای تحلیل تاریخچه کد (Version Control Mining)

کارشان: کمک به محور Change (کجاها با هم تغییر می‌کنند؟)

نمونه کارهایی که باید بکنی:

  • استخراج از گیت/… که:
    • کدام فایل‌ها/ماژول‌ها زیاد در یک commit عوض می‌شوند؛
    • کدام بخش‌ها بیشترین نرخ تغییر را دارند.
  • خروجی:
    خوشه‌هایی از کد که عملاً با هم تغییر می‌کنند → کاندید یک ماژول/کوآنتوم.

نوع ابزار:

  • اسکریپت روی Git log، پلاگین‌های mining، گزارش‌های ساده‌ای که خودت بنویسی.

۲. ابزارهای تحلیل وابستگی (Dependency Analysis)

کارشان: کمک به دیدن Coupling ساختاری در کد.

چه می‌کنند:

  • گراف وابستگی بین:
    • پکیج‌ها، namespaceها، ماژول‌ها، سرویس‌ها را می‌کشند؛
  • حلقه‌ها/وابستگی‌های ناخواسته را نشان می‌دهند:
    • مثلاً ماژول UI به Domain، Domain به Infrastructure، ولی برعکس هم هست (حلقه).

کاربرد:

  • پیدا کردن مرزهای ضعیف،
  • بررسی این‌که آیا معماری لایه‌ای/ماژولار واقعاً رعایت شده یا فقط روی کاغذ است.

نوع ابزار:

  • ابزارهای تحلیل استاتیک (Static Analysis)،
  • پلاگین‌های معماری در IDE/CI،
  • حتی نمودارهای dependency که خودت تولید می‌کنی.

۳. ابزارهای Observability (Logging, Tracing, Metrics)

کارشان: کمک به محور رفتار واقعی در runtime.

چه می‌دهند:

  • Tracing توزیع‌شده:
    • ببینی یک request از کدام سرویس‌ها/ماژول‌ها عبور کرده؛
    • chainهای واقعی dependency (نه فقط تصور ذهنی).
  • Logging و Metrics:
    • درخواست‌های کند کجا هستند؛
    • کدام سرویس bottleneck است؛
    • نرخ خطا، timeout، retry،…

کاربرد معماری:

  • کشف اینکه:
    • کدام سرویس‌ها عملاً tightly coupled شده‌اند (هر درخواست یکی، دیگری را صدا می‌زند)،
    • کدام مسیرها برای SLA و scalability مهم‌ترند،
    • کجا sync را باید به async تبدیل کنی،
    • کجا جدا کردن کوآنتوم معنی‌دار است.

۴. ابزارهای CI/CD و Testing (برای Fitness Function)

کارشان: enforce کردن تصمیم‌های معماری.

چطور استفاده می‌شوند:

  • در pipeline:
    • تست‌هایی می‌نویسی که:
      • اجازه ندهند یک سرویس به DB دیگری وصل شود؛
      • اجازه ندهند call مستقیم خاصی بین دو سرویس ایجاد شود؛
      • latency یک endpoint کلیدی را اندازه بگیرند؛
    • اگر نقض شوند، build شکسته شود یا هشدار داده شود.

نوع ابزار:

  • هر سیستم CI/CD (GitLab CI, GitHub Actions, Jenkins, …)
  • به‌همراه تست‌ها/اسکریپت‌هایی که خودت می‌نویسی (unit/integration/perf tests).

۵. ابزارهای مدل‌سازی و مستندسازی (ADR, Diagram)

کارشان: کمک به قابل‌دیدن و قابل‌ردیابی بودن تصمیم‌ها.

چیزهایی که باید داشته باشی:

  • قالب ساده برای ADR (متن، markdown، ابزار ویکی، هرچه تیم استفاده می‌کند).
  • جایی برای:
    • دیاگرام کانتکست،
    • دیاگرام کوآنتوم‌ها،
    • مرز داده و استقرار.

کتاب روی خود «قالب و عادت» تاکید دارد، نه روی یک نرم‌افزار مشخص.