#تصمیمهای_مهندسی (Engineering Decisions)
یکی از تصمیمهایی که تقریباً هر تیمی دیر یا زود با آن روبهرو میشود این است:
«آیا این عملیات باید Synchronous باشد یا Asynchronous؟»
خیلیها بهمحض شنیدن کلمه Asynchronous تصور میکنند با انتخاب آن، سیستم سریعتر میشود.
اما سؤال اصلی چیز دیگری است.
آیا کاربر واقعاً لازم است منتظر بماند؟
فرض کنید کاربری سفارشی ثبت میکند.
بعد از ثبت سفارش باید:
ایمیل ارسال شود.
پیامک ارسال شود.
فاکتور تولید شود.
موجودی انبار بهروزرسانی شود.
ءNotification برای مدیر ارسال شود.
آیا همه این کارها باید قبل از دریافت پاسخ HTTP انجام شوند؟
اگر جواب نه باشد، شاید نگه داشتن کاربر پشت این عملیات، فقط Latency سیستم را بیشتر کرده باشد.
اما طرف دیگر ماجرا هم مهم است.
اگر همه چیز را Asynchronous کنیم،
حالا باید با چالشهای جدیدی کنار بیاییم:
پیام تکراری (Duplicate Messages)، ترتیب پیامها (Ordering)، تحویل حداقل یکبار (At-Least-Once Delivery)، Idempotency، مانیتورینگ صفها،
مدیریت خطا و Retry.
یعنی مسئله فقط عوض شده است؛ از بین نرفته.
به همین دلیل، مهندسان باتجربه اول از خودشان نمیپرسند:
«چطور این را Asynchronous کنیم؟»
بلکه میپرسند:
«کاربر واقعاً منتظر نتیجه این عملیات است یا نه؟»
چون در مهندسی نرمافزار، Synchronous و Asynchronous، خوب یا بد نیستند.
هر کدام، هزینهها و مزایای خودشان را دارند.
و تصمیم درست،
همان تصمیمی است که با نیاز واقعی سیستم همخوانی داشته باشد.
یکی از تصمیمهایی که تقریباً هر تیمی دیر یا زود با آن روبهرو میشود این است:
«آیا این عملیات باید Synchronous باشد یا Asynchronous؟»
خیلیها بهمحض شنیدن کلمه Asynchronous تصور میکنند با انتخاب آن، سیستم سریعتر میشود.
اما سؤال اصلی چیز دیگری است.
آیا کاربر واقعاً لازم است منتظر بماند؟
فرض کنید کاربری سفارشی ثبت میکند.
بعد از ثبت سفارش باید:
ایمیل ارسال شود.
پیامک ارسال شود.
فاکتور تولید شود.
موجودی انبار بهروزرسانی شود.
ءNotification برای مدیر ارسال شود.
آیا همه این کارها باید قبل از دریافت پاسخ HTTP انجام شوند؟
اگر جواب نه باشد، شاید نگه داشتن کاربر پشت این عملیات، فقط Latency سیستم را بیشتر کرده باشد.
اما طرف دیگر ماجرا هم مهم است.
اگر همه چیز را Asynchronous کنیم،
حالا باید با چالشهای جدیدی کنار بیاییم:
پیام تکراری (Duplicate Messages)، ترتیب پیامها (Ordering)، تحویل حداقل یکبار (At-Least-Once Delivery)، Idempotency، مانیتورینگ صفها،
مدیریت خطا و Retry.
یعنی مسئله فقط عوض شده است؛ از بین نرفته.
به همین دلیل، مهندسان باتجربه اول از خودشان نمیپرسند:
«چطور این را Asynchronous کنیم؟»
بلکه میپرسند:
«کاربر واقعاً منتظر نتیجه این عملیات است یا نه؟»
چون در مهندسی نرمافزار، Synchronous و Asynchronous، خوب یا بد نیستند.
هر کدام، هزینهها و مزایای خودشان را دارند.
و تصمیم درست،
همان تصمیمی است که با نیاز واقعی سیستم همخوانی داشته باشد.
#فرهنگ_مهندسی (Engineering Culture)
در یک جلسه، تیم داشت درباره استفاده از یک تکنولوژی جدید تصمیم میگرفت. Tech Lead پرسید:
«همه موافقیم؟»
چند ثانیه سکوت.بعد همه گفتند:
«آره.»
تصمیم گرفته شد. چند هفته بعد، پروژه به مشکل خورد.
یکی گفت:
«راستش من از اول فکر میکردم این انتخاب مناسب نیست.»
یکی دیگر گفت:
«منم شک داشتم، ولی چون همه موافق بودن چیزی نگفتم.»
مشکل این نبود که تیم تصمیم اشتباهی گرفته بود.
مشکل این بود که کسی مخالفتش را وارد تصمیم نکرده بود.
سکوت همیشه به معنی موافقت نیست.
گاهی یعنی:
یک تیم مهندسی سالم فقط جایی نیست که آدمها راحت نظر بدهند.
باید جایی باشد که بتوانند راحت بگویند:
«من قانع نشدم.»
گاهی یک سؤال مخالف میتواند جلوی هفتهها Refactor را بگیرد:
«اگر Database Down شد چی؟»
«اگر این API ده برابر Traffic بگیره چی؟»
«چرا اصلاً به این Microservice نیاز داریم؟»
تصمیم خوب از این نمیآید که همه سریع بگویند:
«موافقم.»
گاهی از جایی شروع میشود که یک نفر جرئت میکند بگوید:
«صبر کنید، فکر کنم داریم یک چیزی رو نمیبینیم.»
در یک جلسه، تیم داشت درباره استفاده از یک تکنولوژی جدید تصمیم میگرفت. Tech Lead پرسید:
«همه موافقیم؟»
چند ثانیه سکوت.بعد همه گفتند:
«آره.»
تصمیم گرفته شد. چند هفته بعد، پروژه به مشکل خورد.
یکی گفت:
«راستش من از اول فکر میکردم این انتخاب مناسب نیست.»
یکی دیگر گفت:
«منم شک داشتم، ولی چون همه موافق بودن چیزی نگفتم.»
مشکل این نبود که تیم تصمیم اشتباهی گرفته بود.
مشکل این بود که کسی مخالفتش را وارد تصمیم نکرده بود.
سکوت همیشه به معنی موافقت نیست.
گاهی یعنی:
"شاید اشتباه میکنم."
"حوصله بحث ندارم."
"بقیه بیشتر میدونن."
"مخالفت کردن دردسر داره."
یک تیم مهندسی سالم فقط جایی نیست که آدمها راحت نظر بدهند.
باید جایی باشد که بتوانند راحت بگویند:
«من قانع نشدم.»
گاهی یک سؤال مخالف میتواند جلوی هفتهها Refactor را بگیرد:
«اگر Database Down شد چی؟»
«اگر این API ده برابر Traffic بگیره چی؟»
«چرا اصلاً به این Microservice نیاز داریم؟»
تصمیم خوب از این نمیآید که همه سریع بگویند:
«موافقم.»
گاهی از جایی شروع میشود که یک نفر جرئت میکند بگوید:
«صبر کنید، فکر کنم داریم یک چیزی رو نمیبینیم.»
🚨 یک متد Async چطور میتواند کل برنامه را بدون هیچ Exceptionای قفل کند؟
یکی از عجیبترین باگهایی که ممکن است در پروژههای NET. با آن مواجه شوید این است:
برنامه اجرا میشود.
هیچ Exceptionای ندارید.CPU هم لزوماً بالا نیست.
اما یک بخش از برنامه دیگر جلو نمیرود.
همهچیز منتظر چیزی است...
و آن چیز هم منتظر همان Thread است که شما قفلش کردهاید.
به این وضعیت میگوییم:🔒 Async Deadlock
🤔 ءDeadlock دقیقاً چطور اتفاق میافتد؟
فرض کنید یک متد Async دارید:
public async Task<string> GetDataAsync()
{
await Task.Delay(1000);
return "Hello";
}
و آن را اینطور صدا میزنید:
var result = GetDataAsync().Result;
در ظاهر شاید مشکلی نبینید.
اما در محیطهایی که
SynchronizationContext وجود دارد، اتفاق زیر ممکن است رخ دهد:Thread اصلی
│
▼
GetDataAsync()
│
▼
await
│
├── متد کنترل را آزاد میکند
│
▼
ءContinuation باید روی Context اصلی ادامه پیدا کند
│
❌
اما Thread اصلی با Result. بلاک شده است
│
▼
Continuation نمیتواند اجرا شود
│
▼
.Result منتظر پایان Task است
│
🔒 DEADLOCK
یعنی:
ءTask منتظر Thread است
و Thread منتظر Task
و هیچکدام نمیتوانند جلو بروند.
☠️ ترکیب خطرناک:await+.Result
مشکل اصلی
async نیست.مشکل زمانی ایجاد میشود که:
یک عملیات Async دارید
await میکنید Continuation باید به یک SynchronizationContext خاص برگردداما همان Context را با
.Result یا .Wait() بلاک کردهایدمثلاً:
var result = GetDataAsync().Result;
یا:
``
lua
GetDataAsync().Wait();``این دو مورد در چنین سناریوهایی میتوانند یکی از رایجترین دلایل Deadlock باشند.
🧠 نقش SynchronizationContext چیست؟
وقتی شما مینویسید:
await SomeAsyncOperation();
بعضی محیطهای NET. تلاش میکنند بعد از پایان عملیات Async، ادامه متد را دوباره روی همان Context قبلی اجرا کنند.
مثلاً:
UI Thread
│
▼
await
│
▼
عملیات Async
│
▼
بازگشت به UI Thread
این رفتار در محیطهایی مثل:
WPF
Windows Forms
ASP.NET کلاسیک
اهمیت زیادی دارد.
چون ممکن است بعد از
await نیاز داشته باشید دوباره به Context اصلی برگردید.اما اگر همان Context را بلاک کنید:
.Result
ءContinuation جایی برای اجرا ندارد.
و برنامه وارد Deadlock میشود.
🛠 راهکار اول: Async All the Way
بهترین و مهمترین راهکار این است که عملیات Async را دوباره به عملیات Sync تبدیل نکنید.
❌ این:
public string GetData()
{
return GetDataAsync().Result;
}
بهتر است به این تبدیل شود:
public async Task<string> GetDataAsync()
{
return await _service.GetDataAsync();
}
و در لایه بالاتر:
var result = await GetDataAsync();
قاعده ساده است:
اگر وارد دنیای Async شدید، تا جای ممکن در همان دنیا بمانید.
این همان چیزی است که معمولاً با عنوان:
🚀 Async All the Way
شناخته میشود.
🔧 ءConfigureAwait(false) چه کاری انجام میدهد؟
فرض کنید یک Library دارید:
public async Task<string> GetDataAsync()
{
await Task.Delay(1000);
return "Hello";
}
بهصورت پیشفرض، در محیطهایی که Context وجود دارد،
await ممکن است تلاش کند ادامه اجرای متد را روی همان Context قبلی ادامه دهد.اما با:
await Task.Delay(1000).ConfigureAwait(false);
به سیستم میگویید:
بعد از پایان این عملیات، لازم نیست اجرای ادامه متد را روی Context قبلی ادامه بدهی.
مثلاً:
public async Task<string> GetDataAsync()
{
await Task.Delay(1000)
.ConfigureAwait(false);
return "Hello";
}
در نتیجه احتمال گرفتار شدن در Deadlockهای کلاسیک ناشی از SynchronizationContext کاهش پیدا میکند.
⚠️ اما آیا باید همهجا ConfigureAwait(false) بنویسیم؟
نه دقیقاً.
اگر در یک Application UI هستید و بعد از
await میخواهید UI را تغییر دهید، ممکن است نیاز داشته باشید به همان Context اصلی برگردید.مثلاً:
await LoadDataAsync();
// ممکن است نیاز باشد روی UI Thread باشیم
MyLabel.Text = "Loaded";
اما در Libraryها و لایههایی که وابستگی مستقیمی به Context برنامه ندارند، معمولاً عدم وابستگی به Context باعث میشود کد قابلاستفادهتر و مستقلتر باشد.
به همین دلیل در بسیاری از Libraryها و کدهای زیرساختی، استفاده از:
.ConfigureAwait(false)
یک الگوی رایج است.
📌 دستورالعمل عملی برای پروژههای واقعی
اگر میخواهید احتمال Deadlock و مشکلات ناشی از Sync-over-Async را کاهش دهید:
1️⃣ از
غلط❌
درست✅
2️⃣ از .Wait() استفاده نکنید
غلط❌
درست✅
3️⃣ ءAsync All the Way را رعایت کنید
اگر یک متد Async است:
اجازه دهید Task در تمام مسیر حرکت کند.
نه اینکه وسط مسیر ناگهان:
یا:
صدا زده شود.
4️⃣ در Libraryها به Context وابسته نشوید
اگر Library شما نیازی به Context اصلی ندارد، میتوانید در نقاط مناسب از:
استفاده کنید.
5️⃣ از async void فقط برای Event Handler استفاده کنید
تقریباً در تمام Serviceها، Repositoryها، Handlerها و سایر متدهای Async:
یا:
انتخاب مناسبتری است.
ءDeadlock در دنیای Async معمولاً از خود async/await شروع نمیشود.
مشکل زمانی ایجاد میشود که:
یک عملیات Async را مجبور کنیم بهصورت Sync اجرا شود.
یعنی:
یا:
در محیطی که Continuation نیاز دارد دوباره روی یک Context بلاکشده اجرا شود.
راهکار اصلی؟
🚀 Async All the Way
و در صورت نیاز:
🔧 ConfigureAwait(false)
و مهمتر از همه:
در ASP.NET Core شاید Deadlock کلاسیک SynchronizationContext را کمتر ببینید، اما Sync-over-Async همچنان میتواند Thread Pool را اشباع کند و سیستم شما را زیر Load به زانو دربیاورد.
اگر میخواهید احتمال Deadlock و مشکلات ناشی از Sync-over-Async را کاهش دهید:
1️⃣ از
.Result استفاده نکنیدغلط❌
var result = GetDataAsync().Result;
درست✅
var result = await GetDataAsync();
2️⃣ از .Wait() استفاده نکنید
غلط❌
GetDataAsync().Wait();
درست✅
csharp
await GetDataAsync();
3️⃣ ءAsync All the Way را رعایت کنید
اگر یک متد Async است:
Repository
↓
Service
↓
Handler
↓
Controller
اجازه دهید Task در تمام مسیر حرکت کند.
نه اینکه وسط مسیر ناگهان:
.Result
یا:
.Wait()
صدا زده شود.
4️⃣ در Libraryها به Context وابسته نشوید
اگر Library شما نیازی به Context اصلی ندارد، میتوانید در نقاط مناسب از:
.ConfigureAwait(false)
استفاده کنید.
5️⃣ از async void فقط برای Event Handler استفاده کنید
تقریباً در تمام Serviceها، Repositoryها، Handlerها و سایر متدهای Async:
Task
یا:
Task<T>
انتخاب مناسبتری است.
🎯 جمعبندی
ءDeadlock در دنیای Async معمولاً از خود async/await شروع نمیشود.
مشکل زمانی ایجاد میشود که:
یک عملیات Async را مجبور کنیم بهصورت Sync اجرا شود.
یعنی:
.Resultیا:
.Wait()در محیطی که Continuation نیاز دارد دوباره روی یک Context بلاکشده اجرا شود.
راهکار اصلی؟
🚀 Async All the Way
و در صورت نیاز:
🔧 ConfigureAwait(false)
و مهمتر از همه:
ءAsync code را Sync نکنید فقط چون صبر کردن با await برایتان راحتتر نیست.در ASP.NET Core شاید Deadlock کلاسیک SynchronizationContext را کمتر ببینید، اما Sync-over-Async همچنان میتواند Thread Pool را اشباع کند و سیستم شما را زیر Load به زانو دربیاورد.
🔖هشتگها:
#dotnet #csharp #async #await #deadlock #aspnetcore #threadpool
#روایت_تجربه
یه چیزی که کمکم داره توی کارم تغییر میکنه، تعریفم از «تموم شدن کاره».
قبلاً وقتی یک Task رو میبستم، حس میکردم کار تموم شده.
کد نوشته شده بود. تستها پاس شده بودند. PR باز شده بود.Done. ✅
ولی چند وقتیه بیشتر به این فکر میکنم که:
چند بار شده یک Feature رو Merge کنیم و دو هفته بعد تازه بفهمیم هنوز کار داریم؟
چون: Monitoring نداره.Error Scenarioهاش مشخص نیست.Rollback براش در نظر نگرفتیم. Documentationش ناقصه.
یا بدتر از همه...
کسی غیر از کسی که نوشته، واقعاً نمیدونه چطور کار میکنه.
از اون طرف، بعضی وقتها یک نفر چند روز روی یک Feature کار میکنه، PR رو Merge میکنه و میره سراغ Task بعدی.
اما همون Feature از روز اول وارد Production شده و هیچکس مجبور نشده دوباره سمتش بره.
نه Bug. نه Hotfix.نه جلسه اضطراری.
نه پیام ساعت ۲ نصف شب. 😅
به نظرم دومی خیلی بیشتر شبیه «کار تمومشده» است.
شاید یکی از بدترین چیزهایی که توی Software Engineering یاد گرفتیم اینه که Progress رو با تعداد Taskهای Done اندازه بگیریم.
در حالی که بعضی Taskها وقتی Done میشن، تازه هزینهشون شروع میشه.
کد نوشته شده.
ولی نگهداریش شروع شده. Feature Deploy شده.
ولی رفتار واقعیش تازه مشخص میشه.
سیستم ساخته شده.
ولی حالا باید سالها باهاش زندگی کنیم.
برای همین این روزها سعی میکنم کمتر بپرسم:
«کی تمومش میکنیم؟»
و بیشتر بپرسم:
«بعد از اینکه تموم شد، چه چیزی ازش باقی میمونه؟»
چون در نهایت، کد خوب فقط کدی نیست که امروز Merge بشه.
کدیه که فردا کسی بابت Merge کردنش پشیمون نشه.
یه چیزی که کمکم داره توی کارم تغییر میکنه، تعریفم از «تموم شدن کاره».
قبلاً وقتی یک Task رو میبستم، حس میکردم کار تموم شده.
کد نوشته شده بود. تستها پاس شده بودند. PR باز شده بود.Done. ✅
ولی چند وقتیه بیشتر به این فکر میکنم که:
واقعاً Done یعنی چی؟
چند بار شده یک Feature رو Merge کنیم و دو هفته بعد تازه بفهمیم هنوز کار داریم؟
چون: Monitoring نداره.Error Scenarioهاش مشخص نیست.Rollback براش در نظر نگرفتیم. Documentationش ناقصه.
یا بدتر از همه...
کسی غیر از کسی که نوشته، واقعاً نمیدونه چطور کار میکنه.
از اون طرف، بعضی وقتها یک نفر چند روز روی یک Feature کار میکنه، PR رو Merge میکنه و میره سراغ Task بعدی.
اما همون Feature از روز اول وارد Production شده و هیچکس مجبور نشده دوباره سمتش بره.
نه Bug. نه Hotfix.نه جلسه اضطراری.
نه پیام ساعت ۲ نصف شب. 😅
به نظرم دومی خیلی بیشتر شبیه «کار تمومشده» است.
شاید یکی از بدترین چیزهایی که توی Software Engineering یاد گرفتیم اینه که Progress رو با تعداد Taskهای Done اندازه بگیریم.
در حالی که بعضی Taskها وقتی Done میشن، تازه هزینهشون شروع میشه.
کد نوشته شده.
ولی نگهداریش شروع شده. Feature Deploy شده.
ولی رفتار واقعیش تازه مشخص میشه.
سیستم ساخته شده.
ولی حالا باید سالها باهاش زندگی کنیم.
برای همین این روزها سعی میکنم کمتر بپرسم:
«کی تمومش میکنیم؟»
و بیشتر بپرسم:
«بعد از اینکه تموم شد، چه چیزی ازش باقی میمونه؟»
چون در نهایت، کد خوب فقط کدی نیست که امروز Merge بشه.
کدیه که فردا کسی بابت Merge کردنش پشیمون نشه.
#AI_Software_Engineering
یه چیزی که این روزها بیشتر از خود AI ذهنم رو درگیر کرده، اینه که هزینهی تغییر دادن کد داره بهشدت کم میشه.
قبلاً وقتی میخواستی یک تغییر نسبتاً بزرگ روی یک سیستم انجام بدی، اول باید با خودت کنار میاومدی که:
چند روز باید Codebase رو بخونم؟
کدوم قسمتها تحت تأثیر قرار میگیرن؟
چقدر Refactor لازمه؟
چندتا Test ممکنه بشکنه؟
و اصلاً ارزشش رو داره یا نه؟
همین هزینه باعث میشد خیلی از تغییرها اتفاق نیفتن.
نه لزوماً چون تصمیم اشتباهی بودن.
چون هزینهی بررسی و انجامشون زیاد بود.
الان اما یه Codebase بزرگ رو میدی به یک Agent و میگی:
«این بخش رو بررسی کن، Dependencyهاش رو پیدا کن، این تغییر رو اعمال کن و Testها رو هم درست کن.»
چند دقیقه بعد، یک PR آماده داری.
و به نظرم اینجا یک خطر جدید به وجود میاد.
قبلاً یکی از سؤالهای اصلی این بود:
«آیا واقعاً میتونیم این تغییر رو انجام بدیم؟»
الان بیشتر باید بپرسیم:
«آیا واقعاً باید این تغییر رو انجام بدیم؟»
چون وقتی هزینهی تغییر پایین میاد، وسوسهی تغییر دادن همهچیز بالا میره.
این روزها ممکنه یک نفر یک هفته صرف Refactor چیزی کنه که:
هیچ Bugی نداشت.
هیچ Performance Problemی نداشت.
کسی از پیچیدگیش شکایت نداشت.
و هیچ نیازی هم قرار نبود در آینده حل کنه.
فقط چون:
«الان AI میتونه سریع انجامش بده.»
ولی سریع انجام شدن، به معنی ارزشمند بودن نیست.
اتفاقاً فکر میکنم هرچقدر AI بهتر بشه، نقش Engineering Judgment مهمتر میشه.
چون احتمالاً بخش زیادی از «چطور انجام بدیم؟» رو ابزارها جواب میدن.
اما هنوز یک سؤال باقی میمونه:
«اصلاً چرا داریم انجامش میدیم؟»
و شاید این همون جایی باشه که تفاوت یک Developer خوب با کسی که فقط میتونه از ابزارها استفاده کنه، بیشتر خودش رو نشون بده. AI میتونه بهت کمک کنه یک Refactor بزرگ رو در یک ساعت انجام بدی.
اما هنوز باید یک نفر تصمیم بگیره که:
آیا این یک ساعت، بهترین جایی بود که میشد خرجش کرد یا نه؟
هرچقدر ساختن و تغییر دادن ارزانتر میشه،
به نظرم توانایی «انجام ندادن کار اشتباه» ارزشمندتر میشه.
شاید در آینده، مهارت مهم مهندسها این نباشه که چقدر سریع کد میزنن.
بلکه این باشه که بین هزار کاری که AI میتونه انجام بده،
تشخیص بدن کدومش واقعاً ارزش انجام دادن داره.
یه چیزی که این روزها بیشتر از خود AI ذهنم رو درگیر کرده، اینه که هزینهی تغییر دادن کد داره بهشدت کم میشه.
قبلاً وقتی میخواستی یک تغییر نسبتاً بزرگ روی یک سیستم انجام بدی، اول باید با خودت کنار میاومدی که:
چند روز باید Codebase رو بخونم؟
کدوم قسمتها تحت تأثیر قرار میگیرن؟
چقدر Refactor لازمه؟
چندتا Test ممکنه بشکنه؟
و اصلاً ارزشش رو داره یا نه؟
همین هزینه باعث میشد خیلی از تغییرها اتفاق نیفتن.
نه لزوماً چون تصمیم اشتباهی بودن.
چون هزینهی بررسی و انجامشون زیاد بود.
الان اما یه Codebase بزرگ رو میدی به یک Agent و میگی:
«این بخش رو بررسی کن، Dependencyهاش رو پیدا کن، این تغییر رو اعمال کن و Testها رو هم درست کن.»
چند دقیقه بعد، یک PR آماده داری.
و به نظرم اینجا یک خطر جدید به وجود میاد.
قبلاً یکی از سؤالهای اصلی این بود:
«آیا واقعاً میتونیم این تغییر رو انجام بدیم؟»
الان بیشتر باید بپرسیم:
«آیا واقعاً باید این تغییر رو انجام بدیم؟»
چون وقتی هزینهی تغییر پایین میاد، وسوسهی تغییر دادن همهچیز بالا میره.
این روزها ممکنه یک نفر یک هفته صرف Refactor چیزی کنه که:
هیچ Bugی نداشت.
هیچ Performance Problemی نداشت.
کسی از پیچیدگیش شکایت نداشت.
و هیچ نیازی هم قرار نبود در آینده حل کنه.
فقط چون:
«الان AI میتونه سریع انجامش بده.»
ولی سریع انجام شدن، به معنی ارزشمند بودن نیست.
اتفاقاً فکر میکنم هرچقدر AI بهتر بشه، نقش Engineering Judgment مهمتر میشه.
چون احتمالاً بخش زیادی از «چطور انجام بدیم؟» رو ابزارها جواب میدن.
اما هنوز یک سؤال باقی میمونه:
«اصلاً چرا داریم انجامش میدیم؟»
و شاید این همون جایی باشه که تفاوت یک Developer خوب با کسی که فقط میتونه از ابزارها استفاده کنه، بیشتر خودش رو نشون بده. AI میتونه بهت کمک کنه یک Refactor بزرگ رو در یک ساعت انجام بدی.
اما هنوز باید یک نفر تصمیم بگیره که:
آیا این یک ساعت، بهترین جایی بود که میشد خرجش کرد یا نه؟
هرچقدر ساختن و تغییر دادن ارزانتر میشه،
به نظرم توانایی «انجام ندادن کار اشتباه» ارزشمندتر میشه.
شاید در آینده، مهارت مهم مهندسها این نباشه که چقدر سریع کد میزنن.
بلکه این باشه که بین هزار کاری که AI میتونه انجام بده،
تشخیص بدن کدومش واقعاً ارزش انجام دادن داره.
🎯 ءCache فقط Redis گذاشتن جلوی Database نیست!
یکی از سؤالهای جالبی که در مصاحبههای Software Engineering ممکنه ازت بپرسن اینه:
«چه استراتژیهایی برای Caching میشناسی؟»
خیلیها سریع جواب میدن:
«Cache Aside.»
و تمام.
اما وقتی وارد سیستمهای واقعی میشیم، میبینیم که چند مدل مختلف برای مدیریت ارتباط بین Application، Cache و Database داریم.
بریم سراغ مهمترینها 👇
1️⃣ Cache-Aside / Lazy Loading
احتمالاً رایجترین الگویی که در پروژهها میبینید. Application ابتدا Cache را بررسی میکند.
مثلاً:
GET Product:123
اگر محصول داخل Redis باشد، همان را برمیگردانیم.
اگر نباشد:
Redis → Miss
↓
SQL Server
↓
Redis.Set(Product:123)
↓
Response
مزیت اصلی:
فقط چیزی را Cache میکنیم که واقعاً درخواست شده است.
برای Read-heavy workloadها و دادههایی مثل Product، User Profile و Catalog معمولاً انتخاب بسیار خوبی است.
📌اما یک نکته مهم دارد:
ءCache-Aside خودش Consistency بین Cache و Database را تضمین نمیکند.
معمولاً هنگام Update، Database را تغییر میدهیم و Entry مربوطه را Invalidate میکنیم تا درخواست بعدی دوباره آن را Load کند.
2️⃣ Read-Through
از بیرون شبیه Cache-Aside است، اما یک تفاوت مهم دارد.
در Cache-Aside:
ءApplication مسئول Fetch کردن از Database است.
در Read-Through:
ءCache Layer مسئول Fetch کردن Data Source است.
یعنی Application فقط با Cache کار میکند:
Application
↓
Cache
↓
Miss
↓
Data Store
این مدل میتواند Application Code را سادهتر کند، اما نیازمند Cache یا Infrastructureای است که بتواند این Fetch Logic را مدیریت کند.
3️⃣ Write-Through
اینجا داستان از سمت Write شروع میشود.
وقتی Data تغییر میکند، Cache و Primary Store باید در همان مسیر Write بهروز شوند.
هدف این است که بعد از یک Write موفق، Cache هم نسخه بهروز Data را داشته باشد.
مزیت؟
احتمال Cache Miss بعد از Write کمتر میشود و Readهای بعدی میتوانند سریع باشند.
اما یک مشکل دارد:
ممکن است Dataای را وارد Cache کنیم که اصلاً کسی قرار نیست آن را بخواند.
در نتیجه:
ءMemory و Cache Churn بیشتری داریم.
برای همین Write-Through معمولاً بهتنهایی استفاده نمیشود و میتواند در کنار Lazy/Cache-Aside قرار بگیرد.
4️⃣ Write-Behind / Write-Back
اینجا قضیه جدیتر میشود. Application ابتدا Data را در Cache مینویسد و Database در ادامه و بهصورت Asynchronous بهروزرسانی میشود.
Application
↓
Redis
↓
Async Worker
↓
Database
مثلاً یک سیستم با حجم بسیار زیاد:
Like Counter
View Counter
Analytics Event
ممکن است نخواهد برای هر Write منتظر Database بماند.
پس:
100,000 Writes
↓
Redis
↓
Batch
↓
Database
این مدل میتواند Write Throughput را بالا ببرد و فشار روی Database را کاهش دهد.
اما یک Trade-off بسیار مهم دارد: Durability.
اگر Data هنوز به Database نرسیده باشد و Cache از دست برود، ممکن است Data هم از دست برود.
بنابراین برای Dataهایی که از دست رفتنشان قابل قبول نیست، باید بسیار محتاط بود.
5️⃣ Write-Around
یک Pattern کمتر دیدهشده: Write مستقیماً به Database میرود و Cache درگیر Write نمیشود.
Write
↓
Database
Read
↓
Cache
↓
Miss
↓
Database
↓
Cache
برای دادههایی که زیاد Write میشوند ولی بلافاصله Read نمیشوند، میتواند مفید باشد.
چون مجبور نیستیم هر Dataای که نوشته میشود را وارد Cache کنیم.
اما داستان اینجا تمام نمیشود.
حتی اگر بهترین Caching Pattern را انتخاب کنی، هنوز چند سؤال مهم باقی میماند:
ءCache چه زمانی Expire شود؟
چه زمانی Invalidate شود؟
اگر Redis Down شد چه کنیم؟
اگر یک Key ناگهان میلیونها Request داشت چه؟
اگر TTL تمام شد و هزار Request همزمان Cache Miss شدند چه؟
اینجاست که مفاهیمی مثل:
TTL
Cache Invalidation
Cache Stampede
Cache Penetration
Cache Breakdown
Cache Warming
Distributed Lock
Stale-While-Revalidate
وارد بازی میشوند.
پس بالاخره کدام Pattern را انتخاب کنیم؟
جواب حرفهای این نیست که:
«Cache-Aside بهترینه.»
یا:
«Write-Through بهتره.»
جواب این است:
بستگی به Workload و Consistency Requirement دارد.
مثلاً:
ءRead-heavy + تغییرات کم
→ Cache-Aside
ءRead-heavy + نیاز به Cache تازه بعد از Write
→ Cache-Aside + Write-Through
ءRead/Write بسیار سنگین + قابل قبول بودن Async Persistence
→ Write-Behind
ءWrite زیاد + Read کم
→ Write-Around
و در سیستمهای واقعی حتی ممکن است چند Pattern را همزمان داشته باشی.
در نهایت:
ءCaching یک تکنولوژی نیست.
یک Design Decision است.
و Redis فقط ابزاری است که این تصمیم را پیاده میکند.
حتی اگر بهترین Caching Pattern را انتخاب کنی، هنوز چند سؤال مهم باقی میماند:
ءCache چه زمانی Expire شود؟
چه زمانی Invalidate شود؟
اگر Redis Down شد چه کنیم؟
اگر یک Key ناگهان میلیونها Request داشت چه؟
اگر TTL تمام شد و هزار Request همزمان Cache Miss شدند چه؟
اینجاست که مفاهیمی مثل:
TTL
Cache Invalidation
Cache Stampede
Cache Penetration
Cache Breakdown
Cache Warming
Distributed Lock
Stale-While-Revalidate
وارد بازی میشوند.
پس بالاخره کدام Pattern را انتخاب کنیم؟
جواب حرفهای این نیست که:
«Cache-Aside بهترینه.»
یا:
«Write-Through بهتره.»
جواب این است:
بستگی به Workload و Consistency Requirement دارد.
مثلاً:
ءRead-heavy + تغییرات کم
→ Cache-Aside
ءRead-heavy + نیاز به Cache تازه بعد از Write
→ Cache-Aside + Write-Through
ءRead/Write بسیار سنگین + قابل قبول بودن Async Persistence
→ Write-Behind
ءWrite زیاد + Read کم
→ Write-Around
و در سیستمهای واقعی حتی ممکن است چند Pattern را همزمان داشته باشی.
در نهایت:
ءCaching یک تکنولوژی نیست.
یک Design Decision است.
و Redis فقط ابزاری است که این تصمیم را پیاده میکند.
🔖هشتگها:
#Redis #Caching #SoftwareEngineering #SystemDesign
🔐 امضای دیجیتال؛ وقتی یک قابلیت فنی میتواند به ریسک محصول تبدیل شود
در جریان پیادهسازی API مربوط به Digital Signature یکی از سرویسدهندهها، به نکتهای برخوردم که واقعاً برایم عجیب و البته نگرانکننده بود.
سرویس، Digital Certificate را برای کاربر صادر میکرد و فرآیند دریافت امضا هم انجام میشد؛ اما یک ارتباط مهم در این فرآیند وجود نداشت:
❌ ارتباط مشخص بین Certificate / Signature و Documentای که قرار است امضا شود.
یعنی در فرآیند دریافت امضا، عملاً مشخص نمیکردیم:
«این امضا دقیقاً برای کدام سند صادر میشود؟»
و این سؤال ساده، از دید Security و حتی Product بسیار مهم است.
🧩 مسئله دقیقاً چیست؟
فرض کنید کاربر برای امضای یک قرارداد خاص وارد سیستم شده است.
مثلاً: 📄 Contract #1024
کاربر تصور میکند قرار است همین قرارداد را امضا کند.
اما اگر فرآیند صدور امضا هیچ ارتباطی با Document نداشته باشد، ممکن است همان Certificate یا Signature در شرایط دیگری برای یک Document متفاوت مورد استفاده قرار بگیرد.
در این حالت سیستم ممکن است از نظر فنی بگوید:
✅ ءCertificate معتبر است
✅ ءSignature معتبر است
✅ امضا متعلق به این User است
اما یک سؤال مهم همچنان بیپاسخ میماند:
❓ آیا این کاربر واقعاً همین Document را برای امضا تأیید کرده است؟
و این دو مفهوم کاملاً متفاوت هستند.
⚠️ مشکل فقط Technical نیست
اینجا دقیقاً همان جایی است که باید از زاویه Product + Security به سیستم نگاه کنیم.
فرض کنید در یک سیستم مالی، دولتی یا حقوقی، کاربر به تصور اینکه دارد سند A را امضا میکند، فرآیند را تأیید میکند.
اما سیستم در لایه دیگری امکان استفاده از همان اعتبار برای سند B را فراهم میکند.
حتی اگر تمام APIها درست کار کنند و هیچ Exception یا خطای فنی وجود نداشته باشد، رفتار محصول میتواند مشکلدار باشد.
چون: Technical Validity ≠ User Intent
اینکه یک Signature از نظر Cryptography معتبر باشد، الزاماً به این معنی نیست که کاربر قصد داشته همان عملیات خاص را تأیید کند.
🔍 اینجا باید یک سؤال مهمتر بپرسیم
وقتی کاربر روی
Sign کلیک میکند، دقیقاً چه چیزی را تأیید کرده است؟آیا فقط هویت خودش را تأیید کرده؟
یا مشخصاً تأیید کرده که:
«من، User X، این Document مشخص با شناسه Y و نسخه Z را در این لحظه برای امضا تأیید میکنم.»
این تفاوت کوچک در طراحی، میتواند از نظر امنیتی بسیار مهم باشد.
🛡 راهکاری که در طراحی خودمان در نظر گرفتیم
برای اینکه حداقل در سمت خودمان، User Consent و آگاهی کاربر نسبت به سندی که قرار است امضا کند واضحتر باشد، یک مرحله OTP به فرآیند اضافه کردیم. Flow به این شکل شد:
User
│
▼
Select Document
│
▼
Show Document Details
│
▼
Request Signature
│
▼
Generate OTP
│
▼
Send OTP to User
│
▼
Verify OTP
│
▼
Create Signature Request
│
▼
Sign Specific Document
اینجا OTP صرفاً یک Authentication Factor نیست.
در طراحی محصول ما، بخشی از فرآیند Explicit User Consent هم محسوب میشود.
یعنی قبل از اینکه عملیات حساس انجام شود، کاربر باید دوباره در همان Flow تأیید کند که قصد ادامه فرآیند را دارد.
🔗 اما فقط OTP کافی نیست
یک نکته مهم این است که اضافه کردن OTP بهتنهایی نباید به ما حس امنیت کاذب بدهد.
اگر بخواهیم این سیستم را اصولیتر طراحی کنیم، باید Signature Request را به یک Document مشخص Bind کنیم.
مثلاً:
SignatureRequest
----------------
Id
UserId
DocumentId
DocumentVersion
DocumentHash
CreatedAt
ExpiresAt
Status
در این حالت بهجای اینکه صرفاً بگوییم:
User X signed something
میتوانیم بگوییم:
User X
approved
Document Y
Version Z
with Hash H
و این دقیقاً همان چیزی است که در یک سیستم حساس ارزش دارد:
🎯 Binding the user's intent to a specific artifact
🔐 حتی Document Hash هم میتواند مهم باشد
فرض کنید کاربر Document را در لحظه
T1 مشاهده کرده است.بعد از آن، Document تغییر میکند.
اگر Signature فقط به
DocumentId وابسته باشد، ممکن است این سؤال ایجاد شود:کاربر دقیقاً کدام نسخه از Document را امضا کرده است؟
به همین دلیل در سیستمهای حساس، نگهداشتن چیزی مثل:
DocumentId
DocumentVersion
DocumentHash
میتواند بسیار مهم باشد.
در این صورت میتوانیم مشخص کنیم Signature مربوط به همان محتوایی بوده که کاربر تأیید کرده است، نه صرفاً یک رکورد با یک شناسه.
👀 یک نکته مهم برای Developerها
گاهی ما بهعنوان Developer فقط این سؤال را میپرسیم:
اما در سیستمهای حساس باید چند سؤال دیگر هم بپرسیم:
🔹 آیا User دقیقاً میداند چه چیزی را تأیید میکند؟
🔹 آیا عملیات به یک Business Object مشخص متصل است؟
🔹 اگر داده بعداً تغییر کرد، Signature همچنان چه چیزی را نمایندگی میکند؟
🔹 آیا میتوان یک Authorization را در Context دیگری استفاده کرد؟
🔹 آیا Audit Trail کافی برای اثبات Intent کاربر داریم؟
🔹 آیا سیستم فقط Valid بودن Signature را بررسی میکند یا Context امضا را هم بررسی میکند؟
امضای دیجیتال فقط این نیست که:
سؤال مهمتر این است:
برای همین، در طراحی سرویسهای حساس نباید فقط به Implementation نگاه کنیم.
گاهی لازم است از کد فاصله بگیریم و از خودمان بپرسیم:
اگر من جای کاربر بودم، آیا دقیقاً میدانستم چه چیزی را دارم تأیید میکنم؟
چون در نهایت، Security فقط جلوگیری از Attack نیست.
گاهی Security یعنی مطمئن شویم سیستم دقیقاً همان چیزی را انجام میدهد که کاربر فکر میکند دارد انجام میدهد. 🔐
گاهی ما بهعنوان Developer فقط این سؤال را میپرسیم:
«آیا API درست کار میکند؟»
اما در سیستمهای حساس باید چند سؤال دیگر هم بپرسیم:
🔹 آیا User دقیقاً میداند چه چیزی را تأیید میکند؟
🔹 آیا عملیات به یک Business Object مشخص متصل است؟
🔹 اگر داده بعداً تغییر کرد، Signature همچنان چه چیزی را نمایندگی میکند؟
🔹 آیا میتوان یک Authorization را در Context دیگری استفاده کرد؟
🔹 آیا Audit Trail کافی برای اثبات Intent کاربر داریم؟
🔹 آیا سیستم فقط Valid بودن Signature را بررسی میکند یا Context امضا را هم بررسی میکند؟
🎯 در نهایت
امضای دیجیتال فقط این نیست که:
«این Signature معتبر است؟»
سؤال مهمتر این است:
«این Signature دقیقاً چه چیزی را، توسط چه کسی، در چه زمانی و با چه میزان آگاهی و رضایتی تأیید کرده است؟»
برای همین، در طراحی سرویسهای حساس نباید فقط به Implementation نگاه کنیم.
گاهی لازم است از کد فاصله بگیریم و از خودمان بپرسیم:
اگر من جای کاربر بودم، آیا دقیقاً میدانستم چه چیزی را دارم تأیید میکنم؟
چون در نهایت، Security فقط جلوگیری از Attack نیست.
گاهی Security یعنی مطمئن شویم سیستم دقیقاً همان چیزی را انجام میدهد که کاربر فکر میکند دارد انجام میدهد. 🔐
#تصمیمهای_مهندسی | Engineering Decisions
یه Feature قرار بود دو هفتهای آماده بشه.
روز اول دربارهی Architecture بحث کردیم.
روز دوم دربارهی Database.
روز سوم دربارهی اینکه Sync باشه یا Async.
روز چهارم هنوز داشتیم گزینهها رو مقایسه میکردیم.
روز پنجم یکی گفت:
«بذاریم فعلاً بیشتر بررسی کنیم.»
هفته دوم هم گذشت. Feature هنوز نوشته نشده بود.
جالب اینجاست که هیچکس تصمیم اشتباهی نگرفته بود.
اصلاً تصمیمی نگرفته بودیم.
این یکی از چیزهایی بود که بعداً فهمیدم:
گاهی ترس از گرفتن تصمیم اشتباه،
باعث میشه تیم وارد چرخهی Analysis Paralysis بشه.
برای هر تصمیم:
و همیشه یک دلیل جدید برای ادامهی بررسی وجود دارد.
در حالی که خیلی از تصمیمهای Engineering برگشتپذیرند.
اگر امروز یک Implementation انتخاب کنیم و فردا بفهمیم مناسب نیست،میتوانیم تغییرش بدهیم.
اما اگر سه هفته فقط دربارهاش صحبت کنیم،
آن سه هفته دیگر برنمیگردد.
برای همین الان وقتی با یک تصمیم مواجه میشوم، اول میپرسم:
این تصمیم Reversible است یا Irreversible؟
اگر Reversible باشد، لازم نیست برای رسیدن به ۱۰۰٪ اطمینان صبر کنیم.
با اطلاعات کافی تصمیم میگیریم،
پیاده میکنیم،Measure میکنیم،
و اگر لازم بود تغییر میدهیم.
چون در Engineering، تصمیم بد قابل اصلاح است.
اما تصمیم نگرفتن هم هزینه دارد.
و گاهی خیلی بیشتر از چیزی که فکر میکنیم. Perfect Decision نداریم؛ فقط Decisionی داریم که با اطلاعات فعلی، منطقیترین گزینه است.
یه Feature قرار بود دو هفتهای آماده بشه.
روز اول دربارهی Architecture بحث کردیم.
روز دوم دربارهی Database.
روز سوم دربارهی اینکه Sync باشه یا Async.
روز چهارم هنوز داشتیم گزینهها رو مقایسه میکردیم.
روز پنجم یکی گفت:
«بذاریم فعلاً بیشتر بررسی کنیم.»
هفته دوم هم گذشت. Feature هنوز نوشته نشده بود.
جالب اینجاست که هیچکس تصمیم اشتباهی نگرفته بود.
اصلاً تصمیمی نگرفته بودیم.
این یکی از چیزهایی بود که بعداً فهمیدم:
گاهی ترس از گرفتن تصمیم اشتباه،
باعث میشه تیم وارد چرخهی Analysis Paralysis بشه.
برای هر تصمیم:
Option A
Option B
Option C
Option D
...
و همیشه یک دلیل جدید برای ادامهی بررسی وجود دارد.
در حالی که خیلی از تصمیمهای Engineering برگشتپذیرند.
اگر امروز یک Implementation انتخاب کنیم و فردا بفهمیم مناسب نیست،میتوانیم تغییرش بدهیم.
اما اگر سه هفته فقط دربارهاش صحبت کنیم،
آن سه هفته دیگر برنمیگردد.
برای همین الان وقتی با یک تصمیم مواجه میشوم، اول میپرسم:
این تصمیم Reversible است یا Irreversible؟
اگر Reversible باشد، لازم نیست برای رسیدن به ۱۰۰٪ اطمینان صبر کنیم.
با اطلاعات کافی تصمیم میگیریم،
پیاده میکنیم،Measure میکنیم،
و اگر لازم بود تغییر میدهیم.
چون در Engineering، تصمیم بد قابل اصلاح است.
اما تصمیم نگرفتن هم هزینه دارد.
و گاهی خیلی بیشتر از چیزی که فکر میکنیم. Perfect Decision نداریم؛ فقط Decisionی داریم که با اطلاعات فعلی، منطقیترین گزینه است.
🏗 راز معماری نابودنشدنی با Coupling و Cohesion
اگر بخواهیم فقط با دو مفهوم بفهمیم یک طراحی نرمافزاری چقدر سالم است، یکی از بهترین نقاط شروع این دو مفهوماند:
🔗 Coupling
🧩 Cohesion
خیلی وقتها این دو را کنار هم میشنویم، اما دقیقاً نمیدانیم چه مشکلی را حل میکنند.
بیایید از یک سؤال ساده شروع کنیم:
اگر فردا بخواهیم یک قسمت از سیستم را تغییر بدهیم، چند قسمت دیگر مجبور میشوند همراه آن تغییر کنند؟
پاسخ این سؤال، ما را مستقیماً به سمت Coupling میبرد.
🔗 ءCoupling چیست؟
ءCoupling یعنی میزان وابستگی بین Components مختلف سیستم.
هرچه دو Component برای کار کردن بیشتر به جزئیات یکدیگر وابسته باشند، Coupling آنها بیشتر است.
مثلاً:
{
اینجا
private readonly PaymentService _payment;
private readonly InventoryService _inventory;
private readonly EmailService _email;
public async Task CreateOrder()
{
await _payment.Pay();
await _inventory.Reserve();
await _email.Send();
}
}
OrderService مستقیماً به سه Component دیگر وابسته است.حالا فرض کنید API مربوط به
PaymentService تغییر کند.احتمالاً باید
OrderService را هم تغییر بدهیم.این یعنی یک تغییر محلی، اثر خود را به بیرون منتقل کرده است.
این همان چیزی است که در معماری میخواهیم تا حد امکان کنترلش کنیم.
🧩 ءCohesion چیست؟
ءCohesion تقریباً سؤال برعکس را میپرسد:
چیزهایی که داخل یک Component قرار دادهایم، واقعاً چقدر به یکدیگر مربوط هستند؟
اگر یک کلاس، Module یا Service روی یک هدف مشخص متمرکز باشد، Cohesion بالاتری دارد.
مثلاً:
├── AddItem()
├── RemoveItem()
├── CalculateTotal()
└── ApplyDiscount()
تمام این رفتارها حول یک مفهوم مشترک قرار گرفتهاند:🎯 Order
این یعنی Cohesion مناسب.
اما اگر همان کلاس تبدیل شود به:
├── CreateOrder()
├── SendEmail()
├── ResizeImage()
├── GenerateReport()
├── CreateUser()
├── ProcessPayment()
└── ClearCache()
دیگر یک مسئولیت مشخص نداریم.
کلاس به یک محل تجمع Business Logicهای نامرتبط تبدیل شده است.
اینجا Cohesion پایین آمده است.
🎯 پس هدف معماری چیست؟
یک قاعده بسیار مهم:
High Cohesion + Low Coupling
یعنی:
🧩 داخل هر Component، چیزهایی که واقعاً به هم مربوطاند کنار هم باشند.
و:
🔗 بین Componentها، وابستگی غیرضروری حداقل باشد.
این ترکیب یکی از اصول مهم در طراحی سیستمهای قابل نگهداری و قابل تغییر است. AWS نیز در راهنمای معماری خود برای Microservices صراحتاً بر Loose Coupling و High Functional Cohesion تأکید میکند.
💣 اما یک نکته مهم وجود دارد... Low Coupling به معنی:
«هیچ وابستگیای نباید وجود داشته باشد»
نیست.
این تصور اشتباه است.
یک سیستم بدون وابستگی عملاً وجود ندارد.
مثلاً:
↓
Payment
ءOrderبرای انجام یک فرآیند ممکن است واقعاً به Payment نیاز داشته باشد.
مسئله این نیست که Dependency را صفر کنیم.
مسئله این است که:
ءDependencyها را در مرزهای درست قرار دهیم.
🎯 یک معیار بسیار کاربردی
وقتی میخواهی تصمیم بگیری دو Component باید کنار هم باشند یا جدا، این سؤالها را بپرس:
🔹 آیا معمولاً با هم تغییر میکنند؟
🔹 آیا برای انجام یک Business Capability به یکدیگر نیاز دارند؟
🔹 آیا دادههایشان به شدت به هم وابسته است؟
🔹 آیا Transactionهای مشترک دارند؟
🔹 آیا یکی بدون دیگری میتواند مستقل Deploy شود؟
🔹 آیا یکی باید بتواند بدون دیگری کار کند؟
🔹 آیا تیمهای متفاوت مسئول آنها هستند؟
این سؤالها به ما کمک میکنند Boundary را بر اساس رفتار واقعی سیستم پیدا کنیم، نه بر اساس سلیقه.
Forwarded from کدهالیک | codehalic
وقتی بکاند پروژه C# باشه و فرانتاند TypeScript، بزرگترین دردسر اینه که مجبوری ساختار دیتا (مثل DTOها) رو دو جا تعریف کنی و اگه فیلدی تو بکاند عوض بشه، تازه تو Runtime ارورها خودشون رو نشون میدن. اما با معماری Monorepo و ابزار Nx میشه یه Type Safety یکپارچه و فولاستک ساخت! راهحلش اینه که به کمک OpenAPI موقع بیلد شدن پروژهی داتنت، کانترکتهای API به صورت اتوماتیک تولید بشن؛ بعد ابزاری مثل
https://nx.dev/blog/dotnet-openapi-type-safety
@codehalics | کدهالیک
openapi-ts این خروجی رو میخونه و تایپهای فرانتاند رو دقیقاً از روش جنریت میکنه. شاهکارِ Nx اینجاست که با سیستم قدرتمند Task Graph خودش، این پروسه رو به هم زنجیر و هوشمندانه کش Cache میکنه؛ یعنی به محض اینکه یه پراپرتی رو تو کدهای C# تغییر بدی، Nx خودش جریان رو میفهمه، تایپها رو آپدیت میکنه و خطای ناهماهنگی فرانتاند رو همون لحظه تو زمان کامپایل نشون میده تا دیگه هیچوقت باگهای ناشی از تغییر API به پروداکشن نرسن!https://nx.dev/blog/dotnet-openapi-type-safety
@codehalics | کدهالیک
📌200+ سوال مصاحبه واقعی (بخش1️⃣)
[ BUCKET 1 — C# AND .NET ]
🧠 فصل ۱ — انواع و سیستم نوعها (۵ سؤال)
1️⃣ تفاوت بین Value Type و Reference Type در #C چیست؟
2️⃣ تفاوت بین float، double و decimal چیست و هرکدام را چه زمانی استفاده میکنید؟ 🔢💰
3️⃣ تفاوت بین string و StringBuilder چیست و هرکدام در چه شرایطی مناسب هستند؟ 📝
4️⃣ ءBoxing و Unboxing چیست؟ یک مثال کوتاه بزنید و هزینهی آن را توضیح دهید. 📦
5️⃣ ءref struct چیست؟ چه محدودیتهایی دارد و چه مشکلی را حل میکند؟ ⚡️
⚙️ فصل ۲ — عملگرها، دستورات و Expressionها (۲ سؤال)
6️⃣ عملگر Null-Coalescing (??) و عملگر Null-Conditional (?.) چیستند؟
7️⃣ حلقهی foreach در پشت صحنه به چه چیزی کامپایل میشود؟ 🔍
🏗 فصل ۳ — شیءگرایی: کلاسها، وراثت و چندریختی (۵ سؤال)
8️⃣ تفاوت بین یک Abstract Class و یک Interface چیست؟
9️⃣ ءPolymorphism (چندریختی) چیست و #C چگونه از طریق virtual و override از آن پشتیبانی میکند؟ 🔄
🔟 ءPrimary Constructor در C# 12 چیست و چه تفاوتی با یک Constructor معمولی دارد؟ 🆕
1️⃣1️⃣ ءInit-Only Setter چیست و چه مشکلی را حل میکند؟ 🔒
2️⃣1️⃣ ءRuntime چگونه یک Virtual Call را با استفاده از VTable Lookup در مقایسه با یک Interface Call اجرا و Dispatch میکند؟ ⚙️🧠
📦 فصل ۴ — Records، Anonymous Types و Tupleها (۳ سؤال)
3️⃣1️⃣ ءrecord در #C چیست و چه تفاوتی با class دارد؟
4️⃣1️⃣ ءEquality در recordها در مقایسه با classها چگونه کار میکند؟ ⚖️
5️⃣1️⃣ چه زمانی record struct را به record class ترجیح میدهید؟ 🤔
🧬 فصل ۵ — Generics و Variance (۳ سؤال)
6️⃣1️⃣ ءGeneric Constraints چیستند و چه انواعی از آنها وجود دارد؟
7️⃣1️⃣ ءCovariance و Contravariance را با استفاده از in و out توضیح دهید. برای هرکدام مثالی با <IEnumerable<T و <Action<T بزنید. 🔄
8️⃣1️⃣ ءJIT چگونه کدهای Generic را برای Value Typeها در مقایسه با Reference Typeها تخصصیسازی میکند؟ این موضوع چه تأثیری بر Performance دارد؟ 🚀
🗂 فصل ۶ — Collections (۵ سؤال)
9️⃣1️⃣ تفاوت بین <IEnumerable<T و <ICollection<T چیست؟
0️⃣2️⃣ ءConcurrent Collections در NET. چیستند؟ چند مورد از آنها را نام ببرید و کاربرد هرکدام را توضیح دهید. 🔄
1️⃣2️⃣ ء<FrozenSet<T و <FrozenDictionary<TKey, TValue چیستند و چه زمانی نسبت به یک Dictionary معمولی Performance بهتری دارند؟ ❄️🚀
2️⃣2️⃣ ء<Dictionary<TKey TValue در داخل چگونه پیادهسازی شده است و چه عواملی روی Performance جستوجوی آن تأثیر میگذارند؟ 🔍
3️⃣2️⃣ ءConcurrentDictionary چگونه بهصورت همزمان بهروزرسانیها را مدیریت میکند؟ استراتژی Locking آن را توضیح دهید. 🔒⚙️
🎯 فصل ۷ — Delegates، Events و Lambdas (۴ سؤال)
4️⃣2️⃣ تفاوت بین یک Delegate و یک Event چیست؟
5️⃣2️⃣ ءClosure چیست و قوانین مربوط به Captured Variables چگونه است؟ 🧠
6️⃣2️⃣ ءExpression Tree چیست و چه تفاوتی با یک Delegate دارد؟ 🌳
7️⃣2️⃣ ءMulticast Delegate چیست و اگر یکی از Handlerها Exception ایجاد کند، چه اتفاقی میافتد؟ ⚡️
🔎 فصل ۸ — LINQ (۵ سؤال)
8️⃣2️⃣ تفاوت بین <IEnumerable<T و <IQueryable<T در LINQ چیست؟
9️⃣2️⃣ ءDeferred Execution در LINQ چیست؟ مثالی بزنید که نشان دهد چه زمانی این موضوع اهمیت پیدا میکند. ⏳
0️⃣3️⃣ ء<IAsyncEnumerable<T چیست و چگونه آن را با await foreach مصرف میکنید؟ ⚡️
1️⃣3️⃣ ءLINQ Provider مربوط به EF Core چگونه Expression Treeها را به SQL تبدیل میکند؟ 🗄
2️⃣3️⃣ چرا استفاده از Count() == 0 روی یک <IEnumerable<T میتواند بدتر از Any() باشد؟ و آیا شرایطی وجود دارد که استفاده از آن مشکلی نداشته باشد؟ 🤔
🧩 فصل ۹ — Pattern Matching (۱ سؤال)
3️⃣3️⃣ ءList Pattern در C# 11 چیست و چه قابلیتهایی را در اختیار ما قرار میدهد؟ 📋
⚠️ فصل ۱۰ — Nullable Reference Types (۱ سؤال)
4️⃣3️⃣ رفتار Nullable Reference Types در Runtime چگونه است؟ آیا این Annotationها در Runtime اعمال و enforce میشوند؟ 🧠⚙️
⏱️ ءPeriodicTimer در NET.؛ چه زمانی بهتر از Task.Delay است؟
خیلی وقتها در پروژههای NET. نیاز داریم یک عملیات را بهصورت دورهای اجرا کنیم:
🔹 هر ۳۰ ثانیه وضعیت سفارشها را بررسی کنیم
🔹 هر ۵ دقیقه Cache را Refresh کنیم
🔹 هر یک دقیقه Jobهای Pending را پردازش کنیم
🔹 هر چند دقیقه اطلاعات یک سرویس خارجی را Sync کنیم
اولین چیزی که خیلی از Developerها مینویسند چیزی شبیه این است:
while (!cancellationToken.IsCancellationRequested)
{
await DoSomethingAsync();
await Task.Delay(
TimeSpan.FromMinutes(5),
cancellationToken);
}
این کد کاملاً معتبر است و در بسیاری از سناریوها هم انتخاب خوبی است.
اما از NET 6. به بعد، یک API مشخص برای همین مدل سناریو داریم:
PeriodicTimerمایکروسافت آن را بهعنوان Timerای معرفی میکند که امکان waiting asynchronously for timer ticks را فراهم میکند.
⏱️ ءPeriodicTimer دقیقاً چیست؟
ء
PeriodicTimer در namespace زیر قرار دارد:System.Threading
و استفاده اصلی آن این است که بهجای Callback-based Timer، منتظر Tick بعدی Timer بمانیم:
using var timer = new PeriodicTimer(
TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
await DoSomethingAsync(cancellationToken);
}
اینجا یک نکته مهم وجود دارد:
ء( )
WaitForNextTickAsync یک <ValueTask<bool برمیگرداند.تا زمانی که Tick بعدی اتفاق نیفتاده، کد شما منتظر میماند و بعد از Tick وارد بدنه حلقه میشود.
اگر Timer متوقف شود، این متد میتواند
false برگرداند و حلقه تمام شود. همچنین ( )Dispose میتواند یک WaitForNextTickAsync فعال را متوقف کند و باعث شود false برگردد.🔄 پس چه فرقی با Task.Delay دارد؟
مثلاً این دو کد را ببینید.
با
Task.Delay:while (!cancellationToken.IsCancellationRequested)
{
await DoSomethingAsync(cancellationToken);
await Task.Delay(
TimeSpan.FromMinutes(5),
cancellationToken);
}
و با
PeriodicTimer:using var timer = new PeriodicTimer(
TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(
cancellationToken))
{
await DoSomethingAsync(cancellationToken);
}
از نظر نتیجه ظاهری، هر دو میتوانند یک کار را بهصورت دورهای اجرا کنند.
اما مدل ذهنی آنها متفاوت است.
ء
Task.Delay میگوید:بعد از این مدت دوباره ادامه بده.
در حالی که
PeriodicTimer میگوید:من یک Timer دورهای دارم؛ وقتی Tick بعدی رسید، اجازه بده ادامه بدهم.
برای سناریوهایی که ذاتاً periodic هستند، این مدل بسیار واضحتر است.
⚠️ اما یک اشتباه مهم
نباید تصور کنیم:
PeriodicTimer
یعنی Job شما هر دقیقاً ۵ دقیقه یکبار اجرا میشود.
فرض کنید:
Period = 5 minutes
Tick #1
↓
DoSomethingAsync()
↓
7 minutes
↓
Tick #2
در این مدت شما دوباره
WaitForNextTickAsync() را صدا نزدهاید.طبق مستندات،
PeriodicTimer برای استفاده توسط یک consumer در هر لحظه طراحی شده است و فقط یک WaitForNextTickAsync() باید همزمان در حال انتظار باشد.بنابراین نباید از آن انتظار داشته باشید که برای هر Tick یک اجرای موازی از Job ایجاد کند.
🧠 این رفتار در BackgroundService خیلی کاربردی است
مثلاً فرض کنید در یک سیستم فروشگاهی میخواهیم هر ۳۰ ثانیه سفارشهای Pending را بررسی کنیم:
public sealed class OrderWorker(
IServiceScopeFactory scopeFactory)
: BackgroundService
{
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(
TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(
stoppingToken))
{
using var scope =
scopeFactory.CreateScope();
var service =
scope.ServiceProvider
.GetRequiredService<IOrderService>();
await service.ProcessPendingOrdersAsync(
stoppingToken);
}
}
}
اینجا Flow کاملاً مشخص است
و وقتی Application در حال Shutdown باشد، CancellationToken میتواند انتظار Timer را متوقف کند.
📌 پس PeriodicTimer برای چه چیزی مناسب است؟
✅ اجرای Periodic یک عملیات درون یک Process
✅ ءBackground Workerهای ساده
✅ ءPolling
✅ ءCache Refresh
✅ بررسی دورهای وضعیت
✅ ءCleanupهای ساده
✅ ءSync دورهای
و مخصوصاً زمانی که میخواهید این منطق را بهشکل async و قابل Cancellation بنویسید.
#تصمیمهای_مهندسی | Engineering Decisions
یه زمانی فکر میکردم هر چیزی که دوباره استفاده میشه، باید سریع تبدیلش کنیم به یک Abstraction.
مثلاً دو تا Service داشتیم که تقریباً کار مشابهی انجام میدادن.سریع میگفتیم:
«اینارو Generic کنیم که Duplicate Code نداشته باشیم.»
یک Interface. یک Base Class. چندتا Generic Method. چندتا Strategy. و تمام. 😅
روی کاغذ خیلی تمیز به نظر میرسید.
تا چند ماه بعد که Requirement یکی از اون Serviceها تغییر کرد. حالا باید یک رفتار خاص بهش اضافه میکردیم.
ولی چون با یک Abstraction مشترک بسته شده بود، تغییر ساده تبدیل شد به کلی شرط و Exception.
آخرش هم فهمیدیم:
ما دو چیز مشابه را فقط چون امروز شبیه هم بودند، یکی کرده بودیم.
در حالی که ممکن بود فردا مسیرشان کاملاً از هم جدا شود.
از اون موقع یک قانون ساده برای خودم دارم:
ءDuplicate Code همیشه دشمن ما نیست.
گاهی چند خط کد تکراری، از یک Abstraction اشتباه خیلی ارزانتره.
اول اجازه میدم الگو خودش را نشان بده.
بعد اگر واقعاً فهمیدیم این دو رفتار یک مفهوم مشترک دارند،Abstraction میسازیم.
نه صرفاً برای اینکه چند خط Code کمتر داشته باشیم.
چون هدف Refactoring این نیست که Code کمتر شود.
هدف اینه که مدل ذهنی ما از سیستم درستتر شود.
گاهی بهترین Abstraction...
همون Abstractionیه که هنوز نساختیم.
یه زمانی فکر میکردم هر چیزی که دوباره استفاده میشه، باید سریع تبدیلش کنیم به یک Abstraction.
مثلاً دو تا Service داشتیم که تقریباً کار مشابهی انجام میدادن.سریع میگفتیم:
«اینارو Generic کنیم که Duplicate Code نداشته باشیم.»
یک Interface. یک Base Class. چندتا Generic Method. چندتا Strategy. و تمام. 😅
روی کاغذ خیلی تمیز به نظر میرسید.
تا چند ماه بعد که Requirement یکی از اون Serviceها تغییر کرد. حالا باید یک رفتار خاص بهش اضافه میکردیم.
ولی چون با یک Abstraction مشترک بسته شده بود، تغییر ساده تبدیل شد به کلی شرط و Exception.
آخرش هم فهمیدیم:
ما دو چیز مشابه را فقط چون امروز شبیه هم بودند، یکی کرده بودیم.
در حالی که ممکن بود فردا مسیرشان کاملاً از هم جدا شود.
از اون موقع یک قانون ساده برای خودم دارم:
ءDuplicate Code همیشه دشمن ما نیست.
گاهی چند خط کد تکراری، از یک Abstraction اشتباه خیلی ارزانتره.
اول اجازه میدم الگو خودش را نشان بده.
بعد اگر واقعاً فهمیدیم این دو رفتار یک مفهوم مشترک دارند،Abstraction میسازیم.
نه صرفاً برای اینکه چند خط Code کمتر داشته باشیم.
چون هدف Refactoring این نیست که Code کمتر شود.
هدف اینه که مدل ذهنی ما از سیستم درستتر شود.
گاهی بهترین Abstraction...
همون Abstractionیه که هنوز نساختیم.
📌 200+ سوال مصاحبه واقعی (بخش 2️⃣)
[ BUCKET 1 — C# AND .NET ]
⚠️ فصل ۱۱ — مدیریت خطا در #C (۳ سؤال)
5️⃣3️⃣ تفاوت بین throw ex و throw داخل یک catch block چیست؟ ⚠️
6️⃣3️⃣ هزینهی Throw کردن Exception در NET. چقدر است و چرا نباید از Exceptionها برای Control Flow استفاده کنیم؟ 🚨
7️⃣3️⃣ وقتی یک Exception رخ میدهد، CLR چگونه Stack را Unwind میکند؟ و JIT در مرزهای try/catch چه کاری انجام میدهد؟ 🧠⚙️
⚡️ فصل ۱۲ — Async/Await و Taskها (۶ سؤال)
8️⃣3️⃣ تفاوت بین Task و ValueTask چیست و هرکدام را چه زمانی باید استفاده کنیم؟ ⚡️
9️⃣3️⃣ هدف ConfigureAwait(false) چیست و چه زمانی باید از آن استفاده کنیم؟ 🔄
0️⃣4️⃣ ءCompiler برای یک متد async چه State Machineای تولید میکند؟ این State Machine چه چیزهایی را Allocate میکند؟ 🧠
1️⃣4️⃣ ءSynchronizationContext چیست و چگونه روی Resume شدن یک await تأثیر میگذارد؟ 🔄
2️⃣4️⃣ تفاوت async void و async Task چیست و چرا async void خطرناک است؟ ⚠️
3️⃣4️⃣ چرا وقتی در Contextی که SynchronizationContext دارد روی یک Task از .Result یا .Wait() استفاده میکنیم، ممکن است Deadlock رخ دهد؟ 🔒
🧵 فصل ۱۳ — Threading و Synchronization (۴ سؤال)
4️⃣4️⃣ ءlock چیست و به چه چیزی Compile میشود؟ 🔐
5️⃣4️⃣ شیء جدید Lock در C# 13 چیست و چگونه نسبت به Lock کردن روی یک Object دلخواه بهتر عمل میکند؟ 🆕🔒
6️⃣4️⃣ ء<Channel<T در NET. چیست و در مقایسه با <BlockingCollection<T چه سناریوهایی را حل میکند؟ 📡
7️⃣4️⃣ وقتی Threadهای ThreadPool به دلیل sync-over-async یا Blocking روی عملیات I/O دچار Starvation شوند چه اتفاقی میافتد؟ چگونه آن را Diagnose میکنید؟ 🚨🧵
🧠 فصل ۱۴ — مدیریت حافظه و GC (۵ سؤال)
8️⃣4️⃣ ءGenerationهای 0، 1 و 2 در GC را توضیح دهید. Objectها چگونه بین این Generationها Promote میشوند؟ ♻️
9️⃣4️⃣ ءLarge Object Heap (LOH) چیست و چرا برای Performance اهمیت دارد؟ 🧠📦
0️⃣5️⃣ تفاوت Workstation GC و Server GC چیست؟ 🖥⚙️
1️⃣5️⃣ ءDispose Pattern چیست و چرا شامل Dispose(bool disposing) است؟ 🗑
2️⃣5️⃣ چگونه یک Memory Leak را در یک برنامهی NET. پیدا و Diagnose میکنید؟ از چه ابزارهایی استفاده میکنید؟ 🔍🧠
🔎 فصل ۱۵ — Reflection و Dynamic (۲ سؤال)
3️⃣5️⃣ هزینهی Performance مربوط به Reflection چیست و چگونه میتوان نتایج Reflection را Cache کرد؟ 🔍⚡️
4️⃣5️⃣ ءSource Generatorها در مقایسه با Reflection برای حل مسائل مشابه چه تفاوتی دارند؟ ⚙️
📐 فصل ۱۶ — Span، Memory و Pipelines (۳ سؤال)
5️⃣5️⃣ ء<Span<T چیست و چه مشکلی را حل میکند؟ 🚀
6️⃣5️⃣ چرا <Span<T نمیتواند بهعنوان یک Field در یک Class معمولی استفاده شود؟ راهکار چیست؟ 🧠
7️⃣5️⃣ ء<ArrayPool<T چیست و چه زمانی باید از آن استفاده کنید؟ ♻️📦
📦 فصل ۱۷ — Serialization (۲ سؤال)
8️⃣5️⃣ حالت Source Generator در System.Text.Json چیست و چه زمانی باید از آن استفاده کنیم؟ ⚡️
9️⃣5️⃣ چرا BinaryFormatter Deprecated شد و بهجای آن باید از چه چیزی استفاده کنیم؟ 🚫
🆕 فصل ۱۸ — قابلیتهای نسخههای #C (۲ سؤال)
0️⃣6️⃣ کلیدواژهی field در C# 14 چیست و چه مشکلی را در Setterهای Property حل میکند؟ 🆕✨
1️⃣6️⃣ ءCallerArgumentExpression چیست و چگونه در پیادهسازی Guard Clauseها کاربرد دارد؟ 🛡
⚙️ فصل ۱۹ — NET Runtime، CLR، JIT. و AOT (۳ سؤال)
6️⃣2️⃣ ءTiered Compilation در NET. چیست و چگونه بین Startup Performance و Steady-State Performance تعادل ایجاد میکند؟ 🚀
6️⃣3️⃣ ءNative AOT چیست و استفاده از آن چه Trade-offهایی دارد؟ ⚡️
6️⃣4️⃣ ءPGO (Profile-Guided Optimization) چیست و Dynamic PGO در NET. چگونه کار میکند؟ 🧠🚀
📊 فصل ۲۰ — Performance و Benchmarking (۲ سؤال)
6️⃣5️⃣ چرا نباید از Stopwatch و یک حلقهی for بهعنوان جایگزین BenchmarkDotNet استفاده کنیم؟ ⏱️
6️⃣6️⃣ ءMemoryDiagnoser چه چیزی را اندازهگیری میکند و چگونه باید تعداد Gen0، Gen1 و Gen2 را تفسیر کنیم؟ 🧠📊
📁 فصل ۲۱ — BCL و I/O (۲ سؤال)
6️⃣7️⃣ تفاوت بین Stream، MemoryStream و FileStream چیست؟ 📁
6️⃣8️⃣ ءRandomAccess که در NET 6. معرفی شد چیست و چه سناریویی را بهبود میدهد؟ ⚡️📂
📦 فصل ۲۲ — NuGet (۱ سؤال)
6️⃣9️⃣ ءCentral Package Management و فایل Directory.Packages.props چیستند؟ 📦
🛠 فصل ۲۳ — MSBuild و csproj (۲ سؤال)
7️⃣0️⃣ ءDirectory.Build.props و Directory.Build.targets چیستند و چه کاربردی دارند؟ 🛠
7️⃣1️⃣ ءMulti-Targeting چیست و چه زمانی از آن استفاده میکنید؟ 🎯
💻 فصل ۲۴ — NET CLI. (۲ سؤال)
7️⃣2️⃣ تفاوت dotnet build و dotnet publish چیست؟ 💻
7️⃣3️⃣ ءglobal.json چیست و چه مشکلی را در محیطهایی که چند نسخهی NET. دارند حل میکند؟ ⚙️