Interview Bank · بانک سوالات متوسطIntermediate ~44 دقیقه مطالعه~36 min read
پرسشوپاسخ سریع: معماری، همزمانی و طراحی سیستمRapid-Fire Q&A: Architecture, Concurrency & System Design
در این فصل هر پرسش مصاحبهی معماری و همزمانی را با یک تشبیه ملموس و بعد پاسخ دقیق یاد میگیری تا از مدل حافظه جاوا تا ساگا و طراحی سیستم را نه فقط حفظ، بلکه واقعاً بفهمی.In this chapter you learn every architecture and concurrency interview question through a concrete analogy first and then the precise answer, so you truly understand the JMM, sagas, and system design instead of just memorizing them.
خیلی از مهندسهای خوب پای سؤالهای سریع مصاحبه گیر میکنند — نه چون بلد نیستند، بلکه چون هیچوقت مجبور نشدهاند فکرشان را در ۶۰ ثانیه، تمیز و مطمئن، بیرون بریزند. این فصل دقیقاً همان تمرین است: هر مدخل پرسشی است که واقعاً از تو پرسیده میشود، و ما هر پاسخ را اول با یک تشبیه ملموس «حسکردنی» میکنیم و بعد به پاسخ دقیق و سطحارشد میرسیم.
روش استفاده ساده است: پاسخ را بخوان، بعد چشم ببند و آن را با صدای بلند بازسازی کن. اگر نتوانستی در یک نفس بگویی، هنوز مالِ تو نشده است.
هفت ایستگاه پیش رو داری:
- همزمانی و مدل حافظه جاوا (JMM) — happens-before، volatile، بنبست، ریسههای مجازی و Loom.
- الگوهای طراحی و SOLID — پنج اصل، LSP، و خانوادهی الگوهای «پوششدهنده».
- DDD — Entity، Value Object، Aggregate، bounded context، لایه ضدفساد.
- میکروسرویس، CAP و سیستمهای توزیعشده — ایدمپوتنسی، outbox، ساگا، تابآوری.
- داده، سازگاری و مقیاس — سازگاری قوی/نهایی/علّی، شاردینگ، کش.
- طراحی سیستم باز — کوتاهکننده URL، محدودکننده نرخ، فید، مولد شناسه.
- رفتاری و تصمیم معماری — پاسخدهی در قالب STAR. پرسشهای تلهدار با [تله] و پرسشهای رفتاری با نکته STAR مشخص شدهاند.
بخش ۰ — واژههایی که باید حس کنی
قبل از شروع، سه واژه هست که در کل فصل تکرار میشوند. اگر اینها را «حس» کنی، بقیه راحتتر میشود.
تصور کن یک نگهبان بانک استخدام میکنی. سه چیز از او میپرسی:
- ناوردا (invariant) چیست که او حفظ میکند؟ «موجودی هیچ حساب هرگز منفی نشود.» ناوردا یعنی حقیقتی که همیشه باید برقرار بماند.
- حالت خرابی (failure mode) که او ایجاد میکند چیست؟ شاید در ازدحام، صف جلوی در طولانی شود. یعنی راهِ تازهای که سیستم میتواند از آن بشکند.
- هزینه (cost) او چیست؟ حقوقش، و کندیای که به کار اضافه میکند.
مصاحبهگر تعریفِ حفظکرده نمیخواهد؛ میخواهد ببیند میتوانی برای هر مکانیزم این سه را نام ببری: چه ناوردایی را حفظ میکند، چه حالت خرابی میآورد، و چه هزینهای دارد. هر پاسخ این فصل حول همین سهگانه ساخته شده است.
خطیپذیری (linearizability) یعنی سیستم طوری رفتار کند که انگار هر عمل در یک لحظهی واحد اتفاق افتاده و همه بلافاصله آن را میبینند — مثل یک صندوقدار واحد که همه در صف پشت اویند. ایدمپوتنت (idempotent) یعنی عملی که اگر N بار تکرارش کنی همان اثرِ یکبار را دارد — مثل زدن دکمهی طبقه در آسانسور: بار دوم چیزی عوض نمیشود.
بخش ۱ — همزمانی و مدل حافظه جاوا (JMM)
تصور کن دو کارمند (ریسه) روی یک وایتبورد مشترک (حافظه اصلی) کار میکنند، اما هر کدام یک دفترچهی شخصی (کش پردازنده / ثبات) هم دارد. برای سرعت، هر کارمند تا وقتی مجبور نشود چیزی از دفترچهاش را روی وایتبرد پاکنویس نمیکند. حالا سؤال حیاتی این است: کارمند دوم کِی و کدام نوشتههای کارمند اول را روی وایتبرد میبیند؟ JMM دقیقاً همین قانونِ «چه کسی چه چیزی را میبیند» است — نه قانونِ «چه کسی کِی اجرا میشود».
شهود: JMM یک ترتیب زمانیِ کامل نمیسازد؛ فقط یالهای مطمئنی میکشد که میگویند «هرچه اینطرفِ یال نوشته شده، آنطرفِ یال حتماً دیده میشود».
پاسخ: JMM یک ترتیب جزئی به نام رخدادهپیشاز (happens-before) روی عملیات حافظه تعریف میکند. اگر عمل A پیش از B رخ داده باشد، نوشتههای A برای B قابلمشاهده و پیش از آن مرتباند. بدون یال happens-before، نوشتههای یک ریسه ممکن است بازچینش (reorder)، کش، یا برای ریسه دیگر تا ابد نامرئی بمانند. پس JMM درباره «کِی چیزها اجرا میشوند» نیست؛ درباره اینکه یک خواندن مجاز است چه نوشتهای را ببیند است.
یالهای کلیدی که باید حفظ باشی:
- ترتیب برنامه درون یک ریسه؛
unlock→lockروی همان مانیتور؛- نوشتن
volatile→خواندن بعدیvolatileاز همان فیلد؛ Thread.start()→نخستین اقدام ریسه؛- آخرین اقدام یک ریسه→
join()ریسه دیگر؛ - و انجماد فیلد
finalدر پایان سازنده.
شهود: volatile یک بلندگو روی یک متغیر است: هرچه بگویی همه فوراً میشنوند، اما نمیتواند دو نفر را از حرفزدن همزمان بازدارد. synchronized یک اتاق با یک کلید است: فقط یک نفر تو میرود، کار چند فیلد را یکجا انجام میدهد، و موقع خروج همهچیز را منتشر میکند.
پاسخ: volatile برای یک متغیر واحد رؤیتپذیری (visibility) و ترتیب (ordering) میدهد اما اتمیبودن (atomicity) برای عملیات مرکب ندارد — x++ در واقع خواندن-تغییر-نوشتن است و همچنان مسابقهای (racy) میماند. synchronized انحصار متقابل + رؤیتپذیری برای هر چیزی که قفل محافظت میکند میدهد و از طریق unlock/lock یال happens-before میسازد.
قاعده انتخاب: برای یک پرچم ساده یا یک ارجاعِ منتشرشدهی امن از volatile استفاده کن؛ وقتی باید یک ناوردا روی چند فیلد را با هم حفظ کنی از synchronized/lock استفاده کن.
کد:
class Holder {
static boolean ready = false; // volatile نیست
static int value = 0;
static void writer() { value = 42; ready = true; }
static void reader() { while (!ready) {} System.out.println(value); }
}
// ریسه A متد reader() و ریسه B متد writer() را اجرا میکند
شهود: بدون یال happens-before، ریسهی خواننده اجازه دارد ready را «یکبار برای همیشه» در دفترچهی خودش بخواند و دیگر به وایتبرد نگاه نکند.
پاسخ: ممکن است 42 چاپ کند، ممکن است 0 چاپ کند، یا ممکن است هرگز تمام نشود. چون ready از نوع volatile نیست، هیچ یال happens-before میان writer و reader وجود ندارد. کامپایلر JIT مجاز است !ready را از حلقه بیرون بکشد (یکبار در یک ثبات/register بخواند) ← حلقهی بینهایت. حتی اگر از حلقه خارج شود، value هنوز ممکن است 0 دیده شود، چون نوشتن value میتواند نسبت به ready بازچینش شود و هیچ تضمین رؤیتپذیریای نیست.
volatile کردن ready هر دو مشکل را حل میکند: نوشتنِ volatile مقدار value را هم منتشر میکند (ترتیبِ سواریکننده / piggyback)، و خواندنِ volatile در هر تکرار دوباره واکشی میشود.
شهود: ساختن یک شیء دو گام دارد: اول حافظه پر میشود (سازنده)، بعد آدرسش به متغیر داده میشود. اگر این دو گام جابهجا دیده شوند، یک ریسهی دیگر آدرسِ غیرnull میبیند اما پشتش یک شیءِ نیمهساخته است.
پاسخ: ارجاعِ شیء میتوانست پیش از پایان سازنده منتشر شود، پس ریسهی دیگر یک شیء غیرnull اما نیمهساخته میدید. راهحل volatile روی فیلد است: نوشتنِ volatile ارجاع نمیتواند پیش از نوشتههای سازنده بازچینش شود، و خواندنِ volatile یال happens-before برقرار میکند.
class Lazy {
private static volatile Lazy instance; // volatile الزامی است
static Lazy get() {
Lazy r = instance; // یکبار خواندن volatile
if (r == null) {
synchronized (Lazy.class) {
r = instance;
if (r == null) instance = r = new Lazy();
}
}
return r;
}
}
نکتهی ارشد: معمولاً اصطلاح مقداردهی-اولیهی-برحسب-تقاضا با کلاسِ دارنده (initialization-on-demand holder) — یعنی یک کلاس تودرتوی static — بهتر است؛ کلاسلودر مقداردهیِ تنبل و ایمنریسه را رایگان و بدون هیچ volatileای به تو میدهد.
شهود: بنبست مثل چهارراهی است که چهار ماشین همزمان وارد شدهاند و هرکدام منتظر است آنکه سمت راستش است اول برود. اگر فقط یکی از چهار شرط را برداری، گره باز میشود.
پاسخ: چهار شرط عبارتاند از: انحصار متقابل، نگهداشتن-و-انتظار (hold-and-wait)، بدون-پیشدستی (no-preemption)، و انتظار دوری (circular wait). در عمل انتظار دوری را با تحمیل ترتیب سراسری قفلها میشکنی — یعنی همیشه قفلها را به یک ترتیبِ متعارف بگیر (مثلاً بر اساس System.identityHashCode یا یک کلید تجاری). جایگزینها: شکستن hold-and-wait با tryLock + عقبنشینی، یا شکستن no-preemption با مهلت زمانی (timeout).
کد مشکلدار:
void transfer(Account from, Account to, long amount) {
synchronized (from) {
synchronized (to) { from.debit(amount); to.credit(amount); }
}
}
شهود: اگر یکی transfer(A,B) و دیگری همزمان transfer(B,A) را صدا بزند، هرکدام یک قفل میگیرد و برای همیشه منتظر آنیکی میماند — همان انتظار دوری.
پاسخ: با مرتبسازی قفلها بر اساس یک شناسهی پایدار، ترتیب سراسری تحمیل کن تا هر دو فراخوان همیشه یک ترتیب بگیرند:
void transfer(Account from, Account to, long amount) {
Account first = from.id() < to.id() ? from : to;
Account second = from.id() < to.id() ? to : from;
synchronized (first) {
synchronized (second) { from.debit(amount); to.credit(amount); }
}
}
اگر شناسهها میتوانند برابر باشند (همان حساب)، در برابر انتقال به خود محافظت کن یا از یک قفلِ شکنندهی تساوی (tie-breaker) استفاده کن.
شهود: بنبست یعنی همه ایستادهاند و مردهاند. زندهقفل یعنی دو نفر در راهرو که مدام هر دو به یک سمت کنار میروند و باز روبهرو میشوند — فعالاند اما هیچکس رد نمیشود. گرسنگی یعنی یک نفر پشتِ در مانده چون بقیه همیشه زودتر میپرند تو.
پاسخ: بنبست: ریسهها تا ابد مسدود، بدون پیشرفت، بدون مصرف CPU. زندهقفل: ریسهها فعالاند و مدام حالتشان را در واکنش به یکدیگر عوض میکنند اما پیشرفتی نمیکنند. گرسنگی: یک ریسه پیشرفت نمیکند چون دیگران منبعی را انحصار کردهاند (قفلهای ناعادلانه، وارونگی اولویت). سیاستهای عدالت (fairness) و عقبنشینی تصادفی (randomized backoff) زندهقفل و گرسنگی را برطرف میکنند.
یک رستوران را تصور کن با فقط ۸ آشپز (هستهی CPU / ریسهی حامل). سبک قدیمی: به ازای هر مشتری یک آشپز بگذار — با ۱۰٬۰۰۰ مشتری، ۱۰٬۰۰۰ آشپز میخواهی که ممکن نیست. سبک Loom: آشپز فقط وقتی سرِ یک سفارش میایستد که واقعاً دارد میپزد؛ لحظهای که سفارش باید در فر بماند (I/O)، آشپز کنار میرود و سفارش بعدی را برمیدارد. حالا با همان ۸ آشپز میتوانی هزاران سفارش را همزمان مدیریت کنی — به شرطی که کار عمدتاً «منتظر فر ماندن» باشد، نه «خردکردن مداوم».
شهود: بالا را ببین — آشپزها همان ریسههای حاملاند، و مشتریها ریسههای مجازی.
پاسخ: ریسههای مجازی (پایدار از جاوا ۲۱، JEP 444) ریسههای سبکِ مدیریتشده توسط JDK هستند که بهصورت M:N روی استخر کوچکی از ریسههای حامل (carrier threads) سیستمعامل چندگانهسازی میشوند. وقتی یک ریسهی مجازی روی ورودی/خروجی مسدود میشود، از حاملش جدا (unmount) میشود و ریسهی سیستمعامل را آزاد میکند تا ریسهی مجازی دیگری اجرا شود. همین باعث میشود سبک سادهی «یک ریسه به ازای هر درخواست» به میلیونها کارِ همزمان مقیاس بخورد.
آنها وقتی کار مقید به ورودی/خروجی و مسدودکننده است کمک میکنند (فراخوانی پایگاهداده، فناوت HTTP). کارِ مقید به پردازنده (CPU-bound) را سریع نمیکنند — همچنان فقط N هسته داری و یک استخرِ ثابتِ ریسههای پلتفرم برای آن کافی است. و هرگز ریسههای مجازی را استخری (pool) نکن؛ به ازای هر کار یکی بساز.
شهود: پینشدن یعنی آشپز کنار نمیرود؛ حتی وقتی سفارش فقط باید در فر بماند، سرِ آن سفارش قفل میماند و صف بقیه راکد میشود.
پاسخ: پینشدن یعنی یک ریسهی مجازی نمیتواند جدا شود و در طول یک عمل مسدودکننده حاملش را گروگان میگیرد. در جاوا ۲۱ تا ۲۳، ورود به بلوک/متد synchronized ریسهی مجازی را پین میکرد (مانیتور، حامل را ردیابی میکرد نه ریسهی مجازی را)، پس ورودی/خروجیِ طولانی درون synchronized استخرِ حامل را گرسنه میکرد. توصیهی دورانِ جاوا ۲۱ این بود: synchronizedِ داغی که ورودی/خروجی را محافظت میکند با ReentrantLock جایگزین کن.
جاوا ۲۴ (JEP 491) پیادهسازیِ مانیتور را بازنویسی کرد تا آگاه به ریسهی مجازی باشد، پس synchronized دیگر تقریباً در همهی موارد پین نمیکند و پرچمِ -Djdk.tracePinnedThreads حذف شد. منابعِ باقیماندهی پینشدن: اجرای کد بومی (JNI) یا توابعِ خارجی (foreign functions). جمعبندیِ ارشد: نسخهی JDK هدفت را بدان — قاعدهی «از synchronized پرهیز کن» یک قاعدهی جاوا ۲۱/۲۲/۲۳ است، نه دائمی.
شهود: ThreadLocal مثل چسباندن یک برچسبِ یادداشت به خودِ آشپز است، نه به سفارش. اگر آشپزها بازیافت شوند (استخر)، برچسبِ سفارشِ قبلی روی سفارشِ بعدی میماند؛ و اگر میلیونها ریسه داشته باشی، میلیونها برچسب حافظه را میبلعد.
پاسخ: ThreadLocal وقتی با ریسههای استخری استفاده شود نشتی میکند (مقدار میان کارهای بیارتباط زنده میماند) و حافظهی بهازایریسه اضافه میکند که با میلیونها ریسهی مجازی منفجر میشود. Loom مقادیر دامنهای (scoped values) را معرفی میکند — ScopedValue، که حدود جاوا ۲۵ نهایی شد — که تغییرناپذیر، مقید به یک دامنهی پویا، و ارزان برای وراثت به زیرکارهای همزمانیِ ساختاریافته است. برای زمینهی محدود به درخواست (شناسهی مستأجر، اصلِ هویت) اینها را ترجیح بده.
شهود: CompletableFuture مثل فرستادن چند پیک بدون سرپرست است؛ اگر یکی گم شود، بقیه بیخبر به راهشان ادامه میدهند. همزمانیِ ساختاریافته یک سرپرست میگذارد که اگر یک پیک شکست خورد، بقیه را برمیگرداند و یک گزارشِ روشن میدهد.
پاسخ: CompletableFuture فراخوانهای ناهمگام را ترکیب میکند اما رابطهی والد-فرزند را از دست میدهد: خرابی در یک شاخه بهطور خودکار خواهرهایش را لغو نمیکند و خطاها بهصورت استثناهای درهمتنیده ظاهر میشوند. همزمانی ساختاریافته (StructuredTaskScope، پیشنمایش تا جاوا ۲۱ تا ۲۴) یک گروه زیرکار را بهعنوان یک واحد میبیند: اگر یکی خراب شود، دامنه بقیه را لغو و یک خطای روشن را منتشر میکند؛ دامنه نمیتواند ریسه نشت دهد چون پیش از بازگشت join میکند. نظمِ پشتهی فراخوانی را که async شکسته بود بازمیگرداند.
بخش ۲ — الگوهای طراحی و SOLID
شهود: هر اصل یک «بوی بد» در کد را خنثی میکند؛ اگر بو را بشناسی، اصل را بدون حفظکردن به یاد میآوری.
پاسخ:
- SRP — یک دلیل برای تغییر؛ بو: کلاسی که سه تیمِ متفاوت ویرایشش میکنند.
- OCP — باز برای گسترش، بسته برای تغییر؛ بو: افزودنِ یک حالت یعنی ویرایشِ یک
switchغولپیکر. - LSP — زیرنوع باید جایگزینپذیر باشد؛ بو:
if (obj instanceof X)پیش از فراخوانی متد، یا زیرکلاسی کهUnsupportedOperationExceptionپرتاب میکند. - ISP — چند رابطِ کوچک بهتر از یک رابطِ چاق است؛ بو: پیادهسازها متدهایی را که نیاز ندارند
stubمیکنند. - DIP — به انتزاع وابسته باش نه به پیادهسازیِ مشخص؛ بو:
newکردنِ یک سرویسِ مشخص درونِ منطق تجاری.
شهود: در ریاضی مربع یک مستطیل است، اما در کد این ارثبری یک تله است، چون قراردادِ رفتاریشان فرق دارد.
پاسخ: کلاسیک: Square extends Rectangle. Rectangle متدهای مستقلِ setWidth/setHeight دارد؛ یک Square باید آنها را برابر نگه دارد، پس بازنویسیِ setWidth که ارتفاع را هم تنظیم کند، هر کلاینتی را که فرض میکند setWidth(5); setHeight(4) مساحتِ ۲۰ میدهد میشکند. زیرنوع یک پیششرط را تقویت یا یک پسشرط را تضعیف میکند — یعنی قرارداد نقض میشود، نه کد. رفع: آنها را نوعهای جدا مدل کن یا اشکال را تغییرناپذیر (immutable) کن.
شهود: هر سه «رفتار را متغیر» میکنند، اما با سه اهرمِ متفاوت: تزریق، ارثبری، و تغییرِ حالتِ درونی.
پاسخ: Strategy یک الگوریتمِ قابلتعویض را از طریقِ ترکیب (composition) تزریق میکند — تعویض در زمانِ اجرا. Template Method اسکلت را در کلاسِ پایه ثابت میکند و میگذارد زیرکلاسها گامها را از طریق ارثبری پر کنند — زمانِ کامپایل. State همان Strategy است که شیء رفتارِ خودش را با تغییرِ حالتِ درونی عوض میکند و حالتها بلدند چطور گذار (transition) کنند. عموماً Strategy را بر Template Method ترجیح بده — ترکیب بر ارثبری.
شهود: Singleton وقتی خوب است که «فقط یک نمونه» یک واقعیتِ دامنه باشد؛ وقتی بد است که به یک متغیرِ سراسریِ پنهان تبدیل شود که همه به آن دست میزنند.
پاسخ: وقتی به یک متغیرِ سراسریِ تغییرپذیر تبدیل میشود: وابستگیها را پنهان میکند، تستپذیری را از بین میبرد (نمیتوانی جایگزینش کنی)، جفتشدگیِ پنهان میسازد، و روی مقداردهیِ تنبل خطرِ همزمانی دارد. در دنیای تزریقِ وابستگی (DI) یک نمونهی واحدِ مدیریتشده توسط ظرف (container) میخواهی — scope = singleton — که صریحاً تزریق شود؛ همان طولعمر، بدونِ بیماریِ حالتِ سراسری.
شهود: هر چهار الگو دور یک چیز پوشش میکشند، اما با نیتِ متفاوت: تبدیل، سادهسازی، کنترل، افزودن.
پاسخ:
- Adapter: یک رابط را به رابطِ دیگری که کلاینت انتظار دارد تبدیل میکند (یکپارچهسازی).
- Facade: یک نقطهی ورودِ سادهشده روی یک زیرسیستمِ پیچیده (کاهشِ سطح).
- Proxy: همان رابط، دسترسی را کنترل میکند (بارگذاریِ تنبل، امنیت، راهدور، کش)؛ Spring AOP از پروکسیِ پویا استفاده میکند.
- Decorator: همان رابط، با پشتهکردنِ پوششها رفتار اضافه میکند (مثل
BufferedInputStream).
تمایزِ کلیدی: Adapter رابط را عوض میکند، Facade سادهاش میکند، Proxy و Decorator نگهش میدارند — اما Proxy کنترل میکند و Decorator میافزاید.
بخش ۳ — DDD و مدلسازی دامنه
یک اسکناسِ ۱۰ هزار تومانی را در نظر بگیر: برایت مهم نیست کدام اسکناس است، فقط ارزشش مهم است — این یک Value Object است (مقداری، تعویضپذیر). حالا حسابِ بانکیِ تو: حتی اگر موجودیاش صفر شود، همان حساب با همان شماره است و تاریخچه دارد — این یک Entity است (هویتدار، دارای چرخهی عمر).
شهود: بالا را ببین؛ و Aggregate را مثلِ یک «پرونده» تصور کن که فقط از طریقِ روی جلدش باز میشود.
پاسخ: یک Entity هویت و چرخهی عمر دارد (تساوی بر اساسِ شناسه). یک Value Object تغییرناپذیر است و بر اساسِ مقدار مقایسه میشود (Money، Address)، بدونِ هویت. یک Aggregate یک مرزِ سازگاری (consistency boundary) است: خوشهای از موجودیتها/VOها با یک ریشهی تجمیع (aggregate root) که تنها نقطهی ورودِ بیرونی است؛ ناوردای درونِ تجمیع تراکنشی اعمال میشود. قاعدهی سرانگشتی: یک تراکنش باید یک تجمیع را تغییر دهد؛ سازگاریِ بینتجمیعی نهایی (eventual) است — از طریقِ رویدادهای دامنه (domain events).
شهود: واژهی «مشتری» در واحدِ فروش با واحدِ صورتحساب با واحدِ پشتیبانی یک چیز نیست؛ اگر بخواهی همه را در یک تعریفِ واحد بچپانی، یک مدلِ درهمجوشِ همهفنحریف میسازی که هیچکس راضی نیست.
پاسخ: یک bounded context مرزی است که درونِ آن یک مدل و زبانِ فراگیرش (ubiquitous language) سازگارند. تحمیلِ یک مدلِ مشترک روی همهی واحدها یک آشوبِ جفتشده میسازد. زمینههای محدود بهطورِ طبیعی روی مرزهای میکروسرویس و مالکیتِ تیم نگاشت میشوند. روابطِ میانِ زمینهها (شراکت، مشتری-تأمینکننده، همنوا/conformist، لایهی ضدفساد) قراردادِ یکپارچهسازیِ توست.
شهود: مثلِ یک مترجمِ سفارت: هرچه از طرفِ بیگانه میآید به زبانِ خودت ترجمه میشود تا واژگان و مفاهیمِ آنها هرگز مستقیماً واردِ خانهات نشوند.
پاسخ: یک لایهی ترجمه که مدلِ تو را از یک مدلِ بیرونی/قدیمی جدا میکند تا مفاهیم و واژگانشان به دامنهات نشت نکنند. در مرز تبدیل میکند (DTOِ آنها → شیءهای مقداریِ تو)، از زبانِ فراگیرِ تو محافظت میکند، و میگذارد سیستمِ بالادست بدونِ موجانداختن به درون تغییر کند.
بخش ۴ — میکروسرویس، CAP و سیستمهای توزیعشده
تصور کن دو کارمندِ فروش در دو شهر یک دفترِ موجودیِ مشترک دارند. حالا خطِ تلفنِ بینشان قطع میشود (پارتیشن). دو انتخاب داری: یا هر دو دست از فروش بکشند تا شمارهها هرگز ناهماهنگ نشوند (سازگاری، اما دیگر نمیفروشی)، یا هر دو به فروش ادامه دهند و بعداً حسابها را آشتی دهند (دسترسپذیری، اما لحظهای ممکن است دوتا یک قلم را بفروشند). وقتی خط قطع است، نمیتوانی هر دو را داشته باشی.
شهود: بالا را ببین؛ «پارتیشن» یعنی همان خطِ قطعشده.
پاسخ: زیرِ یک پارتیشنِ شبکه (P)، یک سیستمِ توزیعشده باید بینِ سازگاری (C، خطیپذیری/linearizability) و دسترسپذیری (A، هر درخواست یک پاسخِ بدونخطا میگیرد) انتخاب کند. بدخوانیِ رایج: «۲ از ۳ را بردار». پارتیشن اختیاری نیست — شبکهها خراب میشوند — پس در واقع در طولِ یک پارتیشن بینِ CP یا AP انتخاب میکنی. وقتی پارتیشن نیست، میتوانی هم C هم A داشته باشی. PACELC این را گسترش میدهد: در غیرِ اینصورت (Else، بدونِ پارتیشن)، بینِ تأخیر (Latency) و سازگاری مبادله میکنی. این عدسیِ مفیدتری برای طراحیِ روزمره است.
پاسخ: CP: سیستمی بر پایهی اجماع (etcd، ZooKeeper، یا یک RDBMSِ تکرهبر با تکثیرِ همگام) — در سمتِ اقلیتِ پارتیشن نوشتن را رد میکند تا سازگاری حفظ شود. AP: Cassandra/DynamoDB با کورومِ تنظیمشده برای دسترسپذیری، یا DNS — همچنان سرویس میدهند و بعداً آشتی میدهند (آخرین-نوشته-برنده، ساعتهای برداری/vector clocks، CRDT). توجه: میتوان بهازای عملیات تنظیم کرد (سطوحِ سازگاریِ Cassandra).
شهود: در شبکه هرگز نمیفهمی «درخواستم گم شد» یا «جوابش گم شد» — پس مجبوری دوباره بفرستی. اگر عمل ایدمپوتنت نباشد، این «دوباره فرستادن» یعنی دوبار پرداخت.
پاسخ: یک عمل ایدمپوتنت است اگر اعمالِ N بار همان اثرِ اعمالِ یکبار را داشته باشد. چون در شبکه نمیتوانی «درخواست گم شد» را از «پاسخ گم شد» تشخیص دهی، کلاینتها بازتلاش (retry) میکنند و بازتلاشها تکراری تولید میکنند. ایدمپوتنسی همان چیزی است که بازتلاش را امن میکند. با یک کلید ایدمپوتنسی پیاده کن: کلاینت یک کلیدِ یکتا میفرستد؛ سرور (کلید → نتیجه) را ثبت میکند و روی تکرار بهجای اجرای دوباره، نتیجهی ذخیرهشده را برمیگرداند.
-- درج پرداخت ایدمپوتنت: کلید یکتا تکراری را بیاثر میکند
INSERT INTO payments (idempotency_key, order_id, amount, status)
VALUES ('c1f...-uuid', 42, 1999, 'CAPTURED')
ON CONFLICT (idempotency_key) DO NOTHING; -- Postgres: تلاش دوم صفر ردیف درج میکند
شهود: دو کارِ جدا در دو سیستمِ جدا بدونِ یک تراکنشِ مشترک، مثلِ گفتنِ «بله» در محضر و بعد رفتن به بانک برای امضاست — بینِ این دو هر اتفاقی میتواند بیفتد و نیمی انجام شود.
پاسخ: چون آنها دو سیستمِ جدا بدونِ تراکنشِ مشترکاند — مسئلهی نوشتنِ دوگانه (dual-write). اگر پایگاهداده را کامیت کنی و سپس بروکر خاموش باشد، رویداد گم میشود؛ اگر منتشر کنی و سپس پایگاهداده rollback شود، یک رویدادِ شبح منتشر کردهای. اتمیبودن میانِ این دو وجود ندارد. راهحل صندوقِ خروجیِ تراکنشی (Transactional Outbox) است: درونِ همان تراکنشِ پایگاهداده که حالت را عوض میکند، رویداد را در جدولِ outbox درج کن. یک بازپخشکنندهی جداگانه (نظرسنجی/polling یا CDC با Debezium که WAL را میخواند) ردیفهای کامیتشدهی outbox را به کافکا منتشر میکند. حالا تغییرِ حالت و «قصدِ انتشار» اتمیاند؛ تحویل حداقل-یکبار (at-least-once) میشود، پس مصرفکنندهها باید ایدمپوتنت باشند.
شهود: رقصآرایی مثلِ یک رقصِ گروهی بدونِ کارگردان است — هر رقصنده با دیدنِ حرکتِ بغلیاش حرکتِ بعدی را میکند؛ زیبا اما فهمِ کلِ جریان سخت. هماهنگسازیِ متمرکز یک کارگردان دارد که به هرکس میگوید کِی چه کند.
پاسخ: یک ساگا یک تراکنشِ توزیعشده را بهصورتِ دنبالهای از تراکنشهای محلی مدیریت میکند که هرکدام یک اقدامِ جبرانی (compensating action) برای بازگرداندن دارند (بازگردانیِ توزیعشده وجود ندارد). رقصآرایی: سرویسها به رویدادهای یکدیگر واکنش میدهند، بدونِ هماهنگکنندهی مرکزی — جدا اما دنبالکردن و استدلال دربارهاش سخت (جریانِ نوظهور). هماهنگسازیِ متمرکز: یک هماهنگکنندهی مرکزیِ ساگا به هر سرویس میگوید چه کند و جبرانها را میراند — صریح، قابلمشاهده، تغییرِ آسانتر، به بهای یک هماهنگکننده. ساگا سازگاریِ نهایی و اتمیبودن از طریقِ جبران میدهد، هرگز جداسازی (isolation) — چون میتوانی حالتهای میانی را ببینی، پس برای قفلهای معنایی (semantic locks) یا حالتهای در انتظار (pending) طراحی کن.
شهود: 2PC مثلِ این است که چند طرفِ معامله همه دستشان روی قلم باشد و منتظرِ اشارهی یک نفر بمانند تا همزمان امضا کنند؛ اگر آن یک نفر غیب شود، همه با دستِ بالا خشک میمانند.
پاسخ: 2PC (کامیتِ دومرحلهای) اتمیبودنِ واقعیِ ACID میانِ سرویسها را از طریقِ یک هماهنگکننده میدهد (آمادهسازی، سپس کامیت/لغو). اما مسدودکننده است: اگر هماهنگکننده پس از آمادهسازی بمیرد، شرکتکنندگان تا ابد قفل نگه میدارند، که دسترسپذیری و مقیاسپذیری را میکشد و چرخهعمرِ سرویسها را جفت میکند. ساگا جداسازی را با دسترسپذیری مبادله میکند و انتخابِ عملگرایانه در مقیاس است.
شهود: «دقیقاً-یکبار تحویل» روی شبکه تقریباً افسانه است؛ اما «دقیقاً-یکبار اثر» ممکن است — پیام دو بار برسد، ولی اثرش فقط یکبار ثبت شود.
پاسخ: تحویلِ دقیقاً-یکبار نمیگیری — اثرِ پردازشِ دقیقاً-یکبار میگیری با ایدمپوتنتکردنِ مصرفکنندهها: حذفِ تکراری بر اساسِ شناسهی رویداد / کلیدِ ایدمپوتنسی، یا upsertِ ایدمپوتنت، یا تولیدکنندهی تراکنشیِ کافکا + read_committed برای پرشِ داخلیِ کافکا-به-کافکا. اثرِ دقیقاً-یکبار + تحویلِ حداقل-یکبار ترکیبِ دنیای واقعی است.
شهود: اینها مثلِ سیستمِ ایمنیِ یک ساختماناند: مهلت = زنگِ اخطار، قطعکننده = فیوزِ برق، دیواره = درهای ضدحریق که آتش را به یک بخش محدود میکنند، بازتلاش = تلاشِ دوبارهی محتاطانه، محدودکننده = تعدادِ نفراتِ مجاز در آسانسور.
پاسخ:
- مهلت (Timeout): هرگز تا ابد صبر نکن؛ هر فراخوانِ راهدور یکی نیاز دارد. پایه است — بدونِ آن، بقیه شکست میخورند.
- بازتلاش (Retry): خرابیهای گذرا را دوباره تلاش کن، با عقبنشینیِ نمایی + لرزش (exponential backoff + jitter) تا از طوفانِ بازتلاشِ همگام جلوگیری شود؛ فقط عملیاتِ ایدمپوتنت را بازتلاش کن.
- قطعکنندهی مدار: پس از آستانهی خرابی، مدار را باز کن و برای یک دورهی خنکسازی سریع شکست بده، سپس نیمهباز (half-open) تا بازیابی را بسنجی. جلوی کوبیدنِ یک وابستگیِ در حالِ مرگ و آبشاریشدن را میگیرد.
- دیواره (Bulkhead): منابع را جدا کن (استخرِ ریسه / استخرِ اتصالِ جدا بهازای هر وابستگی) تا یک وابستگیِ کند نتواند همهی ریسهها را تمام کند و کلِ سرویس را غرق کند.
- محدودکنندهی نرخ: نرخِ درخواست را سقف بگذار تا از منبعی محافظت شود.
[تله] تعامل: بازتلاشِ سادهلوحانه درونِ یک قطعکنندهی مدار بار را چند برابر میکند؛ و بازتلاش در هر لایه از یک زنجیرهی فراخوانی باعثِ تقویتِ بازتلاش (retry amplification) میشود (۳ لایه × ۳ بازتلاش = ۲۷ برابر). بازتلاش را سرتاسری بودجهبندی کن و ترجیحاً فقط در یک لایه بازتلاش کن.
شهود: مثلِ یک خطِ تولید که کارگرِ آخر به کارگرِ اول علامت میدهد «آهستهتر بگذار، دستم پر است»؛ بدونِ این علامت، قطعات روی زمین تلنبار میشوند تا خطِ تولید بخوابد.
پاسخ: فشار برگشتی یعنی مصرفکننده به تولیدکنندهی سریع علامت میدهد کند شود تا صفها بیکران رشد نکنند (← OOM، فروپاشیِ تأخیر). مکانیزمها: صفهای کراندار که در پُری مسدود/رد میکنند، تقاضای request(n) در Reactive Streams، کنترلِ جریانِ TCP، یا تأخیرِ مصرفکنندهی کافکا (consumer lag) بهعنوانِ بافرِ طبیعی. ضدالگو یک صفِ حافظهایِ بیکران است که بارِ اضافه را پنهان میکند تا فرایند بمیرد.
شهود: چون کافکا بهطورِ پیشفرض حداقل-یکبار است، «رسیدگی به تکراری» یک الزامِ طراحی است نه یک حالتِ مرزیِ نادر.
پاسخ: آفستها/شناسههای رویدادِ پردازششده را تراکنشی با اثرِ جانبی ردیابی کن: پیام را پردازش و شناسهاش را در همان تراکنشِ پایگاهداده ثبت کن، تا یک تحویلِمجدد پس از خرابی تشخیص و رد شود. جایگزین: خودِ نوشتن را ایدمپوتنت طراحی کن (upsert بر اساسِ کلیدِ تجاری).
شهود: سه لایهی متفاوت: «کجا پیدایش کنم؟»، «درِ ورودیِ کل سیستم کجاست؟»، و «قوانینِ رفتوآمدِ بینِ خانهها چیست؟».
پاسخ: کشفِ سرویس (Eureka، Consul، DNSِ کوبرنتیز): نمونههای سالم را پویا پیدا کن. دروازهی API: تنها ورودِ شمال-جنوب — احرازِ هویت، مسیریابی، محدودسازیِ نرخ، تجمیع. مش سرویس (Istio/Linkerd از طریقِ sidecar): نگرانیهای شرق-غرب — mTLS، بازتلاش، مهلت، جابهجاییِ ترافیک، رصدپذیری — که از کدِ اپ به پلتفرم منتقل شدهاند. مش تابآوریِ سطحزیرساخت است تا هر سرویس آن را یکنواخت و بدونِ کتابخانه بگیرد.
بخش ۵ — داده، سازگاری و مقیاسپذیری
شهود: سه سطحِ «تازگیِ» داده: یا همیشه آخرین حقیقت را ببین، یا بالاخره به آن برس، یا دستِکم علت را قبل از معلول ببین.
پاسخ: قوی (خطیپذیر): هر خواندن آخرین نوشته را میبیند؛ برای پول، کاهشِ موجودی، و یکتایی لازم است. نهایی (eventual): نسخهها در نبودِ نوشتهی جدید همگرا میشوند؛ برای فیدها، شمارشها، کشها و «لایکها» خوب است. علّی (causal): ترتیبِ علت→معلول را حفظ میکند (پاسخ را فقط پس از پیامی که به آن پاسخ میدهد میبینی) — نقطهی شیرین برای اپهای همکاری و اجتماعی، و ارزانتر از قوی.
شهود: تکثیر یعنی چند فتوکپیِ یکسان از یک دفتر (برای خواندنِ بیشتر و پشتیبان)؛ شاردینگ یعنی پارهکردنِ دفتر به چند دفترِ کوچکتر که هرکدام بخشِ متفاوتی از داده را دارند (برای نوشتن و ذخیرهی بیشتر).
پاسخ: تکثیر همان داده را کپی میکند (مقیاسِ خواندن + دسترسپذیریِ بالا)؛ شاردینگ دادههای متفاوت را روی نودها پارتیشن میکند (مقیاسِ نوشتن + ذخیرهسازی). بخشهای سختِ شاردینگ: انتخابِ کلیدِ شارد که بار را بدونِ نقطهی داغ (hotspot) پخش کند، پرسوجو/الحاقِ بینشاردی (پرهیز یا scatter-gather)، تراکنشهای بینشاردی (پرهیز — به ساگا/2PC برمیگردی)، و بازشاردینگ (resharding) با رشد (هشِ سازگار/consistent hashing جابهجاییِ داده را کمینه میکند).
شهود: تصور کن یک کلیدِ داغ (مثلِ صفحهی یک سلبریتی) دقیقاً در یک لحظه منقضی شود؛ در همان لحظه هزاران درخواست همزمان میبینند که کش خالی است و همه با هم به پایگاهداده هجوم میبرند — مثلِ بازشدنِ ناگهانیِ درِ استادیوم.
پاسخ: حتی با write-through، یک هجوم به کش رخ میدهد وقتی یک کلیدِ داغ منقضی میشود و هزاران درخواست همزمان miss میکنند و همه به پایگاهداده میزنند. کاهشها: تجمیعِ درخواست / single-flight (فقط یک بارگذار بهازای هر کلید)، انقضای زودهنگامِ احتمالاتی، stale-while-revalidate، یا TTLهای لرزشدار (jittered). جدا از این، نوشتنِ دوگانه میانِ کش و پایگاهداده زیرِ خرابی ناسازگار است — cache-aside با ابطال را ترجیح بده و پنجرهی کوچکِ کهنگی را بپذیر، یا از CDC برای ابطال استفاده کن. کشِ کاملاً سازگار بدونِ قربانیکردنِ دسترسپذیری یا تأخیر وجود ندارد.
بخش ۶ — پرسشهای باز طراحی سیستم
اینها را ۴۵ دقیقه پای تختهسفید بدان. معیار همیشه یکی است: شفافسازیِ نیازها ← تخمینِ مقیاس ← تعریفِ API ← مدلِ داده ← مؤلفههای سطحبالا ← ژرفکاویِ یک بخشِ سخت ← رسیدگی به گلوگاه/خرابی. فرضها را با صدای بلند بگو؛ مصاحبهگر استدلالِ تو را نمره میدهد، نه یک نمودارِ «درست».
شهود: این سیستم بهشدت خواندن-سنگین است (تقریباً ۱۰۰ خواندن به ازای هر نوشتن)، پس همهی هنر در سریعکردنِ مسیرِ خواندن است.
پاسخ: شفافسازی: خواندن-سنگین (۱۰۰:۱)، نامِ مستعارِ سفارشی؟، تحلیل؟، TTL؟ تولیدِ کلید: base62 از یک شمارندهی سراسری (نیازمندِ یک شناسهی توزیعشده — Snowflake یا یک سرورِ بلیتِ بازهای)، نه هش (تصادم، طول). API: POST /urls → کدِ کوتاه، GET /{code} → تغییرمسیرِ 301/302. ذخیرهسازی: انبارِ کلید-مقدار (code → longUrl)، بهشدت کششده (مسیرِ خواندن یک اصابتِ کش + تغییرمسیر است). مقیاس: CDN + Redis جلوی پایگاهداده؛ مسیرِ نوشتن کوچک است. دربارهی 301 (کششده، تحلیل را از دست میدهد) در برابر 302 (هر اصابت به تو میرسد) بحث کن.
شهود: «سطلِ ژتون» را تصور کن که با نرخِ ثابتی ژتون تویش میریزد؛ هر درخواست یک ژتون خرج میکند و اگر سطل خالی باشد رد میشود — این هم انفجارِ کوتاه را اجازه میدهد هم میانگین را کنترل میکند.
پاسخ: الگوریتمها: سطلِ ژتون (token bucket) (اجازهی انفجار تا اندازهی سطل، پرکردن با نرخ) — انتخابِ معمول؛ گزارشِ پنجرهی لغزان (sliding window log) (دقیق، حافظهبر)؛ شمارندهی پنجرهی لغزان (تقریب، ارزان). توزیعشده: شمارندهها را در Redis با اسکریپتهای اتمیِ Lua ذخیره کن تا مسابقه نشود؛ کلید بر اساسِ user:route. دربارهی جای اجرا (دروازه/مش)، fail-open در برابر fail-closed روی قطعیِ Redis، و انحرافِ ساعت (clock skew) بحث کن.
شهود: دو سرِ طیف: یا موقعِ نوشتن فیدِ همه را از پیش بچین (خواندن سریع، اما برای سلبریتیِ چندمیلیونی فاجعه)، یا موقعِ خواندن فید را سرِ هم کن (نوشتن ارزان، خواندن کند).
پاسخ: مبادلهی اصلی فناوت روی نوشتن (push) در برابر فناوت روی خواندن (pull) است. push فیدِ هر دنبالکننده را هنگامِ ارسال پیشمحاسبه میکند (خواندنِ سریع، برای سلبریتیها با میلیونها دنبالکننده گران — مسئلهی «کلیدِ داغ / هجومِ رعدآسا»). pull فید را در زمانِ خواندن مونتاژ میکند (نوشتنِ ارزان، خواندنِ کند). سیستمهای واقعی از ترکیبی (hybrid) استفاده میکنند: push برای کاربرانِ معمولی، pull برای حسابهای سلبریتی، ادغامشده هنگامِ خواندن. یک صفِ پیام برای فناوتِ ناهمگام و تحویلِ ایدمپوتنت اضافه کن.
شهود: میخواهی هر ماشین بدونِ پرسیدن از هیچکس شناسهی یکتا بسازد؛ راهش این است که هر شناسه از تکههایی ساخته شود که با هم هرگز تکراری نمیشوند: زمان + کیستیِ ماشین + یک شمارندهی کوچک.
پاسخ: نیازها: یکتا، تقریباً مرتب بر زمان، توانعملیاتیِ بالا، بدونِ هماهنگی در مسیرِ داغ. Snowflake: ۶۴ بیت = زمانمهر | شناسهی ماشین | دنباله بهازای میلیثانیه. میلیونها/ثانیه، k-مرتب، بدونِ گلوگاهِ مرکزی. تلهها: انحرافِ ساعت / NTP که به عقب میرود (باید رد یا صبر کند)، تخصیصِ شناسهی ماشین، و بودجهی بیت/دورهی حدودِ ۲۰۳۸. جایگزینها: UUIDv7 (مرتب بر زمان، بدونِ هماهنگی) یا سرورهای بلیتِ پایگاهداده.
بخش ۷ — پرسشهای رفتاری و تصمیم معماری
اینها را در قالبِ STAR پاسخ بده (موقعیت، وظیفه، اقدام، نتیجه). موقعیت/وظیفه را دو جمله نگه دار؛ وقتت را روی اقدام بگذار (اینکه تو چه تصمیمی گرفتی و چرا) و نتیجه را کمّی کن.
نکته STAR: موقعیت — قید (مهلت، هدفِ مقیاس، بارِ ناشناخته). وظیفه — تصمیمی که مالکش بودی. اقدام — گزینههایی که سنجیدی، ماتریسِ مبادله (مثلاً «سازگاریِ نهایی از طریقِ outbox را بر 2PC انتخاب کردیم چون برای سفارشها دسترسپذیری از جداسازی مهمتر بود») و اینکه چطور ریسکزدایی کردی (spike، تستِ بار، feature flag). نتیجه — یک متریک (تأخیرِ p99، هزینه، کاهشِ حادثه) و آنچه بازبینی میکردی. ارشدها بر نحوهی استدلال دربارهی مبادلهها نمره میگیرند، نه فناوریِ انتخابشده.
نکته STAR: موقعیت — اثر و شعاعِ انفجار (blast radius). وظیفه — نقشت (مجری/IC یا هماهنگکننده). اقدام — چطور اول خونریزی را بند آوردی (بازگردانی/feature-flag/مقیاس)، سپس علتِ ریشهای را تشخیص دادی؛ بر ارتباطات و پسمرگِ بدونِسرزنش (blameless post-mortem) تأکید کن. نتیجه — MTTR، رفعِ پایدار (افزودنِ هشدار، فشارِ برگشتی، ایدمپوتنسی)، و تغییرِ سیستمی تا دیگر تکرار نشود. هرگز فرد یا فروشنده را سرزنش نکن.
نکته STAR: مخالفت-کن-و-متعهد-شو (disagree-and-commit) را نشان بده. اقدام — با داده/نمونهیاولیه استدلالت را گفتی، به قیدهایشان گوش دادی، و یا نظرت را عوض کردی (صداقتِ فکری) یا به تصمیمشان متعهد شدی و موفقش کردی. نتیجه — پیامد بهعلاوهی رابطهی حفظشده. آنها بلوغ را آزمون میکنند، نه اینکه «بردی».
پاسخ (چارچوبِ تصمیم): پیشفرض یک یکپارچهی ماژولار (modular monolith) است، مگر یک محرکِ مشخص داشته باشی: مقیاسِ مستقلِ مؤلفهها، آهنگِ استقرارِ مستقل بینِ تیمها، مرزهای قویِ تیم/مالکیت (قانونِ کانوی/Conway)، یا نیازِ چندزبانه (polyglot). میکروسرویس مقیاسپذیریِ سازمانی میخرد و با پیچیدگیِ عملیاتی میپردازد (تراکنشِ توزیعشده، رصدپذیری، خرابیِ شبکه، تکرارِ داده). میکروسرویسِ زودهنگام در تیمِ کوچک گرانترین اشتباه است؛ سرویسها را در امتدادِ زمینههای محدود و پس از اثباتِ درزها استخراج کن. نکته STAR اگر داستان خواستند: اندازهی تیم و دردِ استقراری که تقسیم را توجیه کرد (یا نکرد) را کمّی کن.
پاسخ: تدریجی و برگشتپذیر. انجیرِ خفهکننده (strangler fig) برای مهاجرتِ مسیر-به-مسیر از سیستمِ قدیمی؛ گسترش-انقباض (expand-contract / parallel change) برای طرحواره (schema) و API — فیلد/نقطهی جدید را اضافه کن، نوشتن-دوگانه/خواندن-دوگانه، خوانندهها را مهاجرت بده، سپس قدیمی را حذف کن — هرگز جابهجاییِ یکجا (big-bang). همهچیز را پشتِ feature flag محافظت کن، پشتِ یک canary بفرست، و در هر گام مسیرِ بازگردانی نگه دار. در هر دروازه اندازهگیری کن.
دور رعدآسای سریع (پاسخ را در یک نفس بگو)
اینها را باید بیمکث بگویی؛ هرکدام یک تکجملهی حاضروآماده است.
- تفاوتِ همزمانی (concurrency) و موازیسازی (parallelism)؟ همزمانی = مدیریتِ چند چیز همزمان (ساختار)؛ موازیسازی = انجامِ چند چیز همزمان (اجرا).
- قفلِ خوشبینانه در برابر بدبینانه؟ خوشبینانه (optimistic) فرضِ بیتعارض میکند، هنگامِ کامیت یک نسخه را چک میکند (
WHERE version = ?)، در تصادم بازتلاش میکند — عالی برای رقابتِ کم. بدبینانه (pessimistic) از ابتدا قفل میکند (SELECT ... FOR UPDATE) — برای رقابتِ بالا/بخشِ بحرانی. - ارزانترین پیشگیریِ بنبست؟ ترتیبِ سراسریِ قفل.
- حداقل-یکبار در برابر حداکثر-یکبار در برابر دقیقاً-یکبار؟ معناشناسیِ تحویل؛ تحویلِ دقیقاً-یکبار روی شبکه عمدتاً افسانه است — به حداقل-یکبار + ایدمپوتنسی هدف بگیر.
- چه چیزی سریعترین LSP را میشکند؟ زیرکلاسی که روی یک متدِ ارثی استثنا پرتاب میکند.
- چرا
HashMapزیرِ همزمانی ناامن است؟ بهروزرسانیهای گمشده و تاریخاً حلقهی بینهایت هنگامِ تغییرِ اندازه؛ ازConcurrentHashMapاستفاده کن. - CAP در طولِ پارتیشن، یکی را بردار: سازگاری یا دسترسپذیری — هرگز هردو.
- چرا یک POSTِ غیرایدمپوتنت را بازتلاش نکنیم؟ ممکن است دوبار شارژ کنی؛ اول یک کلیدِ ایدمپوتنسی اضافه کن.
- بهترین رفعِ هجوم به کش؟ بارگذاریِ single-flight + TTLِ لرزشدار.
- کِی ریسههای مجازی بیفایدهاند؟ کارِ مقید به پردازنده (CPU-bound).
- مدلِ حافظهی جاوا دربارهی رؤیتپذیری است نه زمانبندی: بدونِ یالِ happens-before هیچ تضمینی نیست؛
volatileرؤیتپذیری/ترتیبِ یک متغیر میدهد،synchronizedانحصار + رؤیتپذیریِ چند فیلد. - بنبست را با ترتیبِ سراسریِ قفل میشکنی؛ ریسههای مجازی فقط کارِ مقید به I/O را مقیاس میدهند، و از جاوا ۲۴ (JEP 491) دیگر
synchronizedتقریباً پین نمیکند. - SOLID را با «بوی» هر اصل به یاد بیاور؛ خانوادهی Adapter/Facade/Proxy/Decorator را با نیتشان تفکیک کن (تبدیل/سادهسازی/کنترل/افزودن).
- DDD: Value Object مقداری، Entity هویتدار، Aggregate مرزِ تراکنش؛ یک تراکنش = یک تجمیع، بقیه نهایی از طریقِ رویداد.
- در سیستمهای توزیعشده ستونِ فقرات ایدمپوتنسی است: بازتلاش امن، outbox برای مسئلهی نوشتنِ دوگانه، ساگا بهجای 2PC، و اثرِ دقیقاً-یکبار روی تحویلِ حداقل-یکبار.
- CAP یعنی زیرِ پارتیشن بینِ C و A انتخاب کن؛ PACELC میگوید بیپارتیشن هم بینِ تأخیر و سازگاری مبادله داری.
- در طراحی سیستم همیشه از قالبِ نیاز→مقیاس→API→داده→مؤلفه→ژرفکاوی→خرابی برو، و در رفتاری با STAR بگو و روی اقدام و نتیجهی کمّی تمرکز کن.
Plenty of strong engineers freeze on rapid-fire interview questions — not because they don't know the material, but because they've never been forced to get the thought out of their head cleanly and confidently in 60 seconds. This chapter is exactly that drill: every entry is a question you'll actually be asked, and we teach each answer by feeling it first through a concrete analogy, then landing on the precise, senior-level answer.
How to use it: read the answer, then close your eyes and reconstruct it out loud. If you can't say it in one breath, it isn't yours yet.
Seven stations ahead:
- Concurrency & the Java Memory Model (JMM) — happens-before, volatile, deadlock, virtual threads, Loom.
- Design patterns & SOLID — the five principles, LSP, and the "wrapper" pattern family.
- DDD — Entity, Value Object, Aggregate, bounded context, anti-corruption layer.
- Microservices, CAP & distributed systems — idempotency, outbox, saga, resilience.
- Data, consistency & scaling — strong/eventual/causal consistency, sharding, caching.
- Open-ended system design — URL shortener, rate limiter, feed, ID generator.
- Behavioral & architecture decisions — answering in STAR. Trap questions are marked [GOTCHA] and behavioral prompts carry a STAR tip.
Part 0 — Words you must feel
Before we start, three words recur through the whole chapter. If you feel these, everything else gets easier.
Imagine hiring a bank guard. You ask three things:
- What invariant does he protect? "No account balance ever goes negative." An invariant is a truth that must always hold.
- What failure mode does he introduce? Maybe in a crowd the queue at the door gets long — a new way the system can break.
- What is his cost? His salary, and the delay he adds.
Interviewers don't want a memorized definition; they want to see that for any mechanism you can name these three: the invariant it protects, the failure mode it introduces, the cost you pay. Every answer in this chapter is built around this triad.
Linearizability means the system behaves as if every operation happened at one single instant and everyone sees it immediately — like a single cashier with everyone queued behind them. Idempotent means an operation that, applied N times, has the same effect as applying it once — like pressing the floor button in an elevator: the second press changes nothing.
Part 1 — Concurrency & the Java Memory Model
Picture two employees (threads) working on a shared whiteboard (main memory), but each also keeps a personal notepad (CPU cache / registers). For speed, each employee only copies things from their notepad onto the whiteboard when forced to. The crucial question: when and which of the first employee's writes does the second one see on the whiteboard? The JMM is precisely that "who sees what" rule — not a "who runs when" rule.
Intuition: The JMM doesn't build a full timeline; it just draws reliable edges that say "whatever was written on this side of the edge is definitely visible on the other side."
Answer: The JMM defines a partial order called happens-before over memory operations. If action A happens-before B, then A's writes are visible to B and ordered before it. Without a happens-before edge, one thread's writes may be reordered, cached, or invisible to another thread — indefinitely. So the JMM is not about "when things run"; it is about which writes a read is allowed to observe.
Key edges to memorize:
- program order within a thread;
unlock→lockon the same monitor;volatilewrite→subsequentvolatileread of the same field;Thread.start()→the thread's first action;- a thread's last action→another thread's
join(); - and
finalfield freeze at the end of the constructor.
Intuition: volatile is a loudspeaker on one variable: whatever you say, everyone hears immediately — but it can't stop two people from talking at once. synchronized is a room with one key: only one person enters, does the work across several fields, and publishes everything on the way out.
Answer: volatile gives visibility and ordering for a single variable but no atomicity for compound actions — x++ is really read-modify-write and stays racy. synchronized gives mutual exclusion + visibility for everything the lock guards, and establishes happens-before via unlock/lock.
Choice rule: use volatile for a flag or a safely-published reference; use synchronized/locks when you need to hold an invariant across multiple fields together.
Code:
class Holder {
static boolean ready = false; // NOT volatile
static int value = 0;
static void writer() { value = 42; ready = true; }
static void reader() { while (!ready) {} System.out.println(value); }
}
// Thread A runs reader(), Thread B runs writer()
Intuition: Without a happens-before edge, the reader thread is allowed to read ready "once and for all" into its notepad and never look at the whiteboard again.
Answer: It may print 42, may print 0, or may never terminate. Because ready is not volatile, there is no happens-before edge between the writer and reader. The JIT is legally allowed to hoist !ready out of the loop (read once into a register) → infinite loop. Even if it does exit, value may still be seen as 0, because the write to value can be reordered relative to ready and there is no visibility guarantee.
Making ready volatile fixes both: the volatile write publishes value too (piggyback ordering), and the volatile read is re-fetched each iteration.
Intuition: Constructing an object is two steps: fill the memory (constructor), then hand its address to the variable. If those two steps are seen out of order, another thread sees a non-null address backed by a half-built object.
Answer: The object reference could be published before the constructor finished, so another thread saw a non-null but partially-initialized object. The fix is volatile on the field: the volatile write of the reference cannot be reordered before the constructor's writes, and the volatile read establishes happens-before.
class Lazy {
private static volatile Lazy instance; // volatile is mandatory
static Lazy get() {
Lazy r = instance; // read volatile once
if (r == null) {
synchronized (Lazy.class) {
r = instance;
if (r == null) instance = r = new Lazy();
}
}
return r;
}
}
Senior note: prefer the initialization-on-demand holder idiom — a static nested class — where the classloader gives you lazy, thread-safe init for free with no volatile at all.
Intuition: Deadlock is like an intersection where four cars have all entered at once and each is waiting for the one on its right to move first. Remove just one of the four conditions and the knot unties.
Answer: The four conditions are: mutual exclusion, hold-and-wait, no-preemption, and circular wait. In practice you break circular wait by imposing a global lock ordering — always acquire locks in a canonical order (e.g. by System.identityHashCode or a business key). Alternatively break hold-and-wait with tryLock + backoff, or no-preemption with timeouts.
Broken code:
void transfer(Account from, Account to, long amount) {
synchronized (from) {
synchronized (to) { from.debit(amount); to.credit(amount); }
}
}
Intuition: If one thread calls transfer(A,B) and another concurrently calls transfer(B,A), each grabs one lock and waits forever for the other — pure circular wait.
Answer: Impose a global order by sorting locks on a stable id, so both calls always take the same order:
void transfer(Account from, Account to, long amount) {
Account first = from.id() < to.id() ? from : to;
Account second = from.id() < to.id() ? to : from;
synchronized (first) {
synchronized (second) { from.debit(amount); to.credit(amount); }
}
}
If ids can be equal (same account), guard against self-transfer or use a tie-breaker lock.
Intuition: Deadlock is everyone standing still and dead. Livelock is two people in a corridor both stepping aside the same way, over and over — active, but nobody gets through. Starvation is one person stuck at the door because others always jump in first.
Answer: Deadlock: threads blocked forever, no progress, no CPU burn. Livelock: threads are active and keep changing state in response to each other but make no progress. Starvation: a thread makes no progress because others monopolize a resource (unfair locks, priority inversion). Fairness policies and randomized backoff address livelock and starvation.
Picture a restaurant with only 8 chefs (CPU cores / carrier threads). Old style: one chef per customer — with 10,000 customers you'd need 10,000 chefs, impossible. Loom style: a chef only stands over an order while actually cooking; the moment the order just needs to sit in the oven (I/O), the chef steps away and picks up the next order. Now with the same 8 chefs you can juggle thousands of orders at once — as long as the work is mostly "waiting on the oven," not "chopping non-stop."
Intuition: See above — the chefs are carrier threads, the customers are virtual threads.
Answer: Virtual threads (stable since Java 21, JEP 444) are JDK-managed lightweight threads multiplexed M:N onto a small pool of OS carrier threads. When a virtual thread blocks on I/O, it unmounts from its carrier, freeing the OS thread to run another virtual thread. This makes the simple thread-per-request blocking style scale to millions of concurrent tasks.
They help when work is I/O-bound and blocking (DB calls, HTTP fan-out). They do not speed up CPU-bound work — you still have only N cores; a fixed-size platform-thread pool is right there. And never pool virtual threads; create one per task.
Intuition: Pinning is when the chef won't step away; even when the order only needs to sit in the oven, he stays glued to it and the whole queue stalls.
Answer: Pinning is when a virtual thread cannot unmount and holds its carrier hostage during a blocking operation. In Java 21–23, entering a synchronized block/method pinned the virtual thread (the monitor tracked the carrier, not the virtual thread), so long I/O inside synchronized starved the carrier pool. The Java-21-era advice was: replace hot synchronized guarding I/O with ReentrantLock.
Java 24 (JEP 491) rewrote the monitor implementation to be virtual-thread-aware, so synchronized no longer pins in virtually all cases, and -Djdk.tracePinnedThreads was removed. Remaining pinning sources: executing native code (JNI) or foreign functions. Senior takeaway: know your target JDK — the "avoid synchronized" rule is a Java-21/22/23 rule, not a permanent one.
Intuition: ThreadLocal is like sticking a sticky-note on the chef himself, not on the order. If chefs get recycled (a pool), the previous order's note lingers on the next order; and with millions of threads, millions of notes devour memory.
Answer: ThreadLocal leaks when used with pooled threads (the value survives across unrelated tasks) and adds per-thread memory that explodes with millions of virtual threads. Loom introduces scoped values (ScopedValue, finalized around Java 25) — immutable, bounded to a dynamic scope, and cheap to inherit into structured-concurrency child tasks. Prefer them for request-scoped context (tenant id, principal).
Intuition: CompletableFuture is like dispatching several couriers with no supervisor; if one gets lost, the others carry on oblivious. Structured concurrency adds a supervisor who, if one courier fails, recalls the rest and gives you one clear report.
Answer: CompletableFuture composes async callbacks but loses the parent-child relationship: a failure in one branch doesn't automatically cancel siblings, and errors surface as tangled exceptions. Structured concurrency (StructuredTaskScope, preview through Java 21–24) treats a group of subtasks as a unit: if one fails, the scope cancels the rest and propagates one clear error; the scope cannot leak threads because it joins before returning. It restores the call-stack discipline that async broke.
Part 2 — Design Patterns & SOLID
Intuition: Each principle neutralizes a "bad smell" in code; know the smell and you recall the principle without memorizing.
Answer:
- SRP — one reason to change; smell: a class edited by three different teams.
- OCP — open for extension, closed for modification; smell: adding a case means editing a giant
switch. - LSP — subtypes must be substitutable; smell:
if (obj instanceof X)before calling a method, or a subclass throwingUnsupportedOperationException. - ISP — many small interfaces beat one fat one; smell: implementers stubbing methods they don't need.
- DIP — depend on abstractions, not concretions; smell:
newof a concrete service inside business logic.
Intuition: In math a square is a rectangle, but in code this inheritance is a trap because their behavioral contracts differ.
Answer: The classic: Square extends Rectangle. Rectangle has independent setWidth/setHeight; a Square must keep them equal, so overriding setWidth to also set height breaks any client that assumes setWidth(5); setHeight(4) yields area 20. The subtype strengthens a precondition / weakens a postcondition — the contract, not the code, is violated. Fix: model them as separate types or make shapes immutable.
Intuition: All three "vary behavior," but with three different levers: injection, inheritance, and internal state change.
Answer: Strategy injects an interchangeable algorithm via composition — runtime swap. Template Method fixes the skeleton in a base class and lets subclasses fill steps via inheritance — compile-time. State is Strategy where the object swaps its own behavior as internal state changes and the states know how to transition. Generally prefer Strategy over Template Method — composition over inheritance.
Intuition: Singleton is fine when "only one instance" is a genuine domain fact; it's bad when it becomes a hidden global variable everyone reaches for.
Answer: When it becomes a global mutable variable: it hides dependencies, defeats testability (you can't substitute it), creates hidden coupling, and is a concurrency hazard on lazy init. In a DI world you want a single instance managed by the container (scope = singleton) injected explicitly — same lifetime, none of the global-state pathology.
Intuition: All four wrap something, but with different intent: convert, simplify, control, augment.
Answer:
- Adapter: converts one interface to another the client expects (integration).
- Facade: a simplified entry point over a complex subsystem (reduces surface).
- Proxy: same interface, controls access (lazy load, security, remote, caching); Spring AOP uses dynamic proxies.
- Decorator: same interface, adds behavior by stacking wrappers (e.g.
BufferedInputStream).
Key discriminator: Adapter changes the interface, Facade simplifies it, Proxy and Decorator keep it — but Proxy controls, Decorator augments.
Part 3 — DDD & Domain Modeling
Take a 10-dollar bill: you don't care which physical bill it is, only its value — that's a Value Object (compared by value, interchangeable). Now take your bank account: even if its balance drops to zero, it's still the same account with the same number and a history — that's an Entity (has identity and a lifecycle).
Intuition: See above; and think of an Aggregate as a "case file" you may only open through its cover.
Answer: An Entity has identity and a lifecycle (equality by id). A Value Object is immutable and compared by value (Money, Address), with no identity. An Aggregate is a consistency boundary: a cluster of entities/VOs with one aggregate root that is the only external entry point; invariants inside the aggregate are enforced transactionally. Rule of thumb: one transaction should modify one aggregate; cross-aggregate consistency is eventual — via domain events.
Intuition: The word "Customer" in Sales, in Billing, and in Support is not the same thing; if you cram them all into one definition, you build a jack-of-all-trades mush that satisfies nobody.
Answer: A bounded context is a boundary within which a model and its ubiquitous language are consistent. Forcing one shared model across all units creates a coupled mess. Bounded contexts map naturally onto microservice boundaries and team ownership. The relationships between contexts (partnership, customer-supplier, conformist, anti-corruption layer) are your integration contract.
Intuition: Like an embassy translator: whatever comes from the foreign side is translated into your own language so their vocabulary and concepts never enter your house directly.
Answer: A translation layer that isolates your model from an external/legacy model, so their concepts and vocabulary don't leak into your domain. It converts at the boundary (their DTO → your value objects), protecting your ubiquitous language and letting the upstream system change without rippling inward.
Part 4 — Microservices, CAP & Distributed Systems
Imagine two salespeople in two cities sharing one inventory ledger. Now the phone line between them drops (a partition). Two choices: either both stop selling so the numbers never disagree (consistency, but you sell nothing), or both keep selling and reconcile later (availability, but for a moment they might both sell the same item). While the line is down, you cannot have both.
Intuition: See above; "partition" is that dropped line.
Answer: Under a network partition (P), a distributed system must choose between consistency (C, linearizability) and availability (A, every request gets a non-error response). The common misreading: "pick 2 of 3." Partitions are not optional — networks fail — so you are really choosing CP or AP during a partition. When there is no partition, you can have both C and A. PACELC extends this: Else (no partition), you trade Latency vs Consistency. That is the more useful lens for daily design.
Answer: CP: a system built on consensus (etcd, ZooKeeper, or a single-leader RDBMS with synchronous replication) — it refuses writes on the minority side of a partition to preserve consistency. AP: Cassandra/DynamoDB with quorum tuned for availability, or DNS — they keep serving and reconcile later (last-write-wins, vector clocks, CRDTs). Note you can tune per-operation (Cassandra consistency levels).
Intuition: On a network you can never tell "my request was lost" from "its response was lost" — so you must resend. If the operation isn't idempotent, that "resend" means charging twice.
Answer: An operation is idempotent if applying it N times has the same effect as applying it once. Because on a network you cannot tell "request lost" from "response lost," clients retry, and retries cause duplicates. Idempotency is what makes retries safe. Implement with an idempotency key: the client sends a unique key; the server records (key → result) and on a repeat returns the stored result instead of re-executing.
-- Idempotent payment insert: the unique key makes the duplicate a no-op
INSERT INTO payments (idempotency_key, order_id, amount, status)
VALUES ('c1f...-uuid', 42, 1999, 'CAPTURED')
ON CONFLICT (idempotency_key) DO NOTHING; -- Postgres: second attempt inserts 0 rows
Intuition: Two separate actions in two separate systems with no shared transaction is like saying "I do" at the registry and then driving to the bank to sign — anything can happen in between and leave you half-done.
Answer: Because they are two separate systems with no shared transaction — a dual-write problem. If you commit the DB then the broker is down, the event is lost; if you publish then the DB rolls back, you emitted a phantom event. There is no atomicity across the two. The fix is the Transactional Outbox: within the same DB transaction that changes state, insert the event into an outbox table. A separate relay (polling or CDC via Debezium reading the WAL) publishes committed outbox rows to Kafka. Now the state change and the "intent to publish" are atomic; delivery becomes at-least-once, so consumers must be idempotent.
Intuition: Choreography is like a group dance with no director — each dancer does the next move on seeing their neighbor's; beautiful, but hard to follow the whole flow. Orchestration has a director telling each person when to do what.
Answer: A saga manages a distributed transaction as a sequence of local transactions, each with a compensating action to undo it (there is no distributed rollback). Choreography: services react to each other's events, no central coordinator — decoupled but hard to follow and reason about (emergent flow). Orchestration: a central saga orchestrator tells each service what to do and drives compensations — explicit, observable, easier to change, at the cost of a coordinator. Sagas give you eventual consistency and atomicity via compensation, never isolation — you can observe intermediate states, so design for semantic locks or pending states.
Intuition: 2PC is like several deal parties all holding their pens, waiting for one person's signal to sign at the same instant; if that person vanishes, everyone freezes with pen raised.
Answer: 2PC (two-phase commit) gives true ACID atomicity across services via a coordinator (prepare, then commit/abort). It is blocking: if the coordinator dies after prepare, participants hold locks indefinitely, killing availability and scalability, and it couples service lifecycles. Sagas trade isolation for availability and are the pragmatic choice at scale.
Intuition: "Exactly-once delivery" over a network is largely a myth; but "exactly-once effect" is achievable — the message may arrive twice, yet its effect is recorded only once.
Answer: You don't get exactly-once delivery — you get exactly-once processing effect by making consumers idempotent: dedupe on the event id / idempotency key, or use idempotent upserts, or Kafka's transactional producer + read_committed for the internal Kafka-to-Kafka hop. Effect-level exactly-once + at-least-once delivery is the real-world combination.
Intuition: These are like a building's safety system: timeout = the alarm, circuit breaker = the electrical fuse, bulkhead = fire doors confining the fire to one section, retry = a cautious second attempt, rate limiter = the elevator's max occupancy.
Answer:
- Timeout: never wait forever; every remote call needs one. It is the foundation — without it, everything else fails.
- Retry: re-attempt transient failures, with exponential backoff + jitter to avoid synchronized retry storms; only retry idempotent ops.
- Circuit breaker: after a failure threshold, open the circuit and fail fast for a cooldown, then half-open to probe recovery. Stops you from hammering a dying dependency and cascading.
- Bulkhead: isolate resources (separate thread pools / connection pools per dependency) so one slow dependency can't exhaust all threads and sink the whole service.
- Rate limiter: cap request rate to protect a resource.
[GOTCHA] interaction: naive retry inside a circuit breaker multiplies load; and retries at every layer of a call chain cause retry amplification (3 layers × 3 retries = 27×). Budget retries end-to-end and prefer retrying at one layer.
Intuition: Like an assembly line where the last worker signals the first "slow down, my hands are full"; without that signal, parts pile up on the floor until the line collapses.
Answer: Backpressure is a consumer signaling a fast producer to slow down so queues don't grow unbounded (→ OOM, latency collapse). Mechanisms: bounded queues that block/reject when full, Reactive Streams' request(n) demand, TCP flow control, or Kafka consumer lag as a natural buffer. The anti-pattern is an unbounded in-memory queue that hides the overload until the process dies.
Intuition: Because Kafka is at-least-once by default, "handle duplicates" is a design requirement, not a rare edge case.
Answer: Track processed offsets/event-ids transactionally with the side effect: process the message and record its id in the same DB transaction, so a redelivery after a crash is detected and skipped. Alternatively design the write itself to be idempotent (upsert by business key).
Intuition: Three different layers: "where do I find it?", "where's the front door to the whole system?", and "what are the rules for traffic between the houses?"
Answer: Service discovery (Eureka, Consul, K8s DNS): find healthy instances dynamically. API gateway: single north-south entry — auth, routing, rate limiting, aggregation. Service mesh (Istio/Linkerd via sidecars): east-west concerns — mTLS, retries, timeouts, traffic shifting, observability — moved out of app code into the platform. Mesh is infrastructure-level resilience so every service gets it uniformly without a library.
Part 5 — Data, Consistency & Scaling
Intuition: Three levels of data "freshness": always see the latest truth, eventually get there, or at least see the cause before the effect.
Answer: Strong (linearizable): every read sees the latest write; needed for money, inventory decrements, uniqueness. Eventual: replicas converge given no new writes; fine for feeds, counts, caches, "likes." Causal: preserves cause→effect ordering (you see a reply only after the message it answers) — a sweet spot for collaborative and social apps, cheaper than strong.
Intuition: Replication is several identical photocopies of one ledger (for more reads and backup); sharding is tearing the ledger into smaller ledgers, each holding different data (for more writes and storage).
Answer: Replication copies the same data (read scaling + HA); sharding partitions different data across nodes (write + storage scaling). The hard parts of sharding: choosing a shard key that spreads load without hotspots, cross-shard queries/joins (avoid or scatter-gather), cross-shard transactions (avoid — you're back to sagas/2PC), and resharding as you grow (consistent hashing minimizes data movement).
Intuition: Imagine a hot key (say a celebrity's page) expiring at one exact instant; in that same instant thousands of requests all see the cache is empty and stampede the DB together — like a stadium gate suddenly opening.
Answer: Even with write-through, a cache stampede happens when a hot key expires and thousands of requests miss simultaneously and all hit the DB. Mitigations: request coalescing / single-flight (only one loader per key), probabilistic early expiration, stale-while-revalidate, or jittered TTLs. Separately, the dual-write between cache and DB is inconsistent under failure — prefer cache-aside with invalidation and accept a small staleness window, or use CDC to invalidate. There is no perfectly consistent cache without giving up availability or latency.
Part 6 — Open-ended System Design Prompts
Treat these as 45-minute whiteboard prompts. The rubric is always the same: clarify requirements → estimate scale → define API → data model → high-level components → deep-dive one hard part → address bottlenecks/failure. State assumptions out loud; interviewers score your reasoning, not a "correct" diagram.
Intuition: This system is heavily read-heavy (roughly 100 reads per write), so all the craft is in making the read path fast.
Answer: Clarify: read-heavy (100:1), custom aliases?, analytics?, TTL? Key generation: base62 of a global counter (needs a distributed id — Snowflake or a ranged ticket server), not a hash (collisions, length). API: POST /urls → short code, GET /{code} → 301/302 redirect. Storage: KV store (code → longUrl), heavily cached (the read path is a cache hit + redirect). Scale: CDN + Redis in front of the DB; the write path is tiny. Discuss 301 (cached, loses analytics) vs 302 (every hit reaches you).
Intuition: Picture a "token bucket" filled at a steady rate; each request spends one token, and if the bucket is empty it's rejected — this allows short bursts yet controls the average.
Answer: Algorithms: token bucket (allows bursts up to bucket size, refills at rate) — the usual choice; sliding window log (accurate, memory heavy); sliding window counter (approximation, cheap). Distributed: store counters in Redis with atomic Lua scripts to avoid races; key by user:route. Discuss where it runs (gateway/mesh), fail-open vs fail-closed on Redis outage, and clock skew.
Intuition: Two ends of a spectrum: either precompute everyone's feed at write time (fast reads, but a disaster for a multimillion-follower celebrity), or assemble the feed at read time (cheap writes, slow reads).
Answer: The core trade-off is fan-out on write (push) vs fan-out on read (pull). Push precomputes each follower's feed on post (fast reads, expensive for celebrities with millions of followers — the "hot key / thundering herd" problem). Pull assembles the feed at read time (cheap writes, slow reads). Real systems use a hybrid: push for normal users, pull for celebrity accounts, merged at read. Add a message queue for async fan-out and idempotent delivery.
Intuition: You want each machine to mint a unique id without asking anyone; the trick is to build each id from pieces that never collide: time + which machine + a small counter.
Answer: Requirements: unique, roughly time-sortable, high throughput, no coordination on the hot path. Snowflake: 64-bit = timestamp | machine-id | per-ms sequence. Handles millions/sec, k-sortable, no central bottleneck. Pitfalls: clock skew / NTP going backwards (must refuse or wait), machine-id assignment, and the 2038-ish epoch/bit-budget. Alternatives: UUIDv7 (time-ordered, no coordination) or DB ticket servers.
Part 7 — Behavioral & Architecture-Decision Questions
Answer these in STAR (Situation, Task, Action, Result). Keep Situation/Task to two sentences; spend your time on Action (what you decided and why) and quantify the Result.
STAR tip: Situation — the constraint (deadline, scale target, unknown load). Task — the decision you owned. Action — the options you weighed, the trade-off matrix (e.g. "we chose eventual consistency via outbox over 2PC because availability mattered more than isolation for orders"), and how you de-risked (spike, load test, feature flag). Result — a metric (p99 latency, cost, incident reduction) and what you'd revisit. Seniors are scored on how they reason about trade-offs, not on the tech chosen.
STAR tip: Situation — impact and blast radius. Task — your role (IC or coordinator). Action — how you stopped the bleeding first (rollback/feature-flag/scale), then diagnosed root cause; emphasize communication and the blameless post-mortem. Result — MTTR, the durable fix (added alert, backpressure, idempotency), and the systemic change so it can't recur. Never blame a person or vendor.
STAR tip: Show disagree-and-commit. Action — you made your case with data/prototype, listened to their constraints, and either changed your mind (intellectual honesty) or committed to their decision and made it succeed. Result — outcome plus preserved relationship. They are testing maturity, not that you "won."
Answer (decision framework): Default to a modular monolith unless you have a concrete forcing function: independent scaling of components, independent deploy cadence across teams, strong team/ownership boundaries (Conway's law), or polyglot needs. Microservices buy organizational scalability and pay in operational complexity (distributed transactions, observability, network failure, data duplication). Premature microservices on a small team is the most expensive mistake; extract services along bounded contexts once the seams are proven. STAR tip if asked for a story: quantify the team size and deploy pain that justified (or didn't justify) the split.
Answer: Incrementally and reversibly. Strangler fig to migrate off a legacy system route-by-route; expand-contract (parallel change) for schema and API — add the new field/endpoint, dual-write/dual-read, migrate readers, then remove the old — never a big-bang cutover. Guard everything behind feature flags, ship behind a canary, and keep a rollback path at each step. Measure at each gate.
Rapid-fire lightning round (say the answer in one breath)
These you should fire off without pausing; each is a ready-made one-liner.
- What's the difference between concurrency and parallelism? Concurrency = dealing with many things at once (structure); parallelism = doing many at once (execution).
- Optimistic vs pessimistic locking? Optimistic assumes no conflict, checks a version at commit (
WHERE version = ?), retries on clash — great for low contention. Pessimistic locks up front (SELECT ... FOR UPDATE) — for high contention/critical sections. - What is deadlock's cheapest prevention? Global lock ordering.
- At-least-once vs at-most-once vs exactly-once? Delivery semantics; exactly-once delivery is largely a myth over a network — aim for at-least-once + idempotency.
- What breaks LSP fastest? A subclass that throws on an inherited method.
- Why is
HashMapunsafe under concurrency? Lost updates and, historically, resize infinite loops; useConcurrentHashMap. - CAP during a partition, pick one: Consistency or availability — never both.
- Why not retry a non-idempotent POST? You may double-charge; add an idempotency key first.
- Best fix for cache stampede? Single-flight loading + jittered TTL.
- When are virtual threads useless? CPU-bound work.
- The Java Memory Model is about visibility, not scheduling: without a happens-before edge there is no guarantee;
volatilegives visibility/ordering for one variable,synchronizedgives exclusion + visibility across many fields. - Break deadlock with global lock ordering; virtual threads scale only I/O-bound work, and since Java 24 (JEP 491)
synchronizedno longer pins in almost all cases. - Recall SOLID by each principle's "smell"; separate the Adapter/Facade/Proxy/Decorator family by intent (convert/simplify/control/augment).
- DDD: Value Object by value, Entity by identity, Aggregate as the transaction boundary; one transaction = one aggregate, the rest eventual via events.
- In distributed systems idempotency is the backbone: safe retries, outbox for the dual-write problem, saga instead of 2PC, and exactly-once effect over at-least-once delivery.
- CAP means choose C or A under a partition; PACELC adds that even without a partition you trade Latency vs Consistency.
- In system design always follow requirements→scale→API→data→components→deep-dive→failure, and in behavioral answer in STAR, focusing on the action and a quantified result.