مدیریت رویدادهای ازدسترفته (Handling Missed Events) 🔄📡
ءEndpoint سادهای که همین الان ساختیم عالی است، اما یک ضعف مهم دارد:
تابآوری (Resilience) ندارد.
یکی از بزرگترین چالشها در استریمهای Real-time، قطع شدن اتصال است.
تا زمانی که مرورگر بهصورت خودکار دوباره وصل شود، ممکن است چندین رویداد ارسال شده و از دست رفته باشند 😕
برای حل این مشکل، SSE یک مکانیزم داخلی دارد:
هدر Last-Event-ID.
وقتی مرورگر reconnect میشود، این ID را دوباره برای سرور ارسال میکند.
در NET 10. میتوانیم از نوع <SseItem<T استفاده کنیم تا داده را بههمراه متادیتاهایی مثل ID و retry interval بستهبندی کنیم.
با ترکیب یک OrderEventBuffer سادهی درونحافظهای و مقدار Last-Event-ID که مرورگر ارسال میکند، میتوانیم رویدادهای ازدسترفته را هنگام reconnect دوباره ارسال کنیم 🔁
app.MapGet("orders/realtime/with-replays", (
ChannelReader<OrderPlacement> channelReader,
OrderEventBuffer eventBuffer,
[FromHeader(Name = "Last-Event-ID")] string? lastEventId,
CancellationToken cancellationToken) =>
{
async IAsyncEnumerable<SseItem<OrderPlacement>> StreamEvents()
{
// 1. بازپخش رویدادهای ازدسترفته از buffer
if (!string.IsNullOrWhiteSpace(lastEventId))
{
var missedEvents = eventBuffer.GetEventsAfter(lastEventId);
foreach (var missedEvent in missedEvents)
{
yield return missedEvent;
}
}
// 2. استریم رویدادهای جدید بهمحض ورود به Channel
await foreach (var order in channelReader.ReadAllAsync(cancellationToken))
{
var sseItem = eventBuffer.Add(order); // Buffer یک ID یکتا اختصاص میدهد
yield return sseItem;
}
}
return TypedResults.ServerSentEvents(StreamEvents(), "orders");
});فیلتر کردن Server-Sent Events بر اساس کاربر 👤🔐
ءSSE روی HTTP استاندارد ساخته شده است.
چون یک درخواست GET معمولی است، زیرساخت فعلی شما بدون تغییر کار میکند:
ءSecurity 🔐: میتوانید JWT را بهصورت عادی در هدر Authorization ارسال کنید
ءUser Context 👤: میتوانید به HttpContext.User دسترسی داشته باشید و استریم را بر اساس UserId فیلتر کنید
→ فقط دادههایی که متعلق به همان کاربر هستند ارسال میشوند
مثال یک Endpoint SSE که فقط سفارشهای کاربر لاگینشده را استریم میکند:
app.MapGet("orders/realtime", (
ChannelReader<OrderPlacement> channelReader,
IUserContext userContext, // کانتکست تزریقشده شامل اطلاعات کاربر
CancellationToken cancellationToken) =>
{
// UserId از JWT توسط IUserContext استخراج میشود
var currentUserId = userContext.UserId;
async IAsyncEnumerable<OrderPlacement> GetUserOrders()
{
await foreach (var order in channelReader.ReadAllAsync(cancellationToken))
{
// فقط دادههای متعلق به کاربر احراز هویتشده ارسال میشود
if (order.CustomerId == currentUserId)
{
yield return order;
}
}
}
return Results.ServerSentEvents(GetUserOrders(), "orders");
})
.RequireAuthorization(); // Authorization استاندارد ASP.NET Core⚠️ نکته مهم:
وقتی یک پیام داخل Channel نوشته میشود، به تمام کلاینتهای متصل broadcast میشود.
این رفتار برای استریمهای per-user ایدهآل نیست.
در محیط production، احتمالاً به راهکار قویتری نیاز دارید.
مصرف Server-Sent Events در JavaScript 🌐🧠
در سمت کلاینت، نیازی به نصب حتی یک پکیج npm هم ندارید 🙌 API بومی مرورگر یعنی EventSource تمام کارهای سنگین را انجام میدهد،
از جمله reconnect خودکار و ارسال Last-Event-ID.
const eventSource = new EventSource('/orders/realtime/with-replays');
// گوش دادن به event type مشخصشده در C#
eventSource.addEventListener('orders', (event) => {
const payload = JSON.parse(event.data);
console.log(New Order ${event.lastEventId}:, payload.data);
});
// وقتی اتصال برقرار میشود
eventSource.onopen = () => {
console.log('Connection opened');
};
// پیامهای عمومی (در صورت وجود)
eventSource.onmessage = (event) => {
console.log('Received message:', event);
};
// مدیریت خطا و reconnect
eventSource.onerror = () => {
if (eventSource.readyState === EventSource.CONNECTING) {
console.log('Reconnecting...');
}
};جمعبندی 🧩✨
ءSSE در NET 10. یک نقطهی تعادل عالی است برای بهروزرسانیهای ساده و یکطرفه مثل:
داشبوردها 📊
نوتیفیکیشنها 🔔
ءProgress barها ⏳
سبک است، مبتنی بر HTTP است و بهراحتی با middlewareهای امنیتی فعلی شما ایمن میشود.
با این حال، SignalR همچنان انتخاب قدرتمند و battle-tested برای ارتباطهای دوطرفهی پیچیده یا مقیاس بسیار بالا (با backplane) باقی میماند.
هدف جایگزینی SignalR نیست ❌
هدف این است که برای کارهای ساده، ابزار سادهتری داشته باشید 🛠
سبکترین ابزاری را انتخاب کنید که مشکل شما را حل میکند.
امیدوارم مفید بوده باشد.😊
ءSSE در NET 10. یک نقطهی تعادل عالی است برای بهروزرسانیهای ساده و یکطرفه مثل:
داشبوردها 📊
نوتیفیکیشنها 🔔
ءProgress barها ⏳
سبک است، مبتنی بر HTTP است و بهراحتی با middlewareهای امنیتی فعلی شما ایمن میشود.
با این حال، SignalR همچنان انتخاب قدرتمند و battle-tested برای ارتباطهای دوطرفهی پیچیده یا مقیاس بسیار بالا (با backplane) باقی میماند.
هدف جایگزینی SignalR نیست ❌
هدف این است که برای کارهای ساده، ابزار سادهتری داشته باشید 🛠
سبکترین ابزاری را انتخاب کنید که مشکل شما را حل میکند.
امیدوارم مفید بوده باشد.😊
🧭 ۵ نکته برای گم نشدن وقتی وارد یک کدبیس موجود (و طاقتفرسا) میشی
ورود به یک کدبیس جدید میتونه مثل پریدن توی قسمت عمیق استخر باشه
(یا برای یک مهندس جونیور… وسط اقیانوس 🌊).
ممکنه ترسناک و بدون جهت به نظر برسه، اما این نکتهها کمک میکنن راهتو پیدا کنی 👇
1️⃣ خردش کن (Break It Down)
ما بهعنوان مهندس نرمافزار باید روی شکستن مسائل پیچیده تمرکز کنیم.
مشکلها رو به بخشهای کوچیکتر و قابلمدیریتتر تقسیم کن.
سعی نکن همهچیز رو یکجا بفهمی.
روی بخشی که الان روش کار میکنی تمرکز کن و کمکم دانشت رو گسترش بده.
وقتی مسئله بهاندازه کافی تجزیه بشه، با تسکهای خیلی سادهتری روبهرو میشی 🧩
2️⃣ سؤال بپرس — زیاد هم بپرس! ❓
یادت باشه هیچ سؤالی کوچیک یا بیاهمیت نیست.
بعضی وقتها آدمها در پرسیدن سؤال مردد میشن چون میترسن بیتجربه به نظر بیان.
اما واقعیت اینه که هر سؤال یک پله به سمت حرفهای شدنه.
تیمت خط نجات توئه — از تجربهشون استفاده کن 🛟
3️⃣ مسیر یادگیریت رو مستند کن 📝
برای خودت یک لاگ درست کن.
نقاشی بکش.
یادگیریها و چالشهات رو بنویس.
این کار هم کمک میکنه پیشرفتت رو ببینی، هم میتونه بعداً به یک منبع ارزشمند برای جونیورهای بعدی تبدیل بشه که جای تو قرار میگیرن.
اگه تیمت داکیومنتیشن داره، اگه دیدی قدیمی شده، با چیزهایی که یاد گرفتی آپدیتش کن 📚
4️⃣ ءPair Programming رو بپذیر 🤝
کار کردن دونفره با یک همتیمی باتجربه میتونه بازی رو عوض کنه.
بهترین پراکتیسها، میانبُرها و تلههایی که باید ازشون دوری کنی رو یاد میگیری.
فقط سعی کن خودت پشت فرمون بشینی 🚗
طوری که تو مسیریابی رو انجام بدی، نه اینکه فقط تماشاچی باشی.
5️⃣ این یک ماراتنه، نه دوی سرعت 🏃♂️
رم یکشبه ساخته نشد،
و درک کامل یک کدبیس هم همینطور.
با خودت صبور باش.
هر روز داری یک تکه از پازل رو اضافه میکنی 🧠🧩
برای همه مهندسهای جونیور 👶👩💻👨💻
یادتون باشه: هر متخصصی یک روزی مبتدی بوده.
مسئلهها رو به بخشهای قابلهضمتر تقسیم کن.
تکهتکه جلو برو 🔹🔹🔹
🚀 چگونه بدون استفاده از کتابخانههای خارجی یک Cache با کارایی بالا بسازیم
چند روز پیش داشتم به یک تکه کد نگاه میکردم که بیش از حد لازم کار انجام میداد 🤯
مطمئنم شما هم میتونید نمونهای مشابه ازش رو در اپلیکیشنهای خودتون پیدا کنید.
شاید یک فراخوانی دیتابیس باشه که باید سریعتر باشه 🐌،
یا یک API خارجی که کمکم داره برای هر درخواست ازتون هزینههای سنگین میگیره 💸.
اولین واکنش من معمولاً اینه:
👉 «خب، فقط cacheش میکنم.»
در NET.، این معمولاً یعنی استفاده از IMemoryCache
یا اضافه کردن یک distributed cache مثل Redis 🧠.
اما تا حالا شده از خودتون بپرسید واقعاً داخل این کتابخانهها چه اتفاقی میافته؟ 🤔
چرا برای ذخیرهی یک مقدار ساده در حافظه به اینهمه پیچیدگی نیاز داریم؟
برای همین، یک بعدازظهر رو صرف این کردم که یک cache با کارایی بالا رو از صفر بسازم ⏳⚙️.
من توصیه نمیکنم برای استفاده در محیط production خودتون یک کتابخانهی cache بنویسید ❌
اما من با انجام دادن یاد میگیرم ✍️.
درک این الگوها (concurrency، race condition و locking)
همون چیزیه که یک «coder» 👨💻 رو از یک مهندس نرمافزار واقعی 🧑🔧 جدا میکنه.
🧭 نقطهی شروع
من روی یک handler ساده برای تبدیل واحد ارز کار میکردم 💱.
ما برای گرفتن نرخ تبدیل ارزها از یک API شخص ثالث استفاده میکنیم 🌐.
این API نرخ تبدیل فعلی یک کد ارز مشخص
(مثل EUR، GBP، JPY) رو نسبت به USD برمیگردونه.
این پیادهسازی اولیه است 👇:
public static class CurrencyConversion
{
public static async Task<IResult> Handle(
string currencyCode,
decimal amount,
CurrencyApiClient currencyClient)
{
// Validate currency code format (3 uppercase letters)
if (string.IsNullOrWhiteSpace(currencyCode)||
currencyCode.Length != 3 ||
!currencyCode.All(char.IsLetter))
{
return Results.BadRequest(
new { error = "Currency code must be a 3-letter uppercase code (e.g., EUR, GBP)" });
}
// Validate amount (must be positive)
if (amount < 0)
{
return Results.BadRequest(new { error = "Amount must be a positive number" });
}
var rate = await currencyClient.GetExchangeRateAsync(currencyCode);
if (rate == null)
{
return Results.NotFound(
new { error = $"Exchange rate for {currencyCode} not found or API error occurred" });
}
var convertedAmount = amount * rate.Value;
return Results.Ok(new ExchangeRateResponse(
Currency: currencyCode,
BaseCurrency: "USD",
Rate: rate.Value,
Amount: amount,
ConvertedAmount: convertedAmount
));
}
}
این کد در محیط توسعهی لوکال کاملاً خوب کار میکنه ✅
اما در محیط production، اگه ۱۰۰ نفر همزمان به این endpoint درخواست بزنن 😬،
شما دارید ۱۰۰ فراخوانی شبکهی کاملاً یکسان انجام میدید 📡📡📡.
ارائهدهندهی API از شما متنفر میشه 😡
(و احتمالاً rate limit هم میشید 🚫)
و latency سیستمتون بهشدت افزایش پیدا میکنه 📈.
حالا بیاید بدون استفاده از هیچ کتابخانهی خارجی،
یک cache بسازیم تا این مشکل رو حل کنیم 🛠🧠.
یادتون باشه، این کار فقط برای یادگیری انجام میشه 🎓.
🧱 سطح 1️⃣ : اضافه کردن ConcurrentDictionary
اولین فکری که احتمالاً به ذهنت میرسد این است که نرخها را داخل یک ConcurrentDictionary ذخیره کنی 🧠.
این ساختار thread-safe است، پس در نگاه اول ابزار درستی به نظر میرسد ✅.
private static readonly ConcurrentDictionary<string, decimal> Cache = new();
// In the Handler:
if (Cache.TryGetValue(currencyCode, out var cachedRate))
{
return cachedRate;
}
var rate = await currencyClient.GetExchangeRateAsync(currencyCode);
Cache.TryAdd(currencyCode, rate.Value);
این کار قطعاً تحت بار بالا به بهبود performance کمک میکند 🚀.
چندین thread میتوانند همزمان از دیکشنری بخوانند یا در آن بنویسند بدون اینکه برنامه کرش کند 💪.
اما ConcurrentDictionary فقط از ساختار دیکشنری محافظت میکند، نه از منطق شما ⚠️.
اگر ۱۰۰ کاربر دقیقاً در یک لحظه نرخ "EUR" را درخواست کنند 👥👥👥، TryGetValue برای همهی آنها false برمیگرداند.
در نتیجه، همهشان همزمان API را صدا میزنند 📡📡📡.
این یک race condition کلاسیک است 🏁.
شما حافظه را امن کردهاید، اما از API خارجی محافظت نکردهاید ❌.
یک مشکل دیگر هم وجود دارد:
نرخها هیچوقت expire نمیشوند ⏳❌.
⏰ سطح 2️⃣: اضافه کردن انقضای Cache
نرخ ارزها برای همیشه ثابت نمیمانند 💱.
ما به راهی نیاز داریم که بعد از مدتی آنها را منقضی کنیم.
از آنجایی که ConcurrentDictionary مفهوم Time To Live (TTL) ندارد،
باید دادههایمان را wrap کنیم 📦.
// Store both the rate and the time it was created
private record CacheEntry(decimal Rate, DateTime CreatedAt);
// Our cache now stores CacheEntry objects
private static readonly ConcurrentDictionary<string, CacheEntry> Cache = new();
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
// Check: Is it there? And is it still "fresh"?
if (Cache.TryGetValue(currencyCode, out var entry) &&
(DateTime.UtcNow - entry.CreatedAt) < CacheDuration)
{
return entry.Rate;
}
حالا expiration داریم ✅.
اما در عوض، یک مشکل جدید ساختهایم 😬:
🐘 Thundering Herd (یا Cache Stampede)
هر ۵ دقیقه یکبار، وقتی cache منقضی میشود ⏱️،
تمام درخواستهای ورودی بهصورت همزمان دادهی «منقضیشده» میبینند
و همگی تلاش میکنند آن را refresh کنند 💥.
پس باید این مشکل را در مرحلهی بعدی حل کنیم.
🚦 سطح 3️⃣: حل مشکل «Cache Stampede»
برای حل این مشکل، باید مطمئن شویم که
فقط یک نفر اجازه دارد داده را بهروزرسانی کند
و بقیه منتظر بمانند ⏸️.
در #C چطور این کار را انجام میدهیم؟ 🤔
ما از SemaphoreSlim و الگویی به نام Double-Checked Locking استفاده میکنیم 🔐.
اول یک بار cache را چک میکنیم (مسیر سریع 🏃♂️)،
بعد lock میگیریم،
و سپس دوباره چک میکنیم تا ببینیم آیا در این فاصله thread دیگری cache را پر کرده یا نه.
// Basically a mutex but async-friendly
private static readonly SemaphoreSlim Lock = new(1, 1);
public static async Task<decimal> GetRateAsync(string code, CurrencyApiClient client)
{
// Fast path: No locking needed
if (Cache.TryGetValue(code, out var entry) && IsFresh(entry))
{
return entry.Rate;
}
var acquired = await Lock.WaitAsync(TimeSpan.FromSeconds(10)); // Avoid deadlocks
if (!acquired)
{
throw new Exception("Could not acquire lock to fetch exchange rate.");
}
try
{
// Double-check: Did someone else finish the API call while we waited?
if (Cache.TryGetValue(code, out entry) && IsFresh(entry))
{
return entry.Rate;
}
var rate = await client.GetExchangeRateAsync(code);
var newEntry = new CacheEntry(rate.Value, DateTime.UtcNow);
// Atomically update the cache
// This is safe because we're inside the lock
Cache.AddOrUpdate(code, newEntry, (_, _) => newEntry);
return rate.Value;
}
finally
{
// Always release the lock
Lock.Release();
}
}
این کار یک بهبود واقعی است 👍.
اما هنوز یک حس بد وجود دارد… 😐
میتوانی مشکل این کد را پیدا کنی؟ 👀
🔒 این lock مثل یک global lock رفتار میکند.
یعنی اگر یک thread در حال گرفتن نرخ "EUR" باشد،
تمام threadهای دیگر (حتی آنهایی که "JPY" میخواهند 🇯🇵)
تا پایان درخواست "EUR" بلاک میشوند ⛔️.
این مشکل به lock contention معروف است ⚠️.
در مرحلهی بعدی، این مشکل را هم حل میکنیم 🛠✨.
🚀 سطح 4️⃣: مقیاسپذیری با Keyed Locking
حرکت «حرفهای» 👌 در اینجا استفاده از Keyed Locking است 🔑.
ما برای هر ارز مشخص یک lock جداگانه ایجاد میکنیم. از آنجایی که تعداد ارزها محدود است 💱، این روش از نظر مصرف حافظه سنگین نیست.
ما به یک ConcurrentDictionary اضافه نیاز داریم تا semaphoreها را بهازای هر کد ارز نگهداری کنیم 🧵.
private static readonly ConcurrentDictionary<string, SemaphoreSlim> Locks = new();
// In the Handler:
var semaphore = Locks.GetOrAdd(currencyCode, _ => new SemaphoreSlim(1, 1));
if (!Cache.TryGetValue(currencyCode, out var cachedRate) &&
DateTime.UtcNow - cachedRate?.CreatedAt < CacheDuration)
{
var acquired = await semaphore.WaitAsync(TimeSpan.FromSeconds(10));
if (!acquired)
{
throw new Exception("Could not acquire lock to fetch exchange rate.");
}
try
{
// Fetch and update logic...
}
finally { semaphore.Release(); }
}
تنها چیزی که تغییر کرده، نحوهی گرفتن lock است 🔒.
حالا اگر یک thread در حال گرفتن نرخ "EUR" باشد 🇪🇺، threadهای دیگر که "JPY" را درخواست کردهاند 🇯🇵، بدون معطلی ادامه میدهند ⏩.
این مقیاسپذیرترین نسخه از cache ماست 📈.
اما… ⚠️
این راهحل فقط در حافظه کار میکند.
پس برای سیستمهای توزیعشده یا چندین instance سرور مناسب نیست 🌐.
همچنین چند edge case دیگر هم وجود دارد که میتوانی آنها را بهعنوان تمرین بررسی کنی 🧠.
🧩 کد نهایی
در اینجا نسخهی نهایی منطق cache را میبینی:
public static class CurrencyConversion
{
private record CacheEntry(decimal Rate, DateTime CreatedAt);
private static readonly ConcurrentDictionary<string, CacheEntry> Cache = new();
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
private static readonly ConcurrentDictionary<string, SemaphoreSlim> Locks = new();
public static async Task<IResult> Handle(
string currencyCode,
decimal amount,
CurrencyApiClient currencyClient)
{
// Validate currency code format (3 uppercase letters)
if (string.IsNullOrWhiteSpace(currencyCode)||
currencyCode.Length != 3 ||
!currencyCode.All(char.IsLetter))
{
return Results.BadRequest(
new { error = "Currency code must be a 3-letter uppercase code (e.g., EUR, GBP)" });
}
// Validate amount (must be positive)
if (amount < 0)
{
return Results.BadRequest(new { error = "Amount must be a positive number" });
}
decimal? rate;
var semaphore = Locks.GetOrAdd(currencyCode, _ => new SemaphoreSlim(1, 1));
if (!Cache.TryGetValue(currencyCode, out var cachedRate) &&
DateTime.UtcNow - cachedRate?.CreatedAt < CacheDuration)
{
var acquired = await semaphore.WaitAsync(TimeSpan.FromSeconds(10));
if (!acquired)
{
throw new Exception("Could not acquire lock to fetch exchange rate.");
}
try
{
// Double-check locking pattern: check again inside the lock
if (!Cache.TryGetValue(currencyCode, out cachedRate) &&
DateTime.UtcNow - cachedRate?.CreatedAt < CacheDuration)
{
rate = await currencyClient.GetExchangeRateAsync(currencyCode);
if (rate == null)
{
return Results.NotFound(
new { error = $"Exchange rate for {currencyCode} not found or API error occurred" });
}
Cache.AddOrUpdate(currencyCode,
_ => new CacheEntry(rate.Value, DateTime.UtcNow),
(_, _) => new CacheEntry(rate.Value, DateTime.UtcNow));
}
else
{
rate = cachedRate!.Rate;
}
}
finally
{
semaphore.Release();
}
}
else
{
rate = cachedRate!.Rate;
}
var convertedAmount = amount * rate.Value;
return Results.Ok(new ExchangeRateResponse(
Currency: currencyCode,
BaseCurrency: "USD",
Rate: rate.Value,
Amount: amount,
ConvertedAmount: convertedAmount
));
}
}
گام بعدی این است که منطق اصلی cache را به یک کلاس reusable جداگانه استخراج کنی 🧩.
به این شکل میتوانی آن را در بخشهای دیگر برنامه هم استفاده کنی 🔁.
🎯 جمعبندی (Takeaway)
چرا اصلاً این همه دردسر؟ 🤔
دیدن یک ConcurrentDictionary ساده خیلی وسوسهبرانگیز است 😌 و فکر میکنی کار تمام شده.
اما همانطور که دیدیم، فاصلهی بین «کار میکند» و «مقیاسپذیر است»
پر از edge caseهایی است که میتوانند یک سیستم production را زمینگیر کنند 💥.
وقتی از یک library استفاده میکنی، این edge caseها برایت مدیریت میشوند 🛡.
اما ساختن آن با دست خودت، به تو سه ستون اصلی کد با کارایی بالا را یاد میدهد 🏛:
🧵 Thread safety
🔒 Lock contention
🛡 Resource protection
گاهی اوقات، «کسلکنندهترین» بخشهای زیرساخت، مثل cache،
در واقع جالبترین بخشها از نظر معماری هستند 🧠✨.
و آن ۱٪ مواقعی هم هست که به یک راهحل کاملاً سفارشی نیاز داری
که هیچ libraryای ارائهاش نمیدهد 🛠.
برای همین دانستن اصول پایه واقعاً ارزشمند است.
کتابخانههای مدرنی مثل HybridCache یا FusionCache این کارها را برایت انجام میدهند 🚀،
اما درک این الگوها باعث میشود دقیقاً بدانی چرا برنامهات تحت load آنطور که میبینی رفتار میکند 📊.
🧩 5️⃣ CQRS (Command Query Responsibility Segregation)(بخش پنجم)
الگوی CQRS یک الگوی طراحی است که عملیات خواندن (Query) را از عملیات نوشتن (Command) جدا میکند 🔀.
در این الگو، برای هرکدام مدل جداگانهای استفاده میشود:
📝 مدل نوشتن (Write Model): مسئول منطق کسبوکار و تغییر دادهها
📊 مدل خواندن (Read Model): بهینهشده برای خواندن، جستجو و گزارشگیری
⚙️ نحوهی کار (How it works)
✏️ ءCommandها نمایانگر عملیاتی هستند که وضعیت سیستم را تغییر میدهند و از طریق مدل نوشتن پردازش میشوند
🧠 مدل نوشتن قوانین کسبوکار را اعمال میکند، دادهها را اعتبارسنجی میکند و تغییرات را در پایگاهداده ذخیره میکند
🔍 ءQueryها داده را از طریق مدل خواندن بازیابی میکنند، بدون اینکه تغییری در وضعیت سیستم ایجاد شود
🗄 مدل خواندن معمولاً یک دیتابیس یا ساختار دادهی جداگانه است که برای عملکرد بهتر در Query بهینه شده
🔄 تغییراتی که در مدل نوشتن ایجاد میشوند، معمولاً بهصورت asynchronous و اغلب از طریق eventها به مدل خواندن منتقل میشوند
✅ مزایا (Benefits)
🚀 امکان بهینهسازی مستقل مدلهای خواندن و نوشتن بر اساس نیازهای عملکردی هرکدام
📈 قابلیت مقیاسپذیری جداگانهی عملیات خواندن و نوشتن، متناسب با الگوی مصرف واقعی
🧩 سادهتر شدن دامنههای پیچیده با جدا کردن منطق اعتبارسنجی Command از منطق Query
⚠️ معایب (Drawbacks)
⏳ ایجاد eventual consistency بین مدل نوشتن و مدل خواندن
🏗 افزایش پیچیدگی سیستم به دلیل وجود چندین مدل و مکانیزم همگامسازی
🔧 نیاز به زیرساخت اضافه برای همگام نگه داشتن مدلهای خواندن و نوشتن
🎯 موارد استفاده (Use cases)
🖥 اپلیکیشنهایی با بار خواندن و نوشتن بسیار متفاوت که نیاز به مقیاسپذیری مستقل دارند
🧠 دامنههای پیچیده که منطق کسبوکار در عملیات نوشتن تفاوت زیادی با نیازهای گزارشگیری دارد
📊 سیستمهایی با نسبت Read به Write بالا که عملکرد Query در آنها حیاتی است
تابآوری (Resiliency): طراحی سیستمهایی که خم میشوند اما نمیشکنند!
تو کتاب فارسی نمیدونم چندم دبستان بود که داستان یه درخت بلوط تنومند بود و چند شاخه نی، داستان اینطوری پیش میرفت که درخت به ریشهها و تنه تنومندش مینازید و برای زندگی نحیف نیها دلسوزی میکرد و میگفت شماها با هر باد خم میشید و هر چقدر اون باد ضعیف هم باشه شما رو خم میکنه، اما چیزی که درخت خبر نداشت انعطافپذیری نیها در برابر بادهای خیلی تند بود که میتونستند از پسش بربیان. که یک روز یکی از اون بادها درخت رو انداخت اما همچنان نیها برقرار بودند. 🌳
دنیای واقعی ذاتاً پر از آشوب است. شبکهها ناپایدارند، سرویسها از کار میافتند، وابستگیها خطا میدهند و این اتفاقها معمولاً در بدترین زمان ممکن رخ میدهند (یا ساعت پایانی کار روز چهارشنبه یا نصفه شب).
حتی مقاومترین سیستمهای دنیا نیز از شکست مصون نیستند. به همین دلیل، تمرکز صرف بر جلوگیری از خرابی کافی نیست؛ آنچه سیستمهای مدرن را متمایز میکند، تابآوری (Resiliency) آنهاست.
در حالی که Robustness (استحکام) تلاش میکند احتمال بروز خطا را کاهش دهد، Resiliency (تابآوری) بر این اصل بنا شده است که:
«خرابی اجتنابناپذیر است؛ مهم این است که سیستم چقدر سریع و هوشمند به حالت پایدار بازمیگردد.»
🪢 تابآوری چیست؟
تابآوری در معماری نرمافزار یعنی توانایی سیستم برای ادامهٔ ارائهٔ سرویس، حتی در شرایط شکست، و بازیابی سریع با حداقل اختلال برای کاربر.
🗯 یک سیستم تابآور:
- شکست را تشخیص میدهد
- آن را مهار میکند
- اجازه نمیدهد خرابی یک جزء، کل سیستم را از کار بیندازد
و در نهایت، خود را بازیابی میکند
سیستم تابآور مانند نیزار در برابر باد است: خم میشود، اما نمیشکند؛ و پس از طوفان دوباره سرپا میایستد.
💥 دو شاخص کلیدی برای اندازه گیری تابآوری:
1️⃣ RTO – Recovery Time Objective
حداکثر زمانی که یک سرویس باید در آن بازیابی شود تا اختلال ایجادشده غیرقابلقبول تلقی نشود.
ءRTO بسته به اهمیت سرویس متفاوت است و مرز بین «اختلال قابل قبول» و «Incident» را مشخص میکند.
2️⃣ MTTR – Mean Time To Repair/Recover
میانگین زمانی که طول میکشد تا یک سرویس پس از شکست به حالت عملیاتی بازگردد.
🌪 چرا تابآوری حیاتی است؟
تابآوری مستقیماً بر تجربهٔ کاربر و درآمد کسبوکار اثر میگذارد.
در بازارهای رقابتی، کاربر تحمل:
• لودینگهای طولانی
• درخواستهای معلق
• صفحات خطای مداوم
را ندارد؛ بهخصوص در روزهای اوج ترافیک مثل جمعه سیاه یا حراجیهای بهمن ماه.
از سوی دیگر، تابآوری مزایای مهمی برای تیمهای فنی دارد:
- کاهش Incidentهای ناگهانی
- فشار کمتر on-call
- تستپذیری و پایداری بالاتر
تابآوری اگر از ابتدا در طراحی لحاظ شود، بسیار کمهزینهتر و مؤثرتر از وصلهکاری بعد از بحران است.
الگوهای کلیدی تابآوری
🔹️ Circuit Breaker: قطع ارتباط موقت یک سرویس
🔸️ Fallback: نسخه از کش یا حذف از UI
🔹️ Bulkhead: جداسازی منابع مثلا گزارشگیریها
🔸️ Redundancy: استفاده از نسخههای جایگزین مخصوصا در معماری Loosely Coupled
🔹️ Fault Tolerance در کد
🔸️ Message Queue
🔹️ Rate Limiter
📌جمعبندی نهایی
تابآوری یعنی:
شکست یک جزء ≠ شکست کل سیستم
عملکرد اصلی باید همیشه زنده بماند
قابلیتهای جانبی باید قابل حذف، جایگزینی یا تضعیف باشند
هدف نهایی: ارائهٔ حداقل تجربهٔ قابل قبول در بدترین شرایط
یا به زبان سادهتر:
هیچوقت به کاربر خطای ۵۰۰ نشان نده؛
حتی وسط طوفان، بگذار سیستم خم شود، نه بشکند.
🔗Link
Microservices Design Patterns in .NET by Trevoir Williams
این کتاب جنبههای کلیدی معماری را پوشش میدهد:
🔹️طراحی مبتنی بر دامنه (DDD). نحوهی تعریف صحیح مرزهای سرویس و استفاده از Aggregates و Value Objects برای جلوگیری از ایجاد یک "distributed monolith".
🔹️ءCommunication Patterns. نگاهی دقیق به تعامل همزمان (HTTP/gRPC) و غیرهمزمان (Message Buses، RabbitMQ).
🔹️تابآوری و زیرساخت. پیادهسازی الگوهای Circuit Breaker، API Gateway و BFF، و همچنین رویکردهای مدرن استقرار با استفاده از Docker و Kubernetes.
چیزی که به ویژه برای تمرین و مصاحبه مفید است این است که بر موضوعاتی تمرکز میکند که اغلب در مورد آنها سوال میشود. به عنوان مثال، الگوی Saga به تفصیل توضیح داده شده است و تفاوتهای بین Choreography و Orchestration و همچنین نحوه پیادهسازی تراکنشهای توزیعشده را نشان میدهد. این کتاب همچنین gRPC را برای ارتباط مؤثر سرویس به سرویس پوشش میدهد.
این کتاب به وضوح نشان میدهد که چگونه نگرانیهای متقاطع (ثبت وقایع، نظارت، پروکسی) را به یک فرآیند جداگانه منتقل کنید تا منطق کسبوکار سرویس را بیش از حد بارگذاری نکنید.
⭐️ ءExtension Members ویژگی موردعلاقهی من در C# 14
ءExtension members ویژگی موردعلاقهی من در C# 14 هستند ✨
آنها یک تکامل مدرن از extension methodها محسوب میشوند که از نسخهی C# 3.0 در زبان وجود داشتند.
این سینتکس جدید باعث میشود بتوانید نهتنها متد، بلکه property و حتی static memberها را هم به typeهای موجود اضافه کنید 🧩
در این پست، موارد زیر را بررسی میکنیم 👇
• تفاوت کلیدواژهی extension با extension methodهای سنتی
• ساخت instance extension property
• اضافه کردن static extension member به typeها
• کار با genericها و type constraintها
• مثالهای واقعی از کاربرد extension memberها
• بهترین روشها برای سازماندهی کدهای extension
🚀 بزن بریم!
🔍 تفاوت Extension Keyword با Extension Methodهای سنتی
برنامهنویسهای #C از نسخهی C# 3.0 از extension methodها استفاده میکردند تا بدون تغییر سورسکد، به typeها قابلیت جدید اضافه کنند.
در روش سنتی، شما یک static class با static methodها میساختید.
قبل از C# 14، extensionها به این شکل نوشته میشدند 👇
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string value)
{
return string.IsNullOrEmpty(value);
}
public static string Truncate(this string value, int maxLength)
{
if (string.IsNullOrEmpty(value) value.Length <= maxLength)
{
return value;
}
return value.Substring(0, maxLength);
}
}
اما C# 14 کلیدواژهی جدید extension را معرفی میکند و یک رویکرد مدرنتر ارائه میدهد 🆕
در سینتکس جدید، receiver (typeای که میخواهید extend کنید) از memberهایی که اضافه میکنید جدا میشود.
بهجای اینکه روی هر متد this بنویسید، یک extension block تعریف میکنید که receiver فقط یکبار مشخص میشود 👇
public static class StringExtensions
{
extension(string value)
{
public bool IsNullOrEmpty()
{
return string.IsNullOrEmpty(value);
}
public string Truncate(int maxLength)
{
if (string.IsNullOrEmpty(value) value.Length <= maxLength)
{
return value;
}
return value.Substring(0, maxLength);
}
}
}
🔹 در اینجا:
• ءextension، receiver را بهعنوان پارامتر میگیرد
• داخل بلاک، متدها و propertyها دقیقاً مثل memberهای واقعی type نوشته میشوند
• پارامتر value در تمام memberهای داخل بلاک در دسترس است
📌 نکتهی مهم:
سینتکس قدیمی و جدید هر دو به کد یکسانی کامپایل میشوند، بنابراین رفتارشان دقیقاً یکسان است.
سینتکس جدید اختیاری است و در کنار روش قدیمی کار میکند.
🧱 ءExtension Memberهای پشتیبانیشده
ءExtension جدید از موارد زیر پشتیبانی میکند ✅
• Methods
• properties
• Static methods
• Static properties
🧩 ساخت Instance Extension Property
ءExtension propertyها باعث میشوند کد شما خواناتر و expressiveتر شود ✨
بهجای صدا زدن متد، میتوانید از propertyهایی استفاده کنید که طبیعیتر به نظر میرسند.
مثلاً هنگام کار با collectionها، خیلی وقتها چک میکنیم که خالی هستند یا نه.
بهجای اینکه همهجا بنویسیم ()items.Any!، میتوانیم یک property مثل IsEmpty بسازیم 👇
public static class CollectionExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
public bool HasItems => source.Any();
public int Count => source.Count();
}
}
public void ProcessOrders(IEnumerable<Order> orders)
{
if (orders.IsEmpty)
{
Console.WriteLine("No orders to process");
return;
}
foreach (var order in orders)
{
// Process order
}
}
➕ افزودن Static Extension Member به Typeها
ءStatic extensionها به شما اجازه میدهند بهجای instance، مستقیماً به خود type متد یا property اضافه کنید.
این نوع extension برای factory methodها 🏭 یا utility functionها 🧰 بسیار کاربردی است.
برای ساخت static extension، از extension بدون نامگذاری receiver استفاده میکنیم 👇
public static class ProductExtensions
{
extension(Product)
{
public static Product CreateDefault() =>
new Product
{
Name = "Unnamed Product",
Price = 0,
StockQuantity = 0,
Category = "Uncategorized",
CreatedDate = DateTime.UtcNow
};
public static bool IsValidPrice(decimal price) =>
price >= 0 && price <= 1000000;
public static string DefaultCategory => "General";
}
}
حالا میتوانی این memberهای static را مستقیماً روی خود type صدا بزنی 👇
var product = Product.CreateDefault();
if (Product.IsValidPrice(999.99m))
{
product.Price = 999.99m;
}
🧬 کار با Generics و Type Constraintها
ءGeneric extensionها به شما اجازه میدهند کدی بنویسید که با چندین type کار کند.
این موضوع بهخصوص هنگام کار با collectionها یا interfaceها بسیار مفید است 📦
بیایید <IEnumerable<T را extend کنیم و قابلیت فیلتر و تبدیل اضافه کنیم 👇
public static class EnumerableExtensions
{
extension<T>(IEnumerable<T> source)
{
public IEnumerable<T> WhereNotNull() =>
source.Where(item => item != null);
public Dictionary<TKey, List<T>> GroupToDictionary<TKey>(
Func<T, TKey> keySelector) where TKey : notnull =>
source.GroupBy(keySelector)
.ToDictionary(g => g.Key, g => g.ToList());
}
}
اینجا از constraint زیر استفاده شده 👇
where TKey : notnull
📌 این تضمین میکند که کلید دیکشنری مقدار null نداشته باشد.
حالا میتوانی این extensionها را بهراحتی با LINQ chain کنی 🔗
var products = _productService.GetAll();
var productsByCategory = products
.WhereNotNull()
.Where(p => p.IsAvailable)
.GroupToDictionary(p => p.Category);
foreach (var category in productsByCategory)
{
Console.WriteLine($"{category.Key}: {category.Value.Count} products");
}
🔢 ءExtension با Constraint عددی
میتوانی constraint تعریف کنی تا extension فقط روی typeهای خاصی کار کند.
مثلاً این extension فقط روی typeهای عددی که <INumber<T را پیادهسازی کردهاند کار میکند 🧮
public static class NumericExtensions
{
extension<T>(IEnumerable<T> source)
where T : INumber<T>
{
public T Sum()
{
var total = T.Zero;
foreach (var item in source)
{
total += item;
}
return total;
}
public T Average()
{
var enumerable = source.ToList();
var sum = enumerable.Sum();
var count = T.CreateChecked(enumerable.Count);
return sum / count;
}
public IEnumerable<T> GreaterThan(T threshold) =>
source.Where(x => x > threshold);
}
}
این extension با هر type عددی سازگار است 👇
var prices = new[] { 10.99m, 25.50m, 5.00m, 15.75m };
var expensiveItems = prices.GreaterThan(15.00m);
var averagePrice = prices.Average();🌍 مثالهای واقعی از Extension Memberها
در Web APIها، معمولاً نیاز داریم اطلاعاتی از HttpContext استخراج کنیم.
بهجای تکرار کدهای مشابه، میتوانیم extension بنویسیم تا کد تمیزتر شود ✨
public static class ApiHttpContextExtensions
{
extension(HttpContext context)
{
public string CorrelationId =>
context.Request.Headers["X-Correlation-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
public string ClientIp =>
context.Request.Headers["X-Forwarded-For"].FirstOrDefault()
?? context.Connection.RemoteIpAddress?.ToString()
?? "Unknown";
public bool IsApiRequest =>
context.Request.Path.StartsWithSegments("/api");
public string? GetBearerToken()
{
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
if (authHeader?.StartsWith("Bearer ") == true)
{
return authHeader.Substring("Bearer ".Length).Trim();
}
return null;
}
public T? GetQueryParameter<T>(string key)
{
if (context.Request.Query.TryGetValue(key, out var value))
{
try
{
return (T?)Convert.ChangeType(value.ToString(), typeof(T));
}
catch
{
return default;
}
}
return default;
}
public void AddResponseHeader(string key, string value)
{
context.Response.Headers[key] = value;
}
}
extension(HttpContext)
{
public static bool IsValidPath(string path) =>
!string.IsNullOrWhiteSpace(path) && path.StartsWith("/");
}
}
حالا middleware و controllerها بسیار خواناتر میشوند 🧼
public class RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation(
"Request {Method} {Path} from {ClientIp} with CorrelationId {CorrelationId}",
context.Request.Method,
context.Request.Path,
context.ClientIp,
context.CorrelationId);
context.AddResponseHeader("X-Correlation-ID", context.CorrelationId);
await _next(context);
}
}
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpGet]
public IActionResult GetOrders(HttpContext httpContext)
{
var pageSize = httpContext.GetQueryParameter<int?>("pageSize") ?? 10;
var pageNumber = httpContext.GetQueryParameter<int?>("page") ?? 1;
var orders = orderService.GetPaged(pageNumber, pageSize);
return Ok(orders);
}
}
📌 نتیجه؟
کد خواناتر 📖، تمیزتر 🧹 و بدون تکرار منطق پیچیدهی استخراج header و query parameterها، با defaultهای معقول ✅
✅ بهترین روشها برای سازماندهی کدهای Extension در C# 14 🧩✨
🗂 چطور Extension Blockها را سازماندهی کنیم؟
وقتی از کلیدواژه extension استفاده میکنید، میتوانید چندین extension member که روی یک type اعمال میشوند را داخل یک extension block قرار دهید.
این کار باعث کاهش تکرار 🔁 و کنار هم ماندن کدهای مرتبط میشود 🧠
همچنین میتوانید در یک static class چند extension block داشته باشید؛
مثلاً برای receiverهای متفاوت یا generic type parameterهای مختلف 👇
public static class CollectionExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
public bool HasItems => source.Any();
public int Count => source.Count();
}
extension(IEnumerable<string> source)
{
public string JoinWithComma() => string.Join(", ", source);
public IEnumerable<string> NonEmpty() => source.Where(s => !string.IsNullOrEmpty(s));
}
extension<T>(List<T> list)
{
public void AddIfNotExists(T item)
{
if (!list.Contains(item))
{
list.Add(item);
}
}
}
}
🔄 ترکیب syntax قدیمی و جدید
میتوانید syntax قدیمی (this) و syntax جدید (extension) را در یک کلاس با هم استفاده کنید 👍
public static class StringExtensions
{
// Traditional syntax
public static bool IsEmail(this string value)
{
return value.Contains("@");
}
// New syntax
extension(string value)
{
public bool IsUrl =>
Uri.TryCreate(value, UriKind.Absolute, out _);
public string ToTitleCase() =>
CultureInfo.CurrentCulture.TextInfo.ToTitleCase(value.ToLower());
}
}
🚫 از ساختن Extension Classهای غولپیکر اجتناب کنید
یک کلاس بزرگ با extension برای دهها type مختلف، بهمرور تبدیل به کابوس نگهداری میشود 😵💫
بهتر است extensionها را بر اساس domain یا کاربرد گروهبندی کنید 👇
public static class ProductExtensions
{
extension(Product product)
{
// Product-specific extensions
}
}
public static class ValidationExtensions
{
extension(Order value)
{
// Validation logic
}
extension(OrderItem value)
{
// Validation logic
}
}
📌 انتخاب ساختار مناسب کاملاً به نیاز پروژه بستگی دارد:
یا یک static class برای هر type
یا گروهبندی بر اساس functionality
🧮 برای مقادیر محاسباتی از Extension Property استفاده کنید
اگر extensionای دارید که پارامتری نمیگیرد و فقط یک مقدار برمیگرداند،
بهتر است بهجای method، آن را property تعریف کنید 👌
extension(Order order)
{
public decimal TotalPrice =>
order.Items.Sum(item => item.Price * item.Quantity);
}
✍️ نامگذاری معنادار انتخاب کنید
ءExtension propertyها باید طوری خوانده شوند که انگار عضو اصلی type هستند.
از نامهایی که «داد میزنند extension هستند» پرهیز کنید ❌
// Good
public bool IsEmpty => !source.Any();
public string DisplayPrice => price.ToString("C");
📚 ءExtensionها را مستند کنید
در پروژههای بزرگ، حتماً از XML Comment استفاده کنید تا بقیه اعضای تیم دقیقاً بدانند این extension چه کاری انجام میدهد 🧑💻📖
extension(Product product)
{
/// <summary>
/// Gets whether the product is currently available for purchase.
/// </summary>
/// <remarks>
/// A product is available if its stock quantity is greater than zero.
/// </remarks>
public bool IsAvailable => product.StockQuantity > 0;
}
🧾 جمعبندی
کلیدواژه extension در C# 14 اختیاری است ✅
تمام extension methodهای قبلی شما بدون هیچ تغییری همچنان کار میکنند.
اما زمانی که:
میخواهید property اضافه کنید 🧩
ءmemberهای static داشته باشید 🏗
یا کدهای مرتبط را تمیزتر و یکجا نگه دارید 🧹
ءsyntax جدید تجربه توسعهدهنده بسیار بهتری فراهم میکند ✨
اگر با C# 14 و NET 10. کار میکنید، حتماً extension propertyها را در پروژههایتان امتحان کنید.
بهزودی متوجه میشوید چقدر جا دارد methodها به property تبدیل شوند و کدها طبیعیتر و قابلفهمتر شوند 🧠💡
امیدوارم این مطلب براتون مفید بوده باشه 🙌
تا مطلب بعدی، موفق باشید 👋
پند روز جمعه: 🔥 یه نظر متفاوت درباره «دوباره اختراع کردن چرخ»:
شاید… واقعاً اتلاف وقتِ خوبی باشه ⏳😉
ادعای رایج منطقیه؛ مخصوصاً وقتی هدفت، تحویل سریع ارزش به کاربره 🚀
اما بیاید از یه زاویه دیگه نگاه کنیم 👀
برای یادگیری، میتونه فوقالعاده باشه.
بهخصوص وقتی اول مسیرت هستی 🌱
وقتی چیزی رو از صفر میسازی، حتی اگه هزار بار قبلاً ساخته شده باشه، مجبور میشی مفاهیم زیرساختی رو عمیقاً بفهمی 🧠
✨ چه اتفاقی میافته؟
• با تصمیمهای طراحی واقعی درگیر میشی 🧩
• با چالشهایی روبهرو میشی که اصلاً انتظارش رو نداشتی ⚠️
• و مهمتر از همه، واقعاً میفهمی چیزها «چطور» کار میکنن 🔍
🍳 بهش اینطوری فکر کن:
هیچکس فقط با خوندن کتاب آشپزی، سرآشپز حرفهای نمیشه.
باید دستهات رو توی آشپزخونه کثیف کنی! 👨🍳🔥
همین موضوع دقیقاً درباره مهندسی نرمافزار هم صدق میکنه 💻
اگه هدفت یادگیری عمیقتر و درک واقعی مفاهیمه، چرا خودت نسازی؟
🔄 الگوی (Saga Pattern) Saga (بخش ششم)
الگوی Saga Pattern برای مدیریت distributed transactions در چندین سرویس استفاده میشود. این الگو با شکستن یک تراکنش توزیعشده به دنبالهای از local transactions کار میکند.
هر local transaction فقط دادههای همان سرویس را تغییر میدهد و سپس یک event یا message منتشر میکند تا مرحله بعدی شروع شود 📣
اگر در هر مرحله خطایی رخ دهد، compensating transactions اجرا میشوند تا تغییرات مراحل قبلی را خنثی کنند و data consistency بین سرویسها حفظ شود 🔁
⚙️ ءSaga چگونه کار میکند؟
1️⃣ یک Saga زمانی شروع میشود که یک فرآیند تجاری نیاز به تغییر داده در چند سرویس داشته باشد
2️⃣ ءSaga coordinator تراکنش توزیعشده را به چند local transaction (یکی برای هر سرویس) تقسیم میکند
3️⃣ هر سرویس local transaction خودش را اجرا میکند و یک event مبنی بر موفقیت یا شکست منتشر میکند
4️⃣ ءCoordinator به این eventها گوش میدهد و مرحله بعدی را فعال میکند 👂
5️⃣ اگر همه مراحل با موفقیت انجام شوند، Saga پایان مییابد و تراکنش کامل تلقی میشود ✅
6️⃣ اگر هر مرحلهای شکست بخورد، compensating transactions بهترتیب معکوس اجرا میشوند تا تغییرات قبلی undo شوند ♻️
7️⃣ دو رویکرد اصلی برای پیادهسازی وجود دارد:
ءChoreography: سرویسها از طریق eventها با هم هماهنگ میشوند 🎭
ءOrchestration: یک coordinator مرکزی جریان کار را مدیریت میکند 🎼
🌟 مزایا (Benefits)
✔️ امکان اجرای distributed transactions در معماری microservices بدون نیاز به distributed locks یا two-phase commit
✔️ حفظ سازگاری دادهها با استفاده از compensating transactions بهجای rollbackهای سنگین
✔️ مناسب برای long-running business processes با تحمل خطای بالاتر 🛡
✔️ مقیاسپذیری بهتر، چون هر سرویس فقط مسئول local transaction خودش است 📈
⚠️ معایب (Drawbacks)
❌ پیچیدگی بالا در طراحی و پیادهسازی compensating transaction برای هر مرحله
❌ ایجاد eventual consistency که ممکن است باعث وضعیتهای موقتاً ناسازگار برای کاربر شود ⏳
❌ سختتر شدن debugging و monitoring چون تراکنشها در چند سرویس و در طول زمان پخش شدهاند
❌ سناریوهای پیچیدهی مدیریت خطا، مخصوصاً زمانی که خود compensating transaction هم شکست بخورد
🎯 موارد استفاده (Use cases)
📌 ءworkflowهای چندمرحلهای در سیستمهای enterprise که هر مرحله توسط یک سرویس جداگانه انجام میشود
📌 هر فرآیند تجاری که نیاز به تغییر هماهنگ دادهها در چند microservice دارد، بدون استفاده از distributed lock
✨ جمعبندی:
ءSaga Pattern یک راهحل کلیدی برای مدیریت تراکنشهای پیچیده در معماریهای توزیعشده است. اگرچه پیادهسازی آن ساده نیست، اما برای سیستمهای microservice-scale تقریباً اجتنابناپذیر است.
ما ۰.۱ ثانیه توی اجرا صرفهجویی کردیم، ولی ۳ روز از عمر تیم رو به باد دادیم!
چند وقت پیش توی پروژهای بودم که یکی از بچهها وسواس عجیبی روی سرعت داشت. ما یه کد تر و تمیز داشتیم که کار میکرد. ولی این همکارم گفتش که: "نه، این تابعی که نوشتی کنده! بذار پرفورمنسش رو ببرم بالا."
شروع کرد به تغییرات عجیب به بهانهی اینکه فراخوانی توابع سربار (Overhead) داره، توابعِ کوچیک و خوانا رو حذف کرد و همه رو ریخت توی یه تابعِ. شرطهای if/else شفاف رو تبدیل کرد به یک خط محاسبهی ریاضی پیچیده تا CPU کمتری مصرف بشه.
ما اولش هیجانزده بودیم و میگفتیم که دمت گرم! الان چون کد کوتاهتر شده و تابع کمتر صدا زده میشه، حتما درخواستا خیلی سریع پردازش میشن.
نتیجه؟ کاربر نهایی اصلا متوجه اون سرعت ناچیز نشد (چون گلوگاه اصلا اونجا نبود). اما چند وقت بعد، وقتی بیزینس یه تغییر کوچیک خواست، ما فلج شدیم.
کدی که قبلا توی ۱۰ دقیقه ادیت میشد، حالا شده بود یه میدون مین. هیچکس نمیفهمید اون خط طولانی ریاضی دقیقا داره چیکار میکنه. ما خوانایی رو قربانی یه سرعت توهمی کرده بودیم.
اینجا بود که یاد جملهی معروف دونالد کنوث افتادم: "بهینهسازی زودهنگام (Premature Optimization)، ریشهی تمام دردسرهاست."
ما داشتیم دقیقا همین مصیبت رو سر خودمون میاوردیم و قانونِ طلایی Uncle Bob رو فراموش کرده بودیم:
قانون ۱۰ به ۱: ما ۱۰ برابر زمانی که کد مینویسیم، صرف خوندن کد میکنیم. اون کدی که دوستمون زد، شاید نوشتنش ۱ ساعت طول کشید، ولی خوندن و دیباگ کردنش ۱۰ ساعت از وقت کل تیم رو گرفت. هر خط کدی که به بهانهی Performance ناخوانا میشه، داره هزینهی نگهداری رو تصاعدی میبره بالا.
ما یاد گرفتیم بهینهسازی واقعی این نیست که کد رو فشرده کنی. بهینهسازی واقعی اینه که کدی بنویسی که نفر بعدی (یا خود ۶ ماه بعدت) بتونه راحت بخونه و توسعهش بده.
چند وقت پیش توی پروژهای بودم که یکی از بچهها وسواس عجیبی روی سرعت داشت. ما یه کد تر و تمیز داشتیم که کار میکرد. ولی این همکارم گفتش که: "نه، این تابعی که نوشتی کنده! بذار پرفورمنسش رو ببرم بالا."
شروع کرد به تغییرات عجیب به بهانهی اینکه فراخوانی توابع سربار (Overhead) داره، توابعِ کوچیک و خوانا رو حذف کرد و همه رو ریخت توی یه تابعِ. شرطهای if/else شفاف رو تبدیل کرد به یک خط محاسبهی ریاضی پیچیده تا CPU کمتری مصرف بشه.
ما اولش هیجانزده بودیم و میگفتیم که دمت گرم! الان چون کد کوتاهتر شده و تابع کمتر صدا زده میشه، حتما درخواستا خیلی سریع پردازش میشن.
نتیجه؟ کاربر نهایی اصلا متوجه اون سرعت ناچیز نشد (چون گلوگاه اصلا اونجا نبود). اما چند وقت بعد، وقتی بیزینس یه تغییر کوچیک خواست، ما فلج شدیم.
کدی که قبلا توی ۱۰ دقیقه ادیت میشد، حالا شده بود یه میدون مین. هیچکس نمیفهمید اون خط طولانی ریاضی دقیقا داره چیکار میکنه. ما خوانایی رو قربانی یه سرعت توهمی کرده بودیم.
اینجا بود که یاد جملهی معروف دونالد کنوث افتادم: "بهینهسازی زودهنگام (Premature Optimization)، ریشهی تمام دردسرهاست."
ما داشتیم دقیقا همین مصیبت رو سر خودمون میاوردیم و قانونِ طلایی Uncle Bob رو فراموش کرده بودیم:
قانون ۱۰ به ۱: ما ۱۰ برابر زمانی که کد مینویسیم، صرف خوندن کد میکنیم. اون کدی که دوستمون زد، شاید نوشتنش ۱ ساعت طول کشید، ولی خوندن و دیباگ کردنش ۱۰ ساعت از وقت کل تیم رو گرفت. هر خط کدی که به بهانهی Performance ناخوانا میشه، داره هزینهی نگهداری رو تصاعدی میبره بالا.
ما یاد گرفتیم بهینهسازی واقعی این نیست که کد رو فشرده کنی. بهینهسازی واقعی اینه که کدی بنویسی که نفر بعدی (یا خود ۶ ماه بعدت) بتونه راحت بخونه و توسعهش بده.