🎯 ایده اصلی کتاب چیست؟
مهمترین ایده کتاب این است که Engineering Management ادامهی طبیعی Senior Software Engineering نیست؛ یک نقش متفاوت با مسئلهای متفاوت است.
وقتی Engineer هستی، بخش زیادی از خروجی تو مستقیماً از چیزهایی میآید که خودت میسازی:
اما وقتی Manager میشوی، دیگر قرار نیست خودت بیشترین کد را بنویسی.
خروجی تو بیشتر از این مسیر میآید:
بنابراین کتاب تلاش میکند به کسی که تازه وارد Management شده یاد بدهد چگونه از حالت «خودم انجام میدهم» به «شرایطی ایجاد میکنم که تیم بتواند انجام دهد» تغییر کند.
1. 🧭 ورود به نقش Manager
🧠 2. اول خودت را مدیریت کن
👥 3. مدیریت افراد
🎯 4. ءDelegation؛ یکی از مهمترین مهارتها
🔥 5. ءMicromanagement
🗣 6. ءFeedback و Performance
🧑🏫 7. ءCoaching و Mentoring
🏗 8. ساختن یک Team خوب
📈 9. رشد شغلی افراد
🧩 10. فقط تیم خودت مهم نیست
اگر بخواهم کتاب را خیلی خلاصه کنم:
مهمترین ایده کتاب این است که Engineering Management ادامهی طبیعی Senior Software Engineering نیست؛ یک نقش متفاوت با مسئلهای متفاوت است.
وقتی Engineer هستی، بخش زیادی از خروجی تو مستقیماً از چیزهایی میآید که خودت میسازی:
Code → Feature → System → Result
اما وقتی Manager میشوی، دیگر قرار نیست خودت بیشترین کد را بنویسی.
خروجی تو بیشتر از این مسیر میآید:
People → Team → Environment → Engineering Output
بنابراین کتاب تلاش میکند به کسی که تازه وارد Management شده یاد بدهد چگونه از حالت «خودم انجام میدهم» به «شرایطی ایجاد میکنم که تیم بتواند انجام دهد» تغییر کند.
📚 کتاب چه چیزهایی را آموزش میدهد؟
1. 🧭 ورود به نقش Manager
🧠 2. اول خودت را مدیریت کن
👥 3. مدیریت افراد
🎯 4. ءDelegation؛ یکی از مهمترین مهارتها
🔥 5. ءMicromanagement
🗣 6. ءFeedback و Performance
🧑🏫 7. ءCoaching و Mentoring
🏗 8. ساختن یک Team خوب
📈 9. رشد شغلی افراد
🧩 10. فقط تیم خودت مهم نیست
📌 در یک جمله
اگر بخواهم کتاب را خیلی خلاصه کنم:
این کتاب به تو یاد نمیدهد چگونه Developerهای بیشتری مدیریت کنی؛ به تو یاد میدهد چگونه محیطی بسازی که Developerها بتوانند بهتر کار کنند، رشد کنند و خروجی بهتری بهعنوان یک تیم داشته باشند.
#تحلیل_و_طرز_تفکر (Engineering Mindset)
گاهی یک سؤال ساده میتواند کیفیت یک طراحی را مشخص کند:
«اگر این بخش سیستم فردا خراب شود، چه چیزی باید بتواند بدون آن ادامه دهد؟»
خیلی از سیستمها برای حالت سالم طراحی میشوند.
ءDatabase در دسترس است.
ءRedis سالم است.
ءMessage Broker کار میکند.
ءExternal API پاسخ میدهد.
ءNetwork پایدار است.
همهچیز طبق انتظار پیش میرود.
اما Production دقیقاً جایی است که این فرضها شروع به شکستن میکنند.
ءDatabase ممکن است ۳۰ ثانیه کند شود.
ءRedis ممکن است از دسترس خارج شود.
یک Message ممکن است دوبار Deliver شود.
یک External API ممکن است Timeout کند.
و Network ممکن است Packet Loss داشته باشد.
اینجاست که تفاوت بین یک سیستم معمولی و یک سیستم Resilient مشخص میشود.
سیستم خوب فقط نمیگوید:
«اگر همهچیز سالم باشد، چه اتفاقی میافتد؟»
بلکه میپرسد:
«اگر یکی از وابستگیهای من خراب شود، دقیقاً چه چیزی باید همچنان کار کند؟»
مثلاً اگر Notification Service از دسترس خارج شد،
آیا ثبت سفارش هم باید Fail شود؟
اگر Analytics Service Down شد،
آیا کاربر باید نتواند وارد سیستم شود؟
اگر Recommendation Service پاسخ نداد، آیا صفحه محصول باید Error بدهد؟
پاسخ این سؤالها را نمیتوان با یک Pattern آماده داد.
باید از Business Requirement بیاید.
به همین دلیل، Resilience فقط اضافه کردن Retry و Circuit Breaker نیست.
اول باید مشخص کنی:
کدام شکستها قابل تحملاند و کدامها نیستند.
بعد برایشان طراحی کنی.
چون در سیستمهای واقعی، خرابی Exception نیست.
بخشی از شرایط عادی سیستم است که باید برایش طراحی شده باشی.
گاهی یک سؤال ساده میتواند کیفیت یک طراحی را مشخص کند:
«اگر این بخش سیستم فردا خراب شود، چه چیزی باید بتواند بدون آن ادامه دهد؟»
خیلی از سیستمها برای حالت سالم طراحی میشوند.
ءDatabase در دسترس است.
ءRedis سالم است.
ءMessage Broker کار میکند.
ءExternal API پاسخ میدهد.
ءNetwork پایدار است.
همهچیز طبق انتظار پیش میرود.
اما Production دقیقاً جایی است که این فرضها شروع به شکستن میکنند.
ءDatabase ممکن است ۳۰ ثانیه کند شود.
ءRedis ممکن است از دسترس خارج شود.
یک Message ممکن است دوبار Deliver شود.
یک External API ممکن است Timeout کند.
و Network ممکن است Packet Loss داشته باشد.
اینجاست که تفاوت بین یک سیستم معمولی و یک سیستم Resilient مشخص میشود.
سیستم خوب فقط نمیگوید:
«اگر همهچیز سالم باشد، چه اتفاقی میافتد؟»
بلکه میپرسد:
«اگر یکی از وابستگیهای من خراب شود، دقیقاً چه چیزی باید همچنان کار کند؟»
مثلاً اگر Notification Service از دسترس خارج شد،
آیا ثبت سفارش هم باید Fail شود؟
اگر Analytics Service Down شد،
آیا کاربر باید نتواند وارد سیستم شود؟
اگر Recommendation Service پاسخ نداد، آیا صفحه محصول باید Error بدهد؟
پاسخ این سؤالها را نمیتوان با یک Pattern آماده داد.
باید از Business Requirement بیاید.
به همین دلیل، Resilience فقط اضافه کردن Retry و Circuit Breaker نیست.
اول باید مشخص کنی:
کدام شکستها قابل تحملاند و کدامها نیستند.
بعد برایشان طراحی کنی.
چون در سیستمهای واقعی، خرابی Exception نیست.
بخشی از شرایط عادی سیستم است که باید برایش طراحی شده باشی.
یک نکتهی تکمیلی: تجمیع مایگریشنها (Squashing Migrations) یکی از مباحث پیشرفته و حساس در نگهداری بلندمدت پروژههای مبتنی بر EF Core است.
زمانی که یک پروژه چند سال فعال است یا در جریان یک بازنویسی سنگین صدها مایگریشن برای افزودن، حذف یا تغییر فیلدها ایجاد میشود، حجم بالای این فایلها سرعت بیلد، اجرای تستها و زمان پردازش مدل را کاهش میدهد. در این شرایط، هدف از Squashing این است که تاریخچه طولانی و خرد مایگریشنها به یک مایگریشن اولیه و یکپارچه (Initial/Baseline) تبدیل شود.
اگر شما صرفاً تمام فایلهای مایگریشن قبلی را پاک کنید و یک مایگریشن جدید به نام
در دیتابیس جدید: همهچیز عالی کار میکند و دیتابیس با آخرین ساختار ساخته میشود.
در دیتابیسهای عملیاتی (Production/Staging): با خطای بحرانی مواجه میشوید! چون EF Core جدول
برای اینکه دیتابیسهای عملیاتی متوجه شوند که نیازی به اجرای مجدد ساختار ندارند، از تکنیک هماهنگسازی تاریخچه استفاده میشود:
1️⃣ ایجاد مایگریشن نهایی و خالی (Checkpoint Migration): پیش از دست زدن به فایلهای قدیمی، ابتدا مطمئن شوید تمام محیطهای عملیاتی به آخرین وضعیت مدل آپدیت شدهاند. سپس یک مایگریشن خالی به عنوان نقطه پایان (Checkpoint) بسازید:
این مایگریشن هیچ تغییری در کالبد ایجاد نمیکند اما یک رکورد با Timestamp مشخص تولید میکند. نام کامل این فایل (شامل عدد تاریخ/زمان ابتدای آن، مثلاً
2️⃣ اعمال مایگریشن نهایی روی تمامی محیطهای موجود: دستور زیر را روی تمام دیتابیسهای عملیاتی، تست و توسعه اجرا کنید تا نام این مایگریشن در جدول
3️⃣ حذف فیزیکی مایگریشنهای قبلی: تمام فایلهای مایگریشن موجود در پوشه
4️⃣ ایجاد یک مایگریشن جامع جدید: اکنون دستور ساخت مایگریشن جدید را صادر کنید تا تمام مدل فعلی در قالب یک فایل منفرد تجمیع شود:
5️⃣ تطبیق نام و Timestamp مایگریشن جدید با Checkpoint: نام فایل، نام کلاس داخل فایل #C و همچنین مقدار شناسه درون فایل Snapshot تولیدشده را تغییر دهید تا دقیقاً برابر با همان نام و Timestamp مرحله اول (
نتیجه این فرآیند چیست؟
برای پایگاهدادههای موجود (Production):
وقتی برنامه را بالا میآورید، EF Core جدول
برای پایگاهدادههای جدید (New Deployments / Local Test DBs):
دیتابیس تازه هیچ رکوردی در جدول تاریخچه ندارد؛ بنابراین همین یک مایگریشن تجمیعشده را از ابتدا اجرا میکند و تمام جداول را دقیقاً مطابق با آخرین مدل میسازد.
چه زمانی باید مایگریشنها را Squash کنیم؟
انتشار نسخه ماژور (Major Release): زمانی که نسخه جدیدی از محصول ارائه میشود و میخواهید تمام تغییرات نسخههای قبل را پاکسازی و یکدست کنید.
کاهش زمان بیلد و تست: در پروژههای بزرگ با صدها مایگریشن، سرعت ایجاد دیتابیس موقت برای تستهای یکپارچگی (Integration Tests) به شدت افزایش مییابد.
تغییرات پرشمار در فاز توسعه (قبل از Production): در شاخههای فیچر پرحجم، تجمیع مایگریشنها پیش از Merge به برنچ اصلی (Main) تاریخچهای تمیز به جا میگذارد.
زمانی که یک پروژه چند سال فعال است یا در جریان یک بازنویسی سنگین صدها مایگریشن برای افزودن، حذف یا تغییر فیلدها ایجاد میشود، حجم بالای این فایلها سرعت بیلد، اجرای تستها و زمان پردازش مدل را کاهش میدهد. در این شرایط، هدف از Squashing این است که تاریخچه طولانی و خرد مایگریشنها به یک مایگریشن اولیه و یکپارچه (Initial/Baseline) تبدیل شود.
چالش اصلی چیست؟
اگر شما صرفاً تمام فایلهای مایگریشن قبلی را پاک کنید و یک مایگریشن جدید به نام
InitialCreate بسازید:در دیتابیس جدید: همهچیز عالی کار میکند و دیتابیس با آخرین ساختار ساخته میشود.
در دیتابیسهای عملیاتی (Production/Staging): با خطای بحرانی مواجه میشوید! چون EF Core جدول
__EFMigrationsHistory را چک میکند؛ این جدول نام تکتک مایگریشنهای قدیمی را دارد اما مایگریشن جدید شما (InitialCreate) را ندارد. در نتیجه تلاش میکند تمام جداول را از اول بسازد و با خطای Table already exists یا تخریب دیتابیس متوقف میشود.راهکار استاندارد EF Core برای تجمیع بدون شکستن دیتابیسهای موجود
برای اینکه دیتابیسهای عملیاتی متوجه شوند که نیازی به اجرای مجدد ساختار ندارند، از تکنیک هماهنگسازی تاریخچه استفاده میشود:
1️⃣ ایجاد مایگریشن نهایی و خالی (Checkpoint Migration): پیش از دست زدن به فایلهای قدیمی، ابتدا مطمئن شوید تمام محیطهای عملیاتی به آخرین وضعیت مدل آپدیت شدهاند. سپس یک مایگریشن خالی به عنوان نقطه پایان (Checkpoint) بسازید:
dotnet ef migrations add CheckpointSync
این مایگریشن هیچ تغییری در کالبد ایجاد نمیکند اما یک رکورد با Timestamp مشخص تولید میکند. نام کامل این فایل (شامل عدد تاریخ/زمان ابتدای آن، مثلاً
20260814050000_CheckpointSync) را یادداشت کنید.2️⃣ اعمال مایگریشن نهایی روی تمامی محیطهای موجود: دستور زیر را روی تمام دیتابیسهای عملیاتی، تست و توسعه اجرا کنید تا نام این مایگریشن در جدول
__EFMigrationsHistory ثبت شود:dotnet ef database update
3️⃣ حذف فیزیکی مایگریشنهای قبلی: تمام فایلهای مایگریشن موجود در پوشه
Migrations پروژه (از جمله فایل مایگریشن مرحله ۱) را حذف کنید. دقت کنید: فقط فایل Snapshot یا کدهای اصلی دیتابیس را دستکاری نکنید، صرفاً فایلهای لیست مایگریشنها را پاک کنید.4️⃣ ایجاد یک مایگریشن جامع جدید: اکنون دستور ساخت مایگریشن جدید را صادر کنید تا تمام مدل فعلی در قالب یک فایل منفرد تجمیع شود:
dotnet ef migrations add InitialBaseline
5️⃣ تطبیق نام و Timestamp مایگریشن جدید با Checkpoint: نام فایل، نام کلاس داخل فایل #C و همچنین مقدار شناسه درون فایل Snapshot تولیدشده را تغییر دهید تا دقیقاً برابر با همان نام و Timestamp مرحله اول (
20260814050000_CheckpointSync) شود.نتیجه این فرآیند چیست؟
برای پایگاهدادههای موجود (Production):
وقتی برنامه را بالا میآورید، EF Core جدول
__EFMigrationsHistory را بررسی میکند. میبیند که رکورد 20260814050000_CheckpointSync قبلاً ثبت و اجرا شده است؛ بنابراین هیچ کدی اجرا نمیکند و دیتابیس دستنخورده باقی میماند.برای پایگاهدادههای جدید (New Deployments / Local Test DBs):
دیتابیس تازه هیچ رکوردی در جدول تاریخچه ندارد؛ بنابراین همین یک مایگریشن تجمیعشده را از ابتدا اجرا میکند و تمام جداول را دقیقاً مطابق با آخرین مدل میسازد.
چه زمانی باید مایگریشنها را Squash کنیم؟
انتشار نسخه ماژور (Major Release): زمانی که نسخه جدیدی از محصول ارائه میشود و میخواهید تمام تغییرات نسخههای قبل را پاکسازی و یکدست کنید.
کاهش زمان بیلد و تست: در پروژههای بزرگ با صدها مایگریشن، سرعت ایجاد دیتابیس موقت برای تستهای یکپارچگی (Integration Tests) به شدت افزایش مییابد.
تغییرات پرشمار در فاز توسعه (قبل از Production): در شاخههای فیچر پرحجم، تجمیع مایگریشنها پیش از Merge به برنچ اصلی (Main) تاریخچهای تمیز به جا میگذارد.
🧠 ءCaching فقط برای کم کردن Latency نیست؛ مشکل اصلی بعد از اضافه کردن Cache شروع میشود!
وقتی برای اولین بار Redis را وارد معماری میکنیم، معمولاً مسئله خیلی ساده به نظر میرسد:
و واقعاً هم همین کار باعث کاهش Load روی Database و کاهش Latency میشود.
اما یک سؤال مهم وجود دارد:
❓ وقتی Data در Database تغییر کرد، Redis از کجا بفهمد که مقدار قبلی دیگر معتبر نیست؟
فرض کنید:
حالا قیمت محصول تغییر میکند:
اما Redis هنوز دارد:
اگر Cache Invalidation درست انجام نشود، کاربر ممکن است همچنان قیمت
اینجاست که مسئله واقعی Caching خودش را نشان میدهد:
🔥 Cache Invalidation
🔹 Cache-Aside Pattern
یکی از رایجترین الگوها برای Cache کردن داده، Cache-Aside است.
در Read:
یعنی Application خودش مسئول مدیریت Cache است.
اگر Data در Cache وجود داشته باشد، همان مقدار استفاده میشود.
اگر وجود نداشته باشد، Application سراغ Database میرود و بعد مقدار خواندهشده را در Cache قرار میدهد.
اما مشکل اصلی در Update اتفاق میافتد.
معمولاً داریم:
مثلاً:
ءRequest بعدی دوباره Data را از Database میخواند و Cache را با مقدار جدید Populate میکند.
تا اینجا همهچیز خوب به نظر میرسد...
اما یک Race Condition میتواند تمام این طراحی را خراب کند. 👇
فرض کنید دو Request تقریباً همزمان اتفاق میافتند. Request A میخواهد قیمت را
ممکن است چیزی شبیه این اتفاق بیفتد:
حالا Database مقدار
این یکی از دلایلی است که طراحی Cache در سیستمهای Concurrent بسیار پیچیدهتر از یک
🔐 پس فقط DEL کردن Redis کافی نیست؟
نه همیشه.
بسته به Requirement سیستم، ممکن است به تکنیکهای دیگری نیاز داشته باشید:
🔹 Optimistic Locking
برای تشخیص اینکه Data در فاصله بین Read و Write تغییر کرده است.
🔹 Distributed Lock
در سناریوهایی که چند Instance نباید همزمان یک عملیات حساس را انجام دهند.
🔹 Versioning
برای اینکه بتوانیم تشخیص دهیم مقدار Cache مربوط به چه Versionی از Data است.
مثلاً:
و Cache هم Version را نگه دارد.
اگر یک Update قدیمی با Version پایینتر بخواهد Cache را Update کند، میتوان آن را نادیده گرفت.
🔹 Event-driven Cache Invalidation
به جای اینکه هر قسمت Application خودش مسئول Invalidate کردن Cache باشد، تغییرات Data را به صورت Event منتشر کنیم:
🔹 TTL
و در نهایت TTL میتواند یک لایه محافظتی دیگر باشد.
⏳ اما یک اشتباه رایج:
ءTTL بهتنهایی مشکل Consistency را حل نمیکند.
اگر:
باشد، این یعنی Data قدیمی ممکن است تا ۵ دقیقه در Cache باقی بماند. TTL فقط میگوید:
اما نمیگوید:
بنابراین اگر Requirement شما Strong Consistency است، نباید TTL را جایگزین Cache Invalidation بدانید.
⚖️ آیا همیشه Strong Consistency لازم داریم؟
اینجا باید از Business Requirement شروع کنیم، نه از تکنولوژی.
برای بعضی Dataها چند ثانیه اختلاف کاملاً قابل قبول است:
اگر View Count یک محصول برای چند ثانیه
در اینجا Eventual Consistency میتواند انتخاب کاملاً منطقیای باشد.
وقتی برای اولین بار Redis را وارد معماری میکنیم، معمولاً مسئله خیلی ساده به نظر میرسد:
«دیتای پرتکرار را از Database بخوان، داخل Redis بگذار و Requestهای بعدی را از Cache جواب بده.»
و واقعاً هم همین کار باعث کاهش Load روی Database و کاهش Latency میشود.
اما یک سؤال مهم وجود دارد:
❓ وقتی Data در Database تغییر کرد، Redis از کجا بفهمد که مقدار قبلی دیگر معتبر نیست؟
فرض کنید:
Database
Product:123
Price = 100
Redis
Product:123
Price = 100
حالا قیمت محصول تغییر میکند:
Database
Product:123
Price = 150
اما Redis هنوز دارد:
Redis
Product:123
Price = 100
اگر Cache Invalidation درست انجام نشود، کاربر ممکن است همچنان قیمت
100 را دریافت کند؛ در حالی که Source of Truth مقدار 150 را دارد.اینجاست که مسئله واقعی Caching خودش را نشان میدهد:
🔥 Cache Invalidation
🔹 Cache-Aside Pattern
یکی از رایجترین الگوها برای Cache کردن داده، Cache-Aside است.
در Read:
Request
↓
Redis
↓
Cache Hit?
┌───────┴───────┐
Yes No
↓ ↓
Return Database
↓
Update Redis
↓
Return
یعنی Application خودش مسئول مدیریت Cache است.
اگر Data در Cache وجود داشته باشد، همان مقدار استفاده میشود.
اگر وجود نداشته باشد، Application سراغ Database میرود و بعد مقدار خواندهشده را در Cache قرار میدهد.
اما مشکل اصلی در Update اتفاق میافتد.
معمولاً داریم:
Update Database
↓
Invalidate Cache
مثلاً:
await db.Products.UpdateAsync(product);
await redis.RemoveAsync($"product:{product.Id}");
ءRequest بعدی دوباره Data را از Database میخواند و Cache را با مقدار جدید Populate میکند.
تا اینجا همهچیز خوب به نظر میرسد...
اما یک Race Condition میتواند تمام این طراحی را خراب کند. 👇
⚠️ ءRace Condition در Cache
فرض کنید دو Request تقریباً همزمان اتفاق میافتند. Request A میخواهد قیمت را
150 کند. Request B تقریباً همزمان Data قدیمی را از Database میخواند.ممکن است چیزی شبیه این اتفاق بیفتد:
Request A Request B
Update DB → 150
Read DB → 100
↓
Set Redis → 100
Delete Redis
حالا Database مقدار
150 دارد، اما بسته به ترتیب دقیق عملیات، Cache ممکن است دوباره با مقدار قدیمی Populate شود.این یکی از دلایلی است که طراحی Cache در سیستمهای Concurrent بسیار پیچیدهتر از یک
GET و SET ساده است.🔐 پس فقط DEL کردن Redis کافی نیست؟
نه همیشه.
بسته به Requirement سیستم، ممکن است به تکنیکهای دیگری نیاز داشته باشید:
🔹 Optimistic Locking
برای تشخیص اینکه Data در فاصله بین Read و Write تغییر کرده است.
🔹 Distributed Lock
در سناریوهایی که چند Instance نباید همزمان یک عملیات حساس را انجام دهند.
🔹 Versioning
برای اینکه بتوانیم تشخیص دهیم مقدار Cache مربوط به چه Versionی از Data است.
مثلاً:
Product
Version = 42
Price = 150
و Cache هم Version را نگه دارد.
اگر یک Update قدیمی با Version پایینتر بخواهد Cache را Update کند، میتوان آن را نادیده گرفت.
🔹 Event-driven Cache Invalidation
به جای اینکه هر قسمت Application خودش مسئول Invalidate کردن Cache باشد، تغییرات Data را به صورت Event منتشر کنیم:
Database Update
↓
Domain / Integration Event
↓
Cache Invalidation Handler
↓
Redis DEL
🔹 TTL
و در نهایت TTL میتواند یک لایه محافظتی دیگر باشد.
⏳ اما یک اشتباه رایج:
ءTTL بهتنهایی مشکل Consistency را حل نمیکند.
اگر:
TTL = 5 minutes
باشد، این یعنی Data قدیمی ممکن است تا ۵ دقیقه در Cache باقی بماند. TTL فقط میگوید:
«این Data بعد از این مدت دیگر معتبر نیست.»
اما نمیگوید:
«به محض تغییر Source of Truth، Cache هم فوراً معتبر بودنش را از دست بدهد.»
بنابراین اگر Requirement شما Strong Consistency است، نباید TTL را جایگزین Cache Invalidation بدانید.
⚖️ آیا همیشه Strong Consistency لازم داریم؟
اینجا باید از Business Requirement شروع کنیم، نه از تکنولوژی.
برای بعضی Dataها چند ثانیه اختلاف کاملاً قابل قبول است:
Product View Count
Analytics
Recommendation
Reporting Data
اگر View Count یک محصول برای چند ثانیه
10,521 و بعد 10,524 باشد، احتمالاً Business مشکلی ندارد.در اینجا Eventual Consistency میتواند انتخاب کاملاً منطقیای باشد.
اما تصور کنید:
اینجا نمیتوانیم به کاربر بگوییم:
در این سناریوها Consistency بخشی از Business Requirement است.
🎯 پس قبل از طراحی Cache این سؤالها را بپرسید:
❓ء Source of Truth کجاست؟
❓ چه مدت Stale Data قابل قبول است؟
❓ آیا Strong Consistency لازم داریم یا Eventual Consistency کافی است؟
❓ چه چیزی Cache را Invalidate میکند؟
❓ اگر دو Request همزمان Update کنند چه اتفاقی میافتد؟
❓ اگر Redis Down شود چه میشود؟
❓ اگر Cache قبل از Database Update شود چه؟
❓ اگر Database Update شود اما Invalidation شکست بخورد چه؟
❓ اگر چند Instance همزمان Cache را Populate کنند چه؟
اینها همان سؤالهایی هستند که تفاوت بین:
«ما Redis داریم»
و
«ما یک Cache درست طراحی کردهایم»
را مشخص میکنند. 🚀
💡 نکته نهایی
ءCache کردن Data ساده است.
اما Cache Design ساده نیست.
به محض اضافه شدن Cache، شما یک State دوم وارد سیستم کردهاید.
از این لحظه باید علاوه بر Performance به این موارد هم فکر کنید:
Consistency
Invalidation
Concurrency
TTL
Failure
Race Condition
Source of Truth
Scalability
و شاید مهمترین سؤال این باشد:
اگر جواب این سؤال را قبل از پیادهسازی ندانید، احتمالاً بعداً در Production جوابش را پیدا خواهید کرد! 😄
Account Balance
Payment Status
Inventory
Available Seats
اینجا نمیتوانیم به کاربر بگوییم:
«نگران نباش، چند ثانیه دیگه درست میشه!» 😐
در این سناریوها Consistency بخشی از Business Requirement است.
🎯 پس قبل از طراحی Cache این سؤالها را بپرسید:
❓ء Source of Truth کجاست؟
❓ چه مدت Stale Data قابل قبول است؟
❓ آیا Strong Consistency لازم داریم یا Eventual Consistency کافی است؟
❓ چه چیزی Cache را Invalidate میکند؟
❓ اگر دو Request همزمان Update کنند چه اتفاقی میافتد؟
❓ اگر Redis Down شود چه میشود؟
❓ اگر Cache قبل از Database Update شود چه؟
❓ اگر Database Update شود اما Invalidation شکست بخورد چه؟
❓ اگر چند Instance همزمان Cache را Populate کنند چه؟
اینها همان سؤالهایی هستند که تفاوت بین:
«ما Redis داریم»
و
«ما یک Cache درست طراحی کردهایم»
را مشخص میکنند. 🚀
💡 نکته نهایی
ءCache کردن Data ساده است.
GET
↓
Redis
↓
MISS
↓
Database
↓
SET
اما Cache Design ساده نیست.
به محض اضافه شدن Cache، شما یک State دوم وارد سیستم کردهاید.
از این لحظه باید علاوه بر Performance به این موارد هم فکر کنید:
Consistency
Invalidation
Concurrency
TTL
Failure
Race Condition
Source of Truth
Scalability
و شاید مهمترین سؤال این باشد:
اگر Cache و Database با هم اختلاف داشتند، سیستم من دقیقاً چه رفتاری باید داشته باشد؟
اگر جواب این سؤال را قبل از پیادهسازی ندانید، احتمالاً بعداً در Production جوابش را پیدا خواهید کرد! 😄
🔖هشتگها:
#Caching #Redis #CacheConsistency #CacheAside #SystemDesign #DotNet
Forwarded from iCodeNext
❤️ شما دعوتید!
https://luma.com/28fu1cid
ساعت 9 صبح 5 شنبه به وقت تهران 29 مرداد ماه.
مدت زمان میتینگ : 90 دقیقه
ظرفیت 99 نفر
موضوع : در مورد همه چیز صحبت میکنیم.
https://luma.com/28fu1cid
ساعت 9 صبح 5 شنبه به وقت تهران 29 مرداد ماه.
مدت زمان میتینگ : 90 دقیقه
ظرفیت 99 نفر
موضوع : در مورد همه چیز صحبت میکنیم.
🧩 ءGitHub Gists؛ یک Git Repository کوچک برای کدهای کوچک!
تا حالا شده یک تکه کد، Regex، Query، کانفیگ یا یک نمونه کوچک از #C داشته باشی که بخواهی با یک نفر به اشتراک بگذاری؟
اما ساختن یک Repository کامل برایش زیادی باشد؟
اینجاست که GitHub Gist وارد میشود. 🚀
ءGist در اصل یک راه ساده برای اشتراکگذاری Code Snippet و فایلهای متنی است؛ اما یک نکته مهم دارد:
هر Gist در واقع یک Git Repository است.
یعنی برخلاف چیزی که شاید در نگاه اول به نظر برسد، فقط یک Text Box ساده برای Paste کردن کد نیست. Gist میتواند:
🔹 ءCommit History داشته باشد
🔹 ءDiff تغییرات را نگه دارد
🔹 ءClone شود
🔹 ءFork شود
🔹 چند فایل داشته باشد
🔹 و حتی داخل Blog یا Website Embed شود.
🎯 چه زمانی Gist واقعاً کاربردی است؟
فرض کن در یک پروژه NET. یک Extension Method نوشتهای:
public static bool IsValidEmail(this string value)
{
return value.Contains('@');
}
اگر بخواهی این را با همکارت به اشتراک بگذاری، احتمالاً یک Repository ساختن برای چنین قطعه کدی منطقی نیست.
میتوانی آن را داخل یک Gist قرار بدهی و لینک همان Gist را ارسال کنی.
یا مثلاً:
📌 یک Regex پیچیده
📌 یک SQL Query
📌 یک Docker Command
📌 یک GitHub Actions Snippet
📌 یک نمونه
appsettings.json📌 یک الگوریتم کوچک
📌 یک قطعه کد برای پاسخ Stack Overflow
اینجا Gist بسیار مناسب است.
🆚 ءGist یا Repository؟
این دو را نباید جایگزین یکدیگر بدانیم. Repository برای یک پروژه یا Codebase کامل است.
مثلاً:
MyShop
├── src
├── tests
├── docs
├── .github
├── Dockerfile
└── README.md
اما Gist بیشتر برای چیزی شبیه این است:
retry-policy.cs
یا:
docker-compose.yml
یا حتی:
useful-sql-query.sql
یعنی وقتی Context و ساختار یک پروژه کامل لازم نیست، Gist انتخاب سادهتری است.
🧠 اما یک نکته مهم درباره Secret Gist!
اینجا یکی از رایجترین سوءتفاهمها وجود دارد. GitHub دو نوع Gist دارد:
🟢 Public Gist
در Discover نمایش داده میشود و قابل Search است.
🟡 Secret Gist
در Discover نمایش داده نمیشود و بهصورت عمومی Search نمیشود.
اما...
ءSecret به معنی Private نیست. ❗️
اگر لینک Secret Gist را برای کسی بفرستی، او میتواند آن را ببیند.
اگر شخص دیگری somehow به URL دسترسی پیدا کند، او هم میتواند محتوا را مشاهده کند.
بنابراین:
Secret Gist
≠
Private Repository
اگر اطلاعات واقعاً حساس هستند، مثل:
API Key
Password
Connection String
Private Certificate
Access Token
نباید آنها را داخل Secret Gist قرار دهید. GitHub هم صراحتاً توصیه میکند برای محتوایی که باید از دیگران مخفی بماند، از Private Repository استفاده کنید.
🔥 چیزی که Gist را جالب میکند
ءGist فقط Share کردن یک فایل نیست.
چون Git Repository است، میتوانی آن را Clone کنی:
git clone https://gist.github.com/<gist-id>.git
بعد:
git add .
git commit -m "Improve retry example"
git push
یعنی میتوانی یک Snippet را مثل یک Repository کوچک مدیریت کنی.
حتی GitHub برای Gist امکان مشاهده Revision History و Diff را هم فراهم میکند.
🛠 حتی از Terminal هم میتوانی Gist بسازی
اگر GitHub CLI نصب باشد:
gh gist create example.cs
برای Public کردن:
gh gist create --public example.cs
و اگر بخواهی چند فایل را همزمان قرار دهی:
gh gist create example.cs example.sql docker-compose.yml
ءGitHub CLI بهصورت پیشفرض Gist را Secret ایجاد میکند و برای Public کردن باید
public-- را مشخص کنی.حتی میتوانی یک Gist را Clone کنی:
gh gist clone <gist-id>
و بعد مانند یک Git Repository معمولی روی آن کار کنی.
🌐 یک کاربرد جالب دیگر: Embed کردن
فرض کن یک Blog فنی داری و میخواهی کد را مستقیماً از GitHub نمایش بدهی.
میتوانی Gist را داخل Blog Embed کنی.
در نتیجه وقتی کد داخل Gist تغییر کند، همان محتوای Gist میتواند در محل Embed شده نمایش داده شود.
این قابلیت برای:
📚 ءTutorialها
📝 مقالات فنی
💻 ءCode Exampleها
🎓 آموزشها
خیلی کاربردی است.
💡 پس Gist را اینطور به خاطر بسپار:
• Repository
برای ساختن و نگهداری یک پروژه.
• Gist
برای نگهداری و اشتراکگذاری یک قطعه کوچک و مستقل از کد یا متن.
و مهمتر از همه:
ءGist یک Pastebin ساده نیست؛ یک Git Repository کوچک برای Snippetهاست. 🚀
اگر تا امروز برای هر تکه کد کوچک یک Repository ساختهای، شاید وقتش رسیده Gist را هم وارد جعبهابزار GitHub خودت کنی. 😄
🔖هشتگها:
#GitHub #Git #Gist #GitHubTips #DeveloperTools
🎯 ایده اصلی کتاب چیست؟
مهمترین ایده کتاب این است که وقتی یک سیستم باید سریع و قابل پیشبینی باشد، صرفاً سریعتر کردن کد کافی نیست.
در یک سیستم Low-Latency، باید کل مسیر درخواست را ببینی:
Request → Network → CPU → Memory → Application → I/O → Response
گاهی مشکل اصلاً در Algorithm نیست.
ممکن است چند میکروثانیه را در کد ذخیره کنی، اما چند میلیثانیه را در Network، GC، Context Switch یا I/O از دست بدهی.
بنابراین کتاب تلاش میکند نگاه مهندسی به Latency را از سطح «این کد کند است» به سطح «دقیقاً کجای مسیر زمان از دست میرود؟» ببرد.
📚 کتاب چه چیزهایی را آموزش میدهد؟
🌐 1️⃣ شناخت Latency
اول باید بفهمیم Latency دقیقاً چیست و چرا با Throughput یکی نیست.
🧭 2️⃣ اندازهگیری Latency
قبل از Optimization باید بتوانی Latency را Measure کنی؛ نه اینکه صرفاً حدس بزنی مشکل کجاست.
⚡️3️⃣ Low-Latency Programming
چگونه تصمیمهای داخل Application میتوانند روی زمان پاسخ تأثیر بگذارند.
🧠4️⃣ CPU و پردازش
ءCPU، Cache، Instructionها و نحوه اجرای کد میتوانند بخشی از Latency باشند.
💾5️⃣ Memory
دسترسی به Memory همیشه یکسان نیست و رفتار Cache و Memory Access میتواند روی Performance تأثیر جدی بگذارد.
🗑6️⃣ Garbage Collection
در Runtimeهایی که Garbage Collection دارند، Allocation و GC میتوانند روی Latency و مخصوصاً Tail Latency تأثیر بگذارند.
🌐7️⃣ Network Latency
گاهی Application سریع است، اما شبکه کند است.
و در سیستمهای Distributed، یک Request ممکن است چندین Network Hop داشته باشد.
💽8️⃣ I/O
ءDisk، Network و سایر I/Oها میتوانند بخش قابلتوجهی از زمان اجرای یک عملیات را مصرف کنند.
📊9️⃣ Tail Latency
ءAverage Latency بهتنهایی تصویر درستی از سیستم نمیدهد.
ممکن است Average بسیار خوب باشد، اما تعداد کمی از Requestها Latency بسیار بالایی داشته باشند.
برای همین Percentileهایی مثل:
p50 → p95 → p99 → p99.9
در سیستمهای حساس اهمیت زیادی پیدا میکنند.
🔬🔟 Performance Measurement
ءPerformance Optimization بدون Measurement میتواند بهینهسازی قسمت اشتباه سیستم باشد.
💡 یک نکته مهم کتاب
یکی از طرز فکرهای مهم در بحث Latency این است:
Don't optimize what you haven't measured.
اگر نمیدانی Latency کجا ایجاد میشود، تغییر دادن کد الزاماً Performance را بهتر نمیکند.
ممکن است Developer یک Method را بهینه کند و چند درصد CPU کمتر مصرف شود، در حالی که ۹۰٪ زمان Request در یک Network Call یا Database Query سپری میشود.
🏗 این کتاب برای چه کسی مناسب است؟
اگر فقط با CRUD Applicationهای ساده کار میکنی، احتمالاً بخش زیادی از مباحث کتاب برایت بیش از نیاز روزمره است.
اما اگر روی این حوزهها کار میکنی، کتاب بسیار ارزشمندتر میشود:
🚀 High-Performance Systems
🌐 Distributed Systems
📡 Networking
💾 Databases
⚡️ Low-Latency Applications
📈 High-Traffic Services
💳 Trading & Financial Systems
🎮 Real-Time Systems
🧠 Performance Engineering
📌 در یک جمله
اگر بخواهم ایده اصلی کتاب را خیلی خلاصه کنم:
ءLatency را نمیتوان فقط با سریعتر نوشتن Code حل کرد؛ باید کل مسیر اجرای یک Request را ببینی، اندازهگیری کنی و بفهمی زمان دقیقاً کجا مصرف میشود.و شاید مهمترین تغییر نگرشی که این کتاب ایجاد میکند همین باشد:
ءPerformance یک Feature نیست؛ یک خاصیت کل سیستم است.
🎯 در System Design اول Kafka و RabbitMQ را انتخاب نکن!
فرض کنید کاربر روی دکمه ثبت سفارش کلیک میکند.
در چند ثانیه آینده ممکن است دهها اتفاق مختلف در سیستم رخ دهد:
🟢 پرداخت سفارش
🟢 بررسی و کسر موجودی
🟢 ارسال Email
🟢 ارسال SMS
🟢 ثبت امتیاز کاربر
🟢 بروزرسانی Analytics
🟢 ارسال Event برای Recommendation Engine
🟢 بهروزرسانی سیستم Loyalty
حالا یک سؤال مهم:
آیا همه این عملیات باید قبل از اینکه به کاربر پاسخ
200 OK بدهیم، انجام شوند؟اگر جواب «بله» باشد، خیلی سریع به یک مشکل معماری میرسیم.
🔴 اشتباه رایج
ممکن است چنین Flowای طراحی کنیم:
Client
↓
Order Service
↓
Payment Service
↓
Inventory Service
↓
Email Service
↓
SMS Service
↓
Analytics Service
↓
Recommendation Service
↓
Response
در ظاهر ساده است.
اما حالا فرض کنید:
Payment → 200msInventory → 100msEmail → 500msSMS → 800msAnalytics → 1sRecommendation → 2sکاربر برای انجام یک عملیات ساده باید منتظر مجموع این وابستگیها بماند.
بدتر از آن، اگر فقط Recommendation Service از دسترس خارج شود، آیا واقعاً باید ثبت سفارش کاربر هم Fail شود؟
احتمالاً نه.
اینجا یک مفهوم بسیار مهم وارد طراحی میشود:
Business Criticality
یعنی:
اگر این عملیات انجام نشود، آیا Business Rule اصلی نقض میشود؟
🟢 عملیات Business-Critical
مثلاً:
Payment
اگر پرداخت انجام نشود، نمیتوانیم سفارش را به وضعیت Paid ببریم.
یا:
Inventory
اگر آخرین واحد محصول همزمان به دو نفر فروخته شود، یک Business Invariant نقض شده است.
بنابراین این عملیاتها معمولاً بخشی از Critical Path هستند.
Create Order
↓
Check Inventory
↓
Reserve Stock
↓
Process Payment
↓
Order Confirmed
در این قسمت، Consistency و نتیجه قطعی عملیات اهمیت زیادی دارد.
🔵 اما همه چیز Business-Critical نیست
فرض کنید سفارش با موفقیت ثبت شده است.
حالا باید:
📧 ءEmail ارسال شود.
📱 ءSMS ارسال شود.
📊 اطلاعات Analytics ثبت شود.
⭐️ امتیاز کاربر محاسبه شود.
🤖 ءRecommendation Engine از خرید جدید مطلع شود.
آیا اگر Email Service برای ۳۰ ثانیه Down باشد، باید Order هم Fail شود؟
معمولاً نه.
این عملیاتها را میتوان از مسیر اصلی خارج کرد.
در اینجا Asynchronous Processing میتواند انتخاب مناسبتری باشد.
اما اینجا یک نکته مهم وجود دارد...
وقتی میگوییم:
«این را Async کنیم»
هنوز نگفتهایم Kafka یا RabbitMQ.
چون انتخاب Broker باید بعد از مشخص شدن Communication Pattern انجام شود.
🐇 چه زمانی RabbitMQ منطقیتر است؟
فرض کنید بعد از ثبت سفارش، یک Task داریم:
SendOrderConfirmationEmail
این Message باید توسط یک Consumer پردازش شود.
ما الزاماً به تاریخچه بلندمدت Eventها، Replay کردن میلیونها Event یا Stream Processing نیاز نداریم.
هدف ما بیشتر این است:
یک Message را قابلاعتماد به Consumer برسانیم و آن را پردازش کنیم.
در چنین سناریویی، یک Message Broker مانند RabbitMQ میتواند انتخاب مناسبی باشد.
ءConsumer میتواند Message را پردازش کند و در صورت موفقیت
Ack بدهد.🟣 ءKafka چه زمانی معنا پیدا میکند؟
حالا سناریوی دیگری را تصور کنید.
وقتی Order ایجاد شد، میخواهیم چندین سیستم مستقل از آن مطلع شوند:
OrderCreated
│
▼
Kafka
│
┌────┼────┬───────────┐
▼ ▼ ▼ ▼
CRM Analytics Recommendation Loyalty
حالا هر Consumer میتواند جریان Eventهای خودش را مستقل دنبال کند.
ءAnalytics ممکن است Event را مصرف کند.
ءRecommendation هم همان Event را مصرف کند.
ءLoyalty هم همان Event را مصرف کند.
و در سناریوهایی که نیاز به نگهداری Eventها و Replay وجود دارد، Kafka مدل متفاوتی نسبت به یک Message Queue سنتی ارائه میکند.
بنابراین سؤال درست این نیست:
«Kafka بهتر است یا RabbitMQ؟»
سؤال درست این است:
«من دقیقاً چه نوع Communication Patternای دارم؟»
⚠️ یک اشتباه خطرناکتر
حتی اگر تصمیم بگیریم عملیات را Async کنیم، هنوز کار تمام نشده است.
مثلاً:
Order DB
↓
Publish Event
اگر Order در Database Commit شود ولی Publish کردن Event شکست بخورد چه؟
DB Commit ✅
Publish Event ❌
حالا Order ایجاد شده، اما هیچ Consumerای از آن خبر ندارد.
اینجاست که الگوهایی مثل Transactional Outbox اهمیت پیدا میکنند.
در این مدل، ایجاد Order و ثبت Event در Outbox میتواند در یک Database Transaction انجام شود.
بعد یک Worker، Event را از Outbox خوانده و به Broker منتشر میکند.
در نتیجه، دیگر به این شکل نیست که:
🎯 یک قانون ساده برای System Design
قبل از اینکه بگویید:
RabbitMQ
یا
Kafka
یا
gRPC
یا
Redis
یا هر تکنولوژی دیگری...
ابتدا این سه سؤال را بپرسید:
1️⃣ اگر این عملیات Fail شود، آیا Business Transaction باید Fail شود؟
اگر بله → احتمالاً Synchronous / Critical Path
اگر نه → احتمالاً میتواند Asynchronous شود.
2️⃣ آیا Consumer باید بلافاصله نتیجه را پردازش کند یا فقط باید از اتفاق رخداده مطلع شود؟
این سؤال به شما کمک میکند بین Command-oriented Messaging و Event-oriented Communication تصمیم بگیرید.
3️⃣ آیا به قابلیتهایی مثل Retention، چند Consumer مستقل، Replay و Stream Processing نیاز دارید؟
اگر بله، انتخابهایی مثل Kafka جدیتر میشوند.
به نظرم یکی از مهمترین مهارتهای System Design این نیست که بتوانی دهها تکنولوژی را نام ببری.
مهمتر این است که بتوانی تشخیص بدهی:
کدام عملیات واقعاً باید همین الان انجام شود؟
و کدام عملیات میتواند چند ثانیه بعد انجام شود، بدون اینکه Business Rule اصلی سیستم نقض شود؟
وقتی این مرز را درست مشخص کردی، انتخاب تکنولوژی خیلی سادهتر میشود.
چون آن موقع دیگر نمیپرسی:
بلکه میپرسی:
و این دقیقاً جایی است که System Design از انتخاب تکنولوژی جدا میشود.
بعد یک Worker، Event را از Outbox خوانده و به Broker منتشر میکند.
در نتیجه، دیگر به این شکل نیست که:
«اول Order را ذخیره کنیم و امیدوار باشیم بعداً Publish هم موفق شود.»
🎯 یک قانون ساده برای System Design
قبل از اینکه بگویید:
RabbitMQ
یا
Kafka
یا
gRPC
یا
Redis
یا هر تکنولوژی دیگری...
ابتدا این سه سؤال را بپرسید:
1️⃣ اگر این عملیات Fail شود، آیا Business Transaction باید Fail شود؟
اگر بله → احتمالاً Synchronous / Critical Path
اگر نه → احتمالاً میتواند Asynchronous شود.
2️⃣ آیا Consumer باید بلافاصله نتیجه را پردازش کند یا فقط باید از اتفاق رخداده مطلع شود؟
این سؤال به شما کمک میکند بین Command-oriented Messaging و Event-oriented Communication تصمیم بگیرید.
3️⃣ آیا به قابلیتهایی مثل Retention، چند Consumer مستقل، Replay و Stream Processing نیاز دارید؟
اگر بله، انتخابهایی مثل Kafka جدیتر میشوند.
🚀 در نهایت...
به نظرم یکی از مهمترین مهارتهای System Design این نیست که بتوانی دهها تکنولوژی را نام ببری.
مهمتر این است که بتوانی تشخیص بدهی:
کدام عملیات واقعاً باید همین الان انجام شود؟
و کدام عملیات میتواند چند ثانیه بعد انجام شود، بدون اینکه Business Rule اصلی سیستم نقض شود؟
وقتی این مرز را درست مشخص کردی، انتخاب تکنولوژی خیلی سادهتر میشود.
چون آن موقع دیگر نمیپرسی:
❌ Kafka یا RabbitMQ؟
بلکه میپرسی:
✅ ءBusiness من چه نوع Communication Patternای نیاز دارد؟
و این دقیقاً جایی است که System Design از انتخاب تکنولوژی جدا میشود.
#اشتباهات_مهندسی (Engineering Mistakes)
یک تیم ۵ نفره داشت یک Feature را توسعه میداد. Project Manager گفت:
«کار را تقسیم کنیم که سریعتر پیش بره.»
پس Feature را کردند ۱۰ تا Task:
روی کاغذ عالی بود.
همه مشغول بودند.
اما سه روز بعد...
ءDeveloper A منتظر API بود.
ءDeveloper B منتظر Database Migration بود.
ءDeveloper C منتظر تصمیم Backend بود.
ءDeveloper D کار خودش را تمام کرده بود، اما نمیتوانست Merge کند.
و Developer E داشت روی چیزی کار میکرد که بعداً مشخص شد دیگر لازم نیست.
همه Busy بودند.
اما Feature جلو نمیرفت.
مشکل کجا بود؟
تیم کار را بر اساس تعداد Task تقسیم کرده بود،
نه بر اساس Dependency.
در واقع جریان کار این شکلی بود:
ولی ما وانمود کرده بودیم همه این کارها مستقلاند.
یکی از خطرناکترین اشتباهات در مدیریت Engineering همین است:
ءBusy بودن را با Progress اشتباه بگیریم.
ممکن است ۱۰ نفر همزمان روی ۱۰ Task کار کنند،
اما اگر ۹ تای آنها منتظر یک Task باشند،
سرعت واقعی تیم همان سرعت آن یک Task است.
گاهی برای سریعتر شدن پروژه،
نباید کار بیشتری بین افراد پخش کنیم.
باید Dependencyهای اصلی را پیدا کنیم.
چون در Software Engineering،
تعداد Taskهای انجامشده مهم نیست.
مهم این است که:
چه مقدار از مسیر رسیدن به یک نتیجه واقعی طی شده است.
یک تیم ۵ نفره داشت یک Feature را توسعه میداد. Project Manager گفت:
«کار را تقسیم کنیم که سریعتر پیش بره.»
پس Feature را کردند ۱۰ تا Task:
Developer A → 2 Tasks
Developer B → 2 Tasks
Developer C → 2 Tasks
Developer D → 2 Tasks
Developer E → 2 Tasks
روی کاغذ عالی بود.
همه مشغول بودند.
اما سه روز بعد...
ءDeveloper A منتظر API بود.
ءDeveloper B منتظر Database Migration بود.
ءDeveloper C منتظر تصمیم Backend بود.
ءDeveloper D کار خودش را تمام کرده بود، اما نمیتوانست Merge کند.
و Developer E داشت روی چیزی کار میکرد که بعداً مشخص شد دیگر لازم نیست.
همه Busy بودند.
اما Feature جلو نمیرفت.
مشکل کجا بود؟
تیم کار را بر اساس تعداد Task تقسیم کرده بود،
نه بر اساس Dependency.
در واقع جریان کار این شکلی بود:
Database
↓
Backend
↓
API Contract
↓
Frontend
↓
Integration
ولی ما وانمود کرده بودیم همه این کارها مستقلاند.
یکی از خطرناکترین اشتباهات در مدیریت Engineering همین است:
ءBusy بودن را با Progress اشتباه بگیریم.
ممکن است ۱۰ نفر همزمان روی ۱۰ Task کار کنند،
اما اگر ۹ تای آنها منتظر یک Task باشند،
سرعت واقعی تیم همان سرعت آن یک Task است.
گاهی برای سریعتر شدن پروژه،
نباید کار بیشتری بین افراد پخش کنیم.
باید Dependencyهای اصلی را پیدا کنیم.
چون در Software Engineering،
تعداد Taskهای انجامشده مهم نیست.
مهم این است که:
چه مقدار از مسیر رسیدن به یک نتیجه واقعی طی شده است.
🖼 کار با ImageMagick در C#؛ وقتی System.Drawing دیگر کافی نیست
اگر در یک پروژه NET. با تصویرها کار کرده باشید، احتمالاً خیلی زود به این نیازها میرسید:
🔹 تغییر اندازه تصاویر
🔹 تبدیل فرمت
JPEG، PNG، WebP و ...🔹 ءCrop کردن تصویر
🔹 فشردهسازی
🔹 ساخت Thumbnail
🔹 چرخاندن یا تغییر Orientation
🔹 اضافه کردن Watermark
🔹 پردازش تصاویر در سمت Server
🔹 تولید چند نسخه از یک تصویر با اندازههای مختلف
در پروژههای ساده شاید بتوانید با APIهای معمول NET. بخش زیادی از این کارها را انجام دهید؛ اما وقتی پردازش تصویر جدیتر میشود، معمولاً به یک کتابخانه قدرتمندتر نیاز دارید.
اینجاست که ImageMagick وارد میشود. 🧰
ءImageMagick یک مجموعه ابزار و کتابخانه قدرتمند برای پردازش تصاویر است که از فرمتهای بسیار متنوعی پشتیبانی میکند و در دنیای NET. نیز میتوان از طریق کتابخانههایی مانند Magick.NET با آن کار کرد.
🤔 ءImageMagick دقیقاً چه مشکلی را حل میکند؟
فرض کنید در یک سایت، کاربر یک تصویر با حجم
8 MB و ابعاد 6000 × 4000 آپلود میکند.شما نمیخواهید همان فایل را مستقیماً برای همه کاربران سرو کنید.
ممکن است لازم باشد:
📌 نسخه اصلی را نگه دارید.
📌 یک نسخه
1920px برای Desktop بسازید.📌 یک نسخه
800px برای Mobile بسازید.📌 یک Thumbnail با ابعاد
300 × 300 ایجاد کنید.📌 تصاویر را به
WebP تبدیل کنید.📌 حجم خروجی را کاهش دهید.
📌 ءMetadata غیرضروری را حذف کنید.
یعنی یک فایل ورودی دارید اما چندین خروجی مختلف تولید میکنید.
ءImageMagick دقیقاً برای چنین پردازشهایی بسیار قدرتمند است.
🚀 نصب در پروژه #C
در پروژه NET. میتوانید از Magick.NET استفاده کنید.
برای مثال:
dotnet add package Magick.NET-Q8-AnyCPU
بعد:
using ImageMagick;
حالا میتوانیم یک تصویر را Load کنیم:
using var image = new MagickImage("input.jpg");
Console.WriteLine(image.Width);
Console.WriteLine(image.Height);📐 تغییر اندازه تصویر
یکی از رایجترین عملیاتها:
using var image = new MagickImage("input.jpg");
image.Resize(1200, 0);
image.Write("output.jpg");در اینجا عرض تصویر به
1200px تغییر میکند و ارتفاع متناسب با نسبت تصویر محاسبه میشود.این موضوع برای ساخت نسخههای مختلف تصاویر بسیار کاربردی است.
مثلاً:
Original
│
├── 1920px
├── 1200px
├── 800px
└── 300px Thumbnail
✂️ ؟Crop کردن تصویر
مثلاً میخواهیم بخشی از تصویر را جدا کنیم:
using var image = new MagickImage("input.jpg");
image.Crop(new MagickGeometry(300, 300)
{
IgnoreAspectRatio = true
});
image.Write("cropped.jpg");در پروژههای واقعی میتوانید از این قابلیت برای تولید Avatar، Thumbnail و تصاویر Preview استفاده کنید.
🔄 تبدیل فرمت
یکی از قابلیتهای مهم ImageMagick تبدیل فرمت تصاویر است.
مثلاً:
using var image = new MagickImage("input.jpg");
image.Format = MagickFormat.WebP;
image.Write("output.webp");یعنی:
JPEG
↓
ImageMagick
↓
WebP
این موضوع مخصوصاً برای Web Applicationها مهم است، چون میتوانید نسخهای مناسب برای نمایش در Web تولید کنید و حجم انتقال داده را کاهش دهید.
🗜 کنترل کیفیت و حجم
میتوان کیفیت خروجی را نیز کنترل کرد:
using var image = new MagickImage("input.jpg");
image.Quality = 80;
image.Write("compressed.jpg");اما یک نکته مهم وجود دارد:
ءQuality پایینتر همیشه به معنی نتیجه بهتر نیست.
باید بین:
Image Quality
↕️
File Size
↕️
Network Cost
↕️
Storage Cost
تعادل برقرار کنید.
💧 اضافه کردن Watermark
فرض کنید یک سیستم مدیریت تصاویر دارید و میخواهید روی تصاویر کاربران Watermark قرار دهید. ImageMagick امکان Composite کردن تصاویر را نیز فراهم میکند.
برای مثال:
using var image = new MagickImage("photo.jpg");
using var watermark = new MagickImage("watermark.png");
image.Composite(
watermark,
Gravity.Southeast,
CompositeOperator.Over);
image.Write("result.jpg");حالا Watermark در گوشه تصویر قرار میگیرد.
🏢 یک مثال واقعی در پروژههای بزرگ
فرض کنید یک E-Commerce دارید که روزانه هزاران تصویر محصول دریافت میکند.
کاربر ممکن است تصویری با مشخصات زیر Upload کند:
File Size: 12 MB
Resolution: 8000 × 6000
Format: JPEG
اشتباه این است که همین فایل را مستقیماً در Response به Browser برگردانیم.
بهتر است Pipeline پردازش تصویر داشته باشیم:
در این معماری، API اصلی نباید مجبور باشد تمام پردازش تصویر را در همان Request انجام دهد.
برای تصاویر بزرگ یا پردازشهای سنگین میتوان:
را پیادهسازی کرد.
این طراحی باعث میشود Upload کاربر سریعتر پاسخ داده شود و پردازش سنگین تصویر به Worker منتقل شود. ⚙️
ءImageMagick ابزار قدرتمندی است؛ اما هر پردازش تصویری را نباید داخل Web Request انجام دهید.
فرض کنید کاربر یک تصویر بسیار بزرگ Upload کرده است.
اگر در همان Request:
را انجام دهید، با افزایش همزمانی ممکن است CPU و Memory سرور بهشدت مصرف شوند.
به همین دلیل در سیستمهای پرترافیک بهتر است:
داشته باشیم.
هر فایل تصویری که کاربر Upload میکند، الزاماً قابل اعتماد نیست.
نباید صرفاً به این اعتماد کنیم:
یا:
در یک سیستم واقعی باید Upload Validation، محدودیت اندازه فایل، محدودیت ابعاد تصویر و سیاستهای مناسب برای فرمتهای مجاز داشته باشیم.
همچنین ImageMagick را نباید با تنظیمات ناامن و بدون توجه به سیاستهای امنیتی محیط Production اجرا کرد.
یعنی:
⚖️ ءImageMagick را چه زمانی استفاده کنیم؟ ImageMagick انتخاب بسیار خوبی است وقتی که:
✅ فرمتهای تصویری متنوع دارید.
✅ عملیات پیچیده Image Processing دارید.
✅ به Resize، Crop، Composite، Conversion و Compression نیاز دارید.
✅ سیستم شما باید تعداد زیادی تصویر را پردازش کند.
✅ نیاز دارید یک Processing Pipeline مستقل برای تصاویر داشته باشید.
✅ میخواهید پردازش تصویر را به Worker منتقل کنید.
❌ چه زمانی شاید انتخاب مناسبی نباشد؟
اگر فقط میخواهید:
استفاده از یک کتابخانه سنگین ممکن است بیش از نیاز شما باشد.
همچنین اگر Processing شما کاملاً وابسته به قابلیتهای خاص GPU یا یک Pipeline تخصصی Computer Vision باشد، ImageMagick الزاماً بهترین ابزار نیست.
برای بعضی سناریوها کتابخانههای تخصصیتر انتخاب بهتری هستند.
🧠 یک اشتباه معماری رایج
این کد:
ممکن است در یک پروژه کوچک کاملاً قابل قبول باشد.
اما اگر همین API:
دریافت کند، داستان کاملاً متفاوت میشود.
پس سؤال اصلی این نیست که:
سؤال معماری مهمتر این است:
ءImageMagick فقط یک ابزار برای تبدیل
میتواند بخشی از یک Image Processing Pipeline باشد.
برای مثال:
و اینجاست که استفاده از آن در یک پروژه NET. واقعاً معنا پیدا میکند.
اگر فقط یک Resize ساده دارید، ساده نگهش دارید.
اما اگر با یک سیستم واقعی و پرتعداد از تصاویر سروکار دارید، ImageMagick + Worker + Object Storage + CDN میتواند یک ترکیب بسیار قدرتمند برای ساخت یک Image Processing Pipeline باشد. 🚀
User Upload
│
▼
Object Storage
│
▼
Image Processing
│
┌────────┼────────┐
▼ ▼ ▼
1920px 800px 300px
│ │ │
└────────┼────────┘
▼
WebP / AVIF
│
▼
CDN / Storage
در این معماری، API اصلی نباید مجبور باشد تمام پردازش تصویر را در همان Request انجام دهد.
برای تصاویر بزرگ یا پردازشهای سنگین میتوان:
Upload
↓
Store Original
↓
Publish Event / Message
↓
Image Processing Worker
↓
ImageMagick
↓
Generate Variants
↓
Object Storage
را پیادهسازی کرد.
این طراحی باعث میشود Upload کاربر سریعتر پاسخ داده شود و پردازش سنگین تصویر به Worker منتقل شود. ⚙️
⚠️ اما یک نکته بسیار مهم
ءImageMagick ابزار قدرتمندی است؛ اما هر پردازش تصویری را نباید داخل Web Request انجام دهید.
فرض کنید کاربر یک تصویر بسیار بزرگ Upload کرده است.
اگر در همان Request:
Load Image
↓
Resize
↓
Crop
↓
Convert
↓
Compress
↓
Save
را انجام دهید، با افزایش همزمانی ممکن است CPU و Memory سرور بهشدت مصرف شوند.
به همین دلیل در سیستمهای پرترافیک بهتر است:
API
│
├── Validate
├── Store
└── Queue Message
│
▼
Image Worker
│
▼
ImageMagick
داشته باشیم.
🔐 یک موضوع امنیتی بسیار مهم
هر فایل تصویری که کاربر Upload میکند، الزاماً قابل اعتماد نیست.
نباید صرفاً به این اعتماد کنیم:
Content-Type: image/jpeg
یا:
file.jpg
در یک سیستم واقعی باید Upload Validation، محدودیت اندازه فایل، محدودیت ابعاد تصویر و سیاستهای مناسب برای فرمتهای مجاز داشته باشیم.
همچنین ImageMagick را نباید با تنظیمات ناامن و بدون توجه به سیاستهای امنیتی محیط Production اجرا کرد.
یعنی:
ءImage Processing فقط یک مسئله فنی مربوط به Resize کردن تصویر نیست؛ بخشی از Security Pipeline سیستم هم محسوب میشود. 🔐
⚖️ ءImageMagick را چه زمانی استفاده کنیم؟ ImageMagick انتخاب بسیار خوبی است وقتی که:
✅ فرمتهای تصویری متنوع دارید.
✅ عملیات پیچیده Image Processing دارید.
✅ به Resize، Crop، Composite، Conversion و Compression نیاز دارید.
✅ سیستم شما باید تعداد زیادی تصویر را پردازش کند.
✅ نیاز دارید یک Processing Pipeline مستقل برای تصاویر داشته باشید.
✅ میخواهید پردازش تصویر را به Worker منتقل کنید.
❌ چه زمانی شاید انتخاب مناسبی نباشد؟
اگر فقط میخواهید:
یک تصویر ساده Upload کنید
↓
Resize ساده
↓
Save
استفاده از یک کتابخانه سنگین ممکن است بیش از نیاز شما باشد.
همچنین اگر Processing شما کاملاً وابسته به قابلیتهای خاص GPU یا یک Pipeline تخصصی Computer Vision باشد، ImageMagick الزاماً بهترین ابزار نیست.
برای بعضی سناریوها کتابخانههای تخصصیتر انتخاب بهتری هستند.
🧠 یک اشتباه معماری رایج
این کد:
public async Task<IActionResult> Upload(IFormFile file)
{
using var image = new MagickImage(file.OpenReadStream());
image.Resize(1200, 0);
image.Write("output.webp");
return Ok();
}
ممکن است در یک پروژه کوچک کاملاً قابل قبول باشد.
اما اگر همین API:
1000 concurrent uploads
دریافت کند، داستان کاملاً متفاوت میشود.
پس سؤال اصلی این نیست که:
«آیا ImageMagick سریع است؟»
سؤال معماری مهمتر این است:
«آیا Image Processing باید در Request/Response Lifecycle من اتفاق بیفتد؟»در سیستمهای بزرگ، پاسخ خیلی وقتها خیر است.
🎯 جمعبندی
ءImageMagick فقط یک ابزار برای تبدیل
JPG به PNG نیست.میتواند بخشی از یک Image Processing Pipeline باشد.
برای مثال:
Upload
↓
Validation
↓
Object Storage
↓
Message Queue
↓
Image Worker
↓
ImageMagick
↓
Resize / Crop / Compress / Convert
↓
Generate Variants
↓
Object Storage
↓
CDN
و اینجاست که استفاده از آن در یک پروژه NET. واقعاً معنا پیدا میکند.
اگر فقط یک Resize ساده دارید، ساده نگهش دارید.
اما اگر با یک سیستم واقعی و پرتعداد از تصاویر سروکار دارید، ImageMagick + Worker + Object Storage + CDN میتواند یک ترکیب بسیار قدرتمند برای ساخت یک Image Processing Pipeline باشد. 🚀
🔖هشتگها:
#aspnetcore #imagemagick #imageprocessing
📌پیادهسازی اصولی لغو عملیات (Cancellation Support) در تمام لایههای داتنت
نادیده گرفتن الگوی لغو عملیات یکی از شایعترین نقصها در پروژههای داتنت است. هنگامی که کاربری تب مرورگر را میبندد یا کلاینت درخواست HTTP را متوقف میکند، در صورت عدم انتشار توکن لغو (
CancellationToken)، سرور همچنان به اجرای کوئریهای سنگین دیتابیس، پردازشهای CPU و فراخوانیهای I/O ادامه میدهد که نتیجه آن هدررفت مستقیم منابع و اشباع Thread Pool است. استفاده از Copilot برای افزودن قابلیت Cancellation نباید به «لغو ظاهری یا نمادین» (Fake Cancellation) با چک کردن دستی توکن منتهی شود، بلکه باید انتشار پیوسته و معنادار توکن در تمام لایهها تا پایینترین سطح I/O را پیادهسازی کند.کالبدشکافی لغو نامعتبر در برابر لغو واقعی
رویکرد اشتباه (لغو ظاهری): قرار دادن متوالی
cancellationToken.ThrowIfCancellationRequested() در متدها، بدون ارسال توکن به عملیات پایهای دیتابیس یا سوکت شبکه. در این حالت، تا زمانی که کوئری دیتابیس تمام نشود، برنامه متوجه توقف درخواست نمیشود.رویکرد اصولی (لغو واقعی در سطح سختافزار/درایور): ارسال توکن به درایور دیتابیس (مانند Npgsql یا Microsoft.Data.SqlClient) تا به محض لغو درخواست کلاینت، دستور
ATTN یا لغو سوکت به سرور دیتابیس فرستاده شده و پردازش همانجا متوقف شود.ساختار پرامپت مهندسی برای ردیابی و افزودن CancellationToken
برای گسترش ایمن توکن لغو در لایههای معماری:
ساختار لغو عملیات (CancellationToken) را در تمام زنجیره این سناریو از لایه Controller تا پایگاه داده پیادهسازی کن.
الزامات و محورهای ردیابی:
1️⃣دریافت توکن از درخواست HTTP کلاینت در لایه اکشن کنترلر / Minimal API.
2️⃣انتقال مستقیم توکن از طریق Interfaceهای لایه سرویس و ریپازیتوری.
3️⃣اعمال مستقیم توکن روی درایور و متدهای ناهمگام EF Core (مانند ToListAsyncیا SaveChangesAsync)
4️⃣پرهیز از لغو ظاهری (Fake Cancellation) و توضیح دقیق نقطهای که لغو به صورت فیزیکی عملیات I/O را قطع میکند.
5️⃣مدیریت تمیز استثنای OperationCanceledException بدون لاگهای خطای بحرانی نامرتبط.
ردیابی گامبهگام در لایههای مختلف معماری داتنت
۱. لایه Controller / Presentation
کنترلر فریمورک ASP.NET Core به طور خودکار
HttpContext.RequestAborted را به پارامترهای از نوع CancellationToken متصل (Bind) میکند:[HttpGet]
public async Task<ActionResult<IReadOnlyList<OrderResponseDto>>> GetOrdersAsync(
CancellationToken cancellationToken)
{
var orders = await orderService.GetOrdersAsync(cancellationToken);
return Ok(orders);
}
۲. لایه Service و Repository
توکن باید بدون دستکاری و ایجاد State اضافی از امضای متدها عبور کند:
public interface IOrderRepository
{
Task<IReadOnlyList<Order>> GetOrdersAsync(CancellationToken cancellationToken);
}
public sealed class OrderRepository(ApplicationDbContext dbContext) : IOrderRepository
{
public async Task<IReadOnlyList<Order>> GetOrdersAsync(CancellationToken cancellationToken)
{
// نقطه اثر واقعی: توکن به صورت مستقیم به درایور پایگاه داده فرستاده میشود
return await dbContext.Orders
.AsNoTracking()
.ToListAsync(cancellationToken);
}
}
نکات تکمیلی برای سناریوهای پیشرفته لغو عملیات
ترکیب توکنها با
CancellationTokenSource.CreateLinkedTokenSource: در پردازشهای پسزمینه یا فراخوانی وبسرویسها، گاهی نیاز به اعمال Timeout مشخص در کنار درخواست توقف کاربر وجود دارد:using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
await externalClient.FetchDataAsync(linkedCts.Token);
مدیریت لاگها در زمان لغو: استثنای
OperationCanceledException یا TaskCanceledException یک رفتار طبیعی و مورد انتظار است، نه یک باگ غیرمنتظره (Crash). به Copilot تأکید کنید که هنگام مدیریت خطا در Middlewareها، لغو درخواست را به عنوان خطای سطحی (LogInformation یا LogDebug) لاگ کند، نه به عنوان خطای بحرانی سطح LogError.عملیات بحرانی غیرقابل لغو: در تراکنشهای مالی یا ثبت لاگهای حساس که پس از شروع نباید متوقف شوند، از
CancellationToken.None استفاده کنید تا لغو سمت کاربر باعث ناتمام ماندن ثبت اسناد نشود.قاعده کلیدی: انتشار توکن لغو باید پیوسته و بدون وقفه باشد؛ هدف لغو عملیات، آزادسازی فوری سوکتها، اتصالات دیتابیس و منابع سختافزاری در پایینترین لایه I/O است.
#تصمیمهای_مهندسی (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، خوب یا بد نیستند.
هر کدام، هزینهها و مزایای خودشان را دارند.
و تصمیم درست،
همان تصمیمی است که با نیاز واقعی سیستم همخوانی داشته باشد.