پست

طراحی سایت رزرو بلیت مانند Ticketmaster

بررسی گام‌به‌گام طراحی سیستم Ticketmaster شامل مشاهده و جستجوی رویدادها، رزرو بلیت بدون رزرو دوگانه، قفل توزیع‌شده، صف انتظار مجازی و بهینه‌سازی جستجو

طراحی سایت رزرو بلیت مانند Ticketmaster

درک مسئله

Ticketmaster یک پلتفرم آنلاین است که کاربران با آن بلیت کنسرت، رویدادهای ورزشی، تئاتر و سایر برنامه‌های زنده را می‌خرند.

نیازمندی‌های عملکردی

مصاحبه را با تعریف نیازمندی‌های عملکردی و غیرعملکردی شروع کنید. نیازمندی‌های عملکردی در چنین اپلیکیشن‌هایی با عبارت «کاربر باید بتواند…» بیان می‌شوند و نیازمندی‌های غیرعملکردی کیفیت‌های سیستم را با عبارت «سیستم باید…» تعریف می‌کنند.

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

نیازمندی‌های اصلی:

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

موارد خارج از محدوده:

  • مشاهده رویدادهای رزروشده توسط کاربر
  • افزودن رویداد توسط مدیران یا برگزارکنندگان
  • قیمت‌گذاری پویا برای رویدادهای محبوب

نیازمندی‌های غیرعملکردی

نیازمندی‌های اصلی:

  • سیستم باید برای جست‌وجو و مشاهده رویدادها Availability (دسترس‌پذیری) را در اولویت قرار بدهد، اما برای رزرو رویداد Consistency (سازگاری) را مقدم بدارد تا رزرو دوگانه رخ ندهد.
  • سیستم باید مقیاس‌پذیر باشد و بتواند Throughput (نرخ گذردهی داده) بالا را برای رویدادهای پرطرفدار تحمل کند (10 میلیون کاربر هم‌زمان برای یک رویداد).
  • جست‌وجو باید Latency (تأخیر) کم داشته باشد (کمتر از 500 میلی‌ثانیه).
  • سیستم خواندن‌محور است و باید نسبت خواندن به نوشتن حدود 100 به 1 را تحمل کند.

موارد خارج از محدوده:

  • محافظت از داده‌های کاربر و پایبندی به GDPR
  • تحمل خطا (Fault Tolerance)
  • تراکنش‌های امن برای پرداخت
  • تست کامل و استقرار آسان (خطوط CI/CD)
  • پشتیبان‌گیری منظم

افزودن ویژگی‌های خارج از محدوده «اضافه‌ای مفید» است. تفکر محصولی را نشان می‌دهد و به مصاحبه‌کننده فرصت می‌دهد بر اساس آنچه می‌خواهد ببیند، شما را در اولویت‌بندی کمک کند. اما اگر این ویژگی‌ها زود به ذهنتان نمی‌رسد، وقت را هدر ندهید و ادامه بدهید.

آماده‌سازی

برنامه‌ریزی رویکرد

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

تعریف موجودیت‌های اصلی

بهتر است با یک نمای کلی از موجودیت‌ها (Entities) شروع کنیم. در این مرحله لازم نیست همه ستون‌ها و جزئیات را بدانیم. وقتی درک روشن‌تری از سیستم داشتیم، به آن‌ها می‌پردازیم. همین موجودیت‌ها فکرمان را هدایت می‌کنند و برای تعریف API پایه‌ای محکم می‌سازند.

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

  • Event (رویداد): اطلاعات اصلی هر رویداد، مثل تاریخ، توضیحات، نوع و مجری یا تیم‌های درگیر. نقطه مرکزی اطلاعات هر رویداد یکتاست.
  • User (کاربر): فردی که با سیستم کار می‌کند. توضیح بیشتری لازم ندارد.
  • Performer (مجری): فرد یا گروهی که در رویداد اجرا می‌کند یا در آن شرکت دارد. ویژگی‌های کلیدی آن شامل نام، توضیح کوتاه و لینک‌های احتمالی به آثار یا پروفایل اوست. این می‌تواند هنرمند، شرکت یا گروه باشد. انتخاب واژه «مجری» برای این است که آن‌قدر عمومی باشد که همه این حالت‌ها را پوشش بدهد.
  • Venue (محل برگزاری): مکان فیزیکی برگزاری رویداد. هر محل شامل جزئیاتی مثل آدرس، ظرفیت و نقشه صندلی (Seat Map) است که چیدمان منحصربه‌فرد صندلی‌ها را نشان می‌دهد.
  • Ticket (بلیت): اطلاعات مربوط به هر بلیت رویداد. شامل شناسه رویداد، جزئیات صندلی (بخش، ردیف و شماره صندلی)، قیمت و وضعیت (موجود یا فروخته‌شده) است.
  • Booking (رزرو): جزئیات خرید بلیت توسط کاربر. معمولاً شامل شناسه کاربر، فهرست شناسه بلیت‌های در حال رزرو، قیمت کل و وضعیت رزرو (مثل در حال انجام یا تأییدشده) است. این موجودیت در مدیریت بخش تراکنشی فرایند خرید نقش کلیدی دارد.

در مصاحبه واقعی، یک فهرست کوتاه مثل همین کافی است. فقط حتماً موجودیت‌ها را با مصاحبه‌کننده مرور کنید تا در یک مسیر باشید.

API یا رابط سیستم

API مشاهده رویدادها ساده است. یک endpoint از نوع GET داریم که eventId را می‌گیرد و جزئیات آن رویداد را برمی‌گرداند:

1
GET /events/:eventId -> Event & Venue & Performer & Ticket[]

بلیت‌ها برای رسم نقشه صندلی در کلاینت لازم‌اند.

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

برای جست‌وجو فقط یک endpoint از نوع GET لازم است که مجموعه‌ای از پارامترهای جست‌وجو را بگیرد و فهرست رویدادهای منطبق را برگرداند:

1
GET /events/search?keyword={keyword}&start={start_date}&end={end_date}&pageSize={page_size}&page={page_number} -> Event[]

برای خرید و رزرو بلیت، یک endpoint از نوع POST داریم که فهرست بلیت‌ها و جزئیات پرداخت را می‌گیرد و bookingId برمی‌گرداند. بعداً در طراحی، این را به دو endpoint جدا تبدیل می‌کنیم، یکی برای رزرو بلیت و یکی برای تأیید خرید. فعلاً این نقطه شروع خوبی است.

1
2
3
4
5
POST /bookings/:eventId -> bookingId
{
  "ticketIds": string[],
  "paymentDetails": ...
}

اشکالی ندارد که از همان ابتدا API های ساده داشته باشید و با پیشرفت طراحی آن‌ها را تکمیل کنید. فقط مثل همیشه ارتباط برقرار کنید: «این یک API ساده برای شروع است، ولی با پیشرفت طراحی احتمالاً باید آن را برای سناریوهای پیچیده‌تر گسترش بدهم.»

طراحی سطح بالا

1) مشاهده رویدادها

وقتی کاربر به صفحه یک رویداد (مثلاً مسیر /event/:eventId سایت) می‌رود، باید جزئیات آن رویداد را ببیند. مهم‌تر از همه، باید یک نقشه صندلی داشته باشد که وضعیت موجود بودن صندلی‌ها را نشان بدهد. این صفحه همچنین نام رویداد و توضیحات آن را نمایش می‌دهد. اطلاعاتی مثل مکان (شامل جزئیات محل برگزاری)، تاریخ رویداد و اطلاعات مجریان یا تیم‌ها هم می‌تواند نمایش داده شود.

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

  • Client (مشتری): کاربران از طریق وب‌سایت یا اپلیکیشن با سیستم کار می‌کنند. همه درخواست‌های کلاینت از طریق API Gateway به باطن می‌رسند.
  • API Gateway (دروازه API): نقطه ورود کلاینت‌ها به میکروسرویس‌های مختلف است. وظیفه اصلی آن مسیریابی درخواست به سرویس مناسب است، اما می‌تواند دغدغه‌های مشترک مثل احراز هویت، Rate Limiting (محدودسازی نرخ درخواست) و لاگ‌گیری را هم مدیریت کند.
  • Event Service: اولین میکروسرویس ما که درخواست‌های مشاهده را با واکشی اطلاعات رویداد، محل برگزاری و مجری از پایگاه داده پاسخ می‌دهد.
  • Events DB: جدول‌های رویدادها، مجریان و محل‌های برگزاری را ذخیره می‌کند.

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

  1. کلاینت یک درخواست GET در قالب REST با eventId می‌فرستد.
  2. API Gateway درخواست را به Event Service می‌رساند.
  3. Event Service اطلاعات رویداد، محل برگزاری و مجری را از Events DB می‌گیرد و به کلاینت برمی‌گرداند.

2) جست‌وجوی رویدادها

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

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

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

  1. کلاینت یک GET با پارامترهای جست‌وجو می‌فرستد.
  2. Load Balancer درخواست را می‌گیرد و با الگوریتم کمترین اتصال فعال (Least Connections) به API Gateway می‌رساند.
  3. API Gateway بعد از احراز هویت اولیه و Rate Limiting، درخواست را به Search Service می‌فرستد.
  4. Search Service از Events DB رویدادهای منطبق با پارامترها را می‌گیرد و به کلاینت برمی‌گرداند.

3) رزرو بلیت رویدادها

مهم‌ترین چیزی که می‌خواهیم از آن جلوگیری کنیم، این است که دو کاربر (یا بیشتر) برای یک بلیت پول بپردازند. این وضعیت در رویداد بسیار ناخوشایند است! برای حل این مشکل سازگاری، باید پایگاه داده‌ای انتخاب کنیم که از تراکنش (Transaction) پشتیبانی کند. این تضمین می‌کند که در هر لحظه فقط یک کاربر می‌تواند بلیت را رزرو کند.

هر پایگاه داده‌ای از MySQL تا DynamoDB که ویژگی‌های ACID را داشته باشد، انتخاب خوبی است. ما PostgreSQL را انتخاب می‌کنیم. علاوه بر این باید سطوح ایزوله‌سازی (Isolation Level) مناسب و قفل در سطح ردیف (Row-level Locking) یا Optimistic Concurrency Control (OCC) را برای جلوگیری کامل از رزرو دوگانه پیاده کنیم.

الگو: مدیریت رقابت (Dealing with Contention). این نمونه روشنی است که رقابت زیاد می‌تواند به نتایج بدی مثل رزرو دوگانه برسد. مدیریت Contention الگویی است که در بسیاری از مصاحبه‌های طراحی سیستم دیده می‌شود و ارزش دارد عمیق‌تر یادش بگیرید.

جریان رزرو ساده

دو جدول جدید در Events DB: ابتدا دو جدول به پایگاه داده اضافه می‌کنیم: Bookings و Tickets. جدول Bookings جزئیات هر رزرو را نگه می‌دارد، یعنی شناسه کاربر، شناسه بلیت‌ها، قیمت کل و وضعیت رزرو. جدول Tickets جزئیات هر بلیت را نگه می‌دارد، یعنی شناسه رویداد، جزئیات صندلی، قیمت و وضعیت. جدول Tickets یک ستون bookingId هم دارد که آن را به جدول Bookings وصل می‌کند.

Booking Service: این میکروسرویس مسئول عملکرد اصلی فرایند رزرو بلیت است. با پایگاه داده‌ای که داده رزروها و بلیت‌ها را نگه می‌دارد کار می‌کند.

  • برای تراکنش‌های پرداخت با Payment Processor (مثل Stripe) ارتباط می‌گیرد. پس از تأیید پرداخت، وضعیت بلیت را به «فروخته‌شده» (sold) تغییر می‌دهد.
  • با جدول‌های Bookings و Tickets برای واکشی، به‌روزرسانی یا ذخیره داده‌های مربوط ارتباط می‌گیرد.

Payment Processor (مثل Stripe): سرویس خارجی که پرداخت‌ها را انجام می‌دهد. وقتی پرداخت پردازش شد، وضعیت تراکنش را به Booking Service اطلاع می‌دهد.

وقتی کاربر می‌خواهد بلیت رزرو کند، این اتفاق‌ها می‌افتد:

  1. کاربر به صفحه رزرو می‌رود و در آنجا جزئیات پرداخت را وارد و رزرو را تأیید می‌کند.
  2. بعد از تأیید، یک درخواست POST با شناسه بلیت‌های انتخاب‌شده به endpoint ‏/bookings فرستاده می‌شود.
  3. سرور رزرو یک تراکنش شروع می‌کند تا: موجود بودن بلیت‌های انتخاب‌شده را بررسی کند، وضعیت آن‌ها را به «رزروشده» (booked) تغییر بدهد، و یک رکورد رزرو جدید در جدول Bookings بسازد.
  4. اگر تراکنش موفق بود، سرور پاسخ موفقیت به کلاینت برمی‌گرداند. در غیر این صورت، مثلاً اگر کاربر دیگری در این فاصله بلیت را رزرو کرده باشد، سرور پاسخ شکست می‌دهد و ما آن را به کلاینت اطلاع می‌دهیم.

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

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

بررسی‌های عمیق

با پوشش نیازمندی‌های عملکردی اصلی، وقت آن است که نیازمندی‌های غیرعملکردی را با بررسی‌های عمیق جلو ببریم. این‌ها مهم‌ترین موضوع‌هایی هستند که برای این سؤال پوشش می‌دهم.

میزان پیش‌قدمی داوطلب در هدایت بررسی‌های عمیق، تابعی از سطح ارشدیت اوست. مثلاً در مصاحبه Mid-level کاملاً منطقی است که مصاحبه‌کننده بیشتر بخش‌های عمیق را هدایت کند. اما در مصاحبه Senior و Staff به بالا، انتظار از داوطلب برای هدایت و مالکیت بیشتر می‌شود. باید بتواند فعالانه گوشه‌های سیستم را بررسی کند و از زاویه‌هایی به سیستم بپردازد که مصاحبه‌کننده فکرش را نکرده است.

1) بهبود تجربه رزرو با نگه‌داشتن موقت بلیت

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

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

باید مطمئن شویم که هنگام پرداخت، بلیت برای کاربر قفل شده است. همچنین اگر کاربر فرایند پرداخت را رها کرد، بلیت باید برای خرید کاربران دیگر آزاد شود. و در نهایت، اگر کاربر پرداخت را کامل کرد، بلیت «فروخته‌شده» و رزرو «تأییدشده» علامت بخورد. چند راه برای این کار داریم.

رویکرد بد: قفل طولانی‌مدت پایگاه داده

راه‌حل بدی که بسیاری از داوطلبان پیشنهاد می‌کنند، قفل‌های طولانی‌مدت پایگاه داده است (که گاهی «تراکنش تعاملی» هم نامیده می‌شود). در این روش از خود پایگاه داده برای قفل کردن ردیف بلیت استفاده می‌کنیم و دسترسی انحصاری را به اولین کاربری که رزرو می‌کند می‌دهیم. معمولاً این کار در PostgreSQL با SELECT FOR UPDATE انجام می‌شود که ردیف‌های انتخاب‌شده را به‌عنوان بخشی از تراکنش قفل می‌کند. قفل تا زمانی که تراکنش Commit یا Rollback شود نگه داشته می‌شود. در این مدت، تراکنش‌های دیگری که بخواهند همان ردیف را با SELECT FOR UPDATE بگیرند، تا آزادشدن قفل مسدود می‌مانند. این تضمین می‌کند که در هر لحظه فقط یک کاربر می‌تواند رزرو بلیت را پردازش کند.

برای آزادسازی قفل دو حالت داریم:

  • اگر کاربر خرید را نهایی کند، تراکنش Commit می‌شود، قفل پایگاه داده آزاد می‌شود و وضعیت بلیت به «رزروشده» (booked) تغییر می‌کند.
  • اگر کاربر بیش از حد معطل کند یا خرید را رها کند، سیستم باید برای آزادسازی قفل به اقدام‌های بعدی یا Timeout نشست (Session) تکیه کند. اگر درست مدیریت نشود، بلیت‌ها ممکن است برای همیشه قفل بمانند.

چالش‌ها: چرا این ایده بدی است؟ قفل‌های پایگاه داده برای مدت کوتاه (یک تراکنش، تقریباً لحظه‌ای) طراحی شده‌اند. باز نگه‌داشتن تراکنش برای مدت طولانی، مثلاً قفل 5 دقیقه‌ای، معمولاً توصیه نمی‌شود. این کار منابع پایگاه داده را تحت فشار می‌گذارد و خطر رقابت روی قفل (Lock Contention) و بن‌بست (Deadlock) را بالا می‌برد. علاوه بر این، پایگاه‌های داده SQL مثل PostgreSQL به‌طور بومی از Timeout برای قفل داخل تراکنش پشتیبانی نمی‌کنند و پیاده‌سازی Timeout نیاز به مدیریت در سطح اپلیکیشن و پیچیدگی بیشتر دارد. در نهایت این رویکرد زیر بار بالا خوب مقیاس نمی‌گیرد، چون قفل‌های طولانی زمان انتظار دیگر کاربران را بیشتر می‌کنند و گلوگاه عملکردی می‌سازند. مدیریت موارد لبه‌ای مثل خرابی اپلیکیشن یا مشکل شبکه هم سخت‌تر می‌شود، چون ممکن است قفل‌ها در وضعیت نامعلوم بمانند.

رویکرد خوب: وضعیت و زمان انقضا روی بلیت

راه‌حل بهتر این است که بلیت را با اضافه کردن فیلد وضعیت و زمان انقضا در جدول Tickets قفل کنیم. بلیت می‌تواند در یکی از سه حالت باشد: AVAILABLE (موجود)، RESERVED (رزرو موقت) و BOOKED (رزروشده قطعی). با این کار وضعیت هر بلیت را دنبال می‌کنیم و به محض رسیدن زمان انقضا، قفل را به‌طور خودکار آزاد می‌کنیم. وقتی کاربر بلیتی را انتخاب می‌کند، وضعیت از AVAILABLE به RESERVED تغییر می‌کند و مهر زمانی فعلی ثبت می‌شود.

حالا ببینیم آزادسازی قفل چطور می‌شود:

  • اگر کاربر خرید را نهایی کند، وضعیت به BOOKED تغییر می‌کند و قفل آزاد می‌شود.
  • اگر کاربر زیاد معطل کند یا خرید را رها کند، بعد از اتمام زمان انقضا وضعیت به AVAILABLE برمی‌گردد.

بخش دشوار، مدیریت زمان انقضاست. می‌توانیم از یک Cron Job استفاده کنیم که دوره‌ای دنبال ردیف‌هایی بگردد که وضعیتشان RESERVED است و زمان سپری‌شده‌شان از مدت قفل بیشتر شده، و آن‌ها را به AVAILABLE برگرداند. این خیلی بهتر است، ولی فاصله بین زمان انقضا و زمان تغییر ردیف برای رویدادهای محبوب ایده‌آل نیست. بهتر است قفل تقریباً بلافاصله پس از انقضا برداشته شود.

چالش‌ها: Cron Job دو ایراد مهم دارد:

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

رویکرد خوب: وضعیت ضمنی با انقضا (Implicit Status)

می‌توانیم از راه‌حل مبتنی بر Cron هم بهتر عمل کنیم. وضعیت هر بلیت را ترکیبی از دو ویژگی تعیین می‌کنیم: یا AVAILABLE است، یا RESERVED است ولی رزرو آن منقضی شده. به‌جای استفاده از تراکنش یا قفل تعاملی طولانی، تراکنش‌های کوتاه می‌سازیم که فیلدهای رکورد بلیت را به‌روز می‌کنند (مثلاً AVAILABLE را به RESERVED تغییر می‌دهند و انقضا را روی «الان + 10 دقیقه» می‌گذارند). داخل این تراکنش‌ها قبل از رزرو بلیت بررسی می‌کنیم که رزرو قبلی منقضی شده باشد.

در شبه‌کد، تراکنش ما این‌طور است:

  1. یک تراکنش شروع می‌کنیم.
  2. بررسی می‌کنیم بلیت AVAILABLE است یا (RESERVED ولی منقضی‌شده).
  3. بلیت را به RESERVED با انقضای «الان + 10 دقیقه» به‌روز می‌کنیم.
  4. تراکنش را Commit می‌کنیم.

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

چالش‌ها: چون باید روی دو مقدار فیلتر کنیم، عملیات خواندن کمی کندتر می‌شود. این را تا حدی با Materialized View یا دیگر قابلیت‌های RDBMS های مدرن همراه با یک ایندکس ترکیبی حل می‌کنیم. جدول پایگاه داده هم برای دیگر مصرف‌کنندگان داده خوانایی کمتری دارد، چون بعضی رزروها در واقع منقضی شده‌اند. می‌توانیم با یک Cron یا جاروی دوره‌ای مثل قبل این را حل کنیم، با یک تفاوت بسیار مهم: اگر این جارو تأخیر کند، رفتار سیستم اثری نمی‌پذیرد.

رویکرد عالی: قفل توزیع‌شده با TTL

راه‌حل عالی دیگر، پیاده‌سازی یک Distributed Lock (قفل توزیع‌شده) با TTL (زمان زنده‌ماندن) با یک سیستم توزیع‌شده مثل Redis است. Redis یک ذخیره‌ساز داده درون‌حافظه‌ای است که از قفل توزیع‌شده پشتیبانی می‌کند و برای محیط‌های با هم‌زمانی بالا مناسب است. دسترس‌پذیری بالا هم دارد و می‌توان از آن برای مکانیزم قفل توزیع‌شده در فرایند رزرو بلیت استفاده کرد. روش کار این است:

  1. وقتی کاربر بلیتی را انتخاب می‌کند، با یک شناسه یکتا (مثلاً شناسه بلیت) و یک TTL از پیش‌تعریف‌شده، قفلی در Redis می‌گیریم. این TTL مثل زمان انقضای خودکار برای قفل عمل می‌کند.
  2. اگر کاربر خرید را کامل کند، وضعیت بلیت در پایگاه داده به BOOKED تغییر می‌کند و اپلیکیشن قفل Redis را دستی آزاد می‌کند (بدون انتظار برای TTL).
  3. اگر TTL منقضی شود (یعنی کاربر خرید را به‌موقع انجام نداده)، Redis قفل را خودکار آزاد می‌کند. این تضمین می‌کند که بلیت بدون مداخله اضافی برای رزرو توسط کاربران دیگر در دسترس قرار می‌گیرد.

حالا جدول Tickets ما فقط دو حالت دارد: AVAILABLE و BOOKED. قفل‌کردن بلیت‌های در حال رزرو کاملاً توسط Redis انجام می‌شود. جفت کلید-مقدار در Redis، شناسه بلیت (کلید) و شناسه کاربر (مقدار) است. به این ترتیب می‌توانیم مطمئن شویم وقتی کاربر رزرو را تأیید می‌کند، همان کاربری است که بلیت را قفل کرده بود.

چالش‌ها: ضعف اصلی به خرابی در مدیریت قفل برمی‌گردد. اگر به هر دلیلی قفل ما از دست برود، برای مدتی تجربه کاربری افت می‌کند. توجه کنید که با این حال هرگز «رزرو دوگانه» نخواهیم داشت، چون پایگاه داده ما از OCC یا هر کنترل هم‌زمانی دیگر برای تضمین این موضوع استفاده می‌کند. ضعف فقط این است که ممکن است کاربر بعد از پر کردن جزئیات پرداخت با خطا مواجه شود، اگر کسی از او جلو بیفتد. این بد است، اما به نظر من نتیجه‌ای بهتر از در دسترس نبودن همه بلیت‌هاست (که در راه‌حل قبلی با شکست Cron Job پیش می‌آمد).

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

جریان رزرو با قفل توزیع‌شده

وقتی کاربر می‌خواهد بلیت رزرو کند:

  1. کاربر یک صندلی را از نقشه صندلی تعاملی انتخاب می‌کند. این کار یک POST /bookings با شناسه بلیت مربوط به آن صندلی راه می‌اندازد.
  2. درخواست از API Gateway به Booking Service می‌رسد.
  3. Booking Service بلیت را با افزودن آن به قفل توزیع‌شده Redis با TTL برابر 10 دقیقه قفل می‌کند (مدتی که بلیت را نگه می‌داریم).
  4. Booking Service همچنین یک ورودی رزرو جدید با وضعیت «در حال انجام» (in-progress) در پایگاه داده می‌نویسد.
  5. سپس شناسه رزرو تازه ساخته‌شده را به کاربر برمی‌گردانیم و کلاینت را به صفحه پرداخت هدایت می‌کنیم.
  6. اگر کاربر همین‌جا متوقف شود، بعد از 10 دقیقه قفل خودکار آزاد می‌شود و بلیت برای خرید کاربر دیگر موجود می‌شود.
  7. کاربر جزئیات پرداخت را وارد می‌کند و «خرید» را می‌زند. پرداخت همراه با bookingId برای پردازش به Stripe فرستاده می‌شود و Stripe از طریق Webhook اعلام می‌کند که پرداخت موفق بوده است.
  8. پس از تأیید پرداخت موفق از Stripe، Webhook در سیستم ما bookingId را که در Metadata درخواست Stripe گذاشته بودیم برمی‌گرداند. با این bookingId، Webhook یک تراکنش پایگاه داده شروع می‌کند تا جدول‌های Tickets و Bookings را هم‌زمان به‌روز کند. یعنی وضعیت بلیت مربوط به آن رزرو در جدول Tickets به «فروخته‌شده» (sold) تغییر می‌کند و ورودی مربوط در جدول Bookings «تأییدشده» (confirmed) علامت می‌خورد.

حالا بلیت رزرو شده است!

2) مقیاس‌دهی API مشاهده برای 10 میلیون درخواست هم‌زمان در رویدادهای پرطرفدار

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

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

رویکرد عالی: کش، Load Balancing و مقیاس افقی

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

  • از یک کش توزیع‌شده با جفت کلید-مقدار مثل eventId:eventObject برای ارائه کارآمد داده‌های پربازدید استفاده کنید.
  • از Redis یا Memcached به‌عنوان ذخیره‌ساز درون‌حافظه‌ای استفاده کنید و از سرعتشان برای حجم بالای عملیات خواندن بهره ببرید. استراتژی Read-through تضمین می‌کند داده در دسترس است. در Cache Miss از پایگاه داده می‌خوانیم و کش را به‌روز می‌کنیم.

ابطال کش و سازگاری (Cache Invalidation and Consistency).

  • Trigger های پایگاه داده را طوری تنظیم کنید که با تغییر داده (مثل تغییر تاریخ رویداد یا ردیف مجری) به سیستم کش اطلاع بدهند تا ورودی‌های مربوط را باطل کند.
  • برای ورودی‌های کش سیاست TTL تعریف کنید و تازه‌سازی دوره‌ای را تضمین کنید. TTL برای داده‌های ثابت مثل اطلاعات محل برگزاری می‌تواند طولانی و برای داده‌هایی که زیاد به‌روز می‌شوند، مثل موجودی رویداد، کوتاه باشد.

Load Balancing. از الگوریتم‌هایی مثل Round Robin یا Least Connections برای توزیع یکنواخت ترافیک بین نمونه‌های سرور استفاده کنید. Load Balancing را برای همه سرویس‌ها و پایگاه‌های داده همراه با مقیاس افقی اجرا کنید. معمولاً لازم نیست این را در طراحی نشان بدهید، اما ذکرش خوب است.

مقیاس افقی. Event Service بدون حالت (Stateless) است و می‌توانیم برای پاسخ به تقاضا، آن را افقی مقیاس بدهیم، یعنی نمونه‌های بیشتری اضافه کنیم و بین آن‌ها بار را توزیع کنیم.

چالش‌ها: یکی از چالش‌های اصلی حفظ سازگاری بین کش و پایگاه داده است. این در صورت به‌روزرسانی‌های مکرر جزئیات رویداد سخت‌تر می‌شود (اما ما چنین انتظاری نداریم). مدیریت تعداد زیادی نمونه هم پیچیدگی می‌آورد و تضمین استقرار روان و رویه‌های Rollback مؤثر، چالش عملیاتی اضافه می‌کند.

3) تجربه کاربری خوب در رویدادهای پرتقاضا با میلیون‌ها رزرو هم‌زمان

در رویدادهای محبوب، نقشه صندلی که بارگذاری شده خیلی زود کهنه می‌شود. کاربران از کلیک مکرر روی صندلی‌ای که قبلاً رزرو شده ناامید می‌شوند. باید مطمئن شویم نقشه صندلی همیشه به‌روز است و کاربران از تغییرات به‌صورت بلادرنگ باخبر می‌شوند.

گاهی بهترین راه‌حل از نظر فنی چالش‌برانگیزترین نیست. نشانه مهندس Senior یا Staff توانایی حل مسائل کسب‌وکار است و گاهی این یعنی خارج از چارچوب محدودیت‌های فرضی فکر کردن. راه‌حل‌های «خوب» و «عالی» زیر تفاوت مشترک بین داوطلبان Senior و Staff را نشان می‌دهند.

رویکرد خوب: Server-Sent Events برای به‌روزرسانی بلادرنگ

برای اینکه نقشه صندلی همیشه به‌روز باشد، می‌توانیم از Server-Sent Events (SSE) برای ارسال به‌روزرسانی‌های بلادرنگ به کلاینت استفاده کنیم. با این روش، به محض اینکه کاربر دیگری صندلی را رزرو (یا موقتاً رزرو) کند، نقشه صندلی بدون نیاز به رفرش صفحه به‌روز می‌شود. SSE یک کانال ارتباطی یک‌طرفه بین سرور و کلاینت است. سرور می‌تواند بدون درخواست کلاینت، داده برایش بفرستد.

چالش‌ها: این رویکرد برای رویدادهای نسبتاً محبوب خوب کار می‌کند، ولی در رویدادهای فوق‌العاده پرطرفدار تجربه کاربری همچنان آسیب می‌بیند. مثلاً در «حالت تیلور سوئیفت»، نقشه صندلی بلافاصله پر می‌شود و کاربران با ناپدید شدن لحظه‌به‌لحظه صندلی‌های موجود، تجربه‌ای گیج‌کننده و طاقت‌فرسا خواهند داشت.

رویکرد عالی: صف انتظار مجازی

برای رویدادهای بسیار پرطرفدار، می‌توانیم یک صف انتظار مجازی (Virtual Waiting Queue) پیاده کنیم تا دسترسی کاربران را در زمان‌های تقاضای فوق‌العاده بالا مدیریت کند. کاربران حتی پیش از اینکه صفحه رزرو (نقشه صندلی) را ببینند، وارد این صف می‌شوند. هدف کنترل جریان دسترسی کاربران به رابط رزرو است، تا از اضافه‌بار سیستم جلوگیری شود و تجربه کاربری بهتر شود. در سطح بالا این‌طور کار می‌کند:

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

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

الگو: به‌روزرسانی بلادرنگ (Real-time Updates). اگرچه برای این مورد SSE را انتخاب نمی‌کنیم، بسیاری از سیستم‌ها جنبه‌ای از ارسال به‌روزرسانی بلادرنگ به کلاینت دارند. همه رویکردها را در الگوی Real-time Updates توضیح داده‌ایم.

4) بهبود جست‌وجو برای رعایت نیاز Latency کم

پیاده‌سازی فعلی جست‌وجو کافی نیست. کوئری‌هایی که رویدادها را بر اساس کلمه کلیدی در نام، توضیحات یا فیلدهای دیگر جست‌وجو می‌کنند، به‌خاطر عبارت LIKE با علامت درصد در ابتدا، به اسکن کامل جدول (Full Table Scan) نیاز دارند. این می‌تواند بسیار کند باشد، به‌ویژه با افزایش تعداد رویدادها.

1
2
3
4
5
-- slow query
SELECT *
FROM Events
WHERE name LIKE '%Taylor%'
  OR description LIKE '%Taylor%'

چند استراتژی برای بهبود عملکرد جست‌وجو و رعایت نیاز Latency کم را بررسی می‌کنیم.

رویکرد خوب: ایندکس‌گذاری و بهینه‌سازی کوئری

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

کوئری‌ها را هم برای عملکرد بهتر بهینه کنید. مثلاً با EXPLAIN طرح اجرای کوئری را تحلیل کنید، از SELECT * پرهیز کنید و با LIMIT تعداد ردیف‌های برگشتی را محدود کنید. همچنین استفاده از UNION به‌جای OR برای ترکیب چند شرط می‌تواند عملکرد را بهتر کند.

چالش‌ها: ایندکس‌های استاندارد برای کوئری‌های تطبیق جزئی رشته مؤثر نیستند. مثلاً وقتی «Taylor» را به‌جای «Taylor Swift» کامل جست‌وجو می‌کنیم. این به ملاحظات بیشتری نیاز دارد، مثل پیاده‌سازی جست‌وجوی متن کامل (Full-text Search) یا عملگرهای LIKE که ممکن است کارایی کمتری داشته باشند.

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

رویکرد خوب: ایندکس متن کامل در پایگاه داده

می‌توانیم استراتژی ایندکس‌گذاری بالا را با استفاده از ایندکس‌های متن کامل در پایگاه داده گسترش بدهیم، اگر در دسترس باشند. در پایگاه‌های داده SQL شناخته‌شده مثل MySQL یا Postgres افزونه‌های متن کامل وجود دارد که در پشت صحنه از موتورهای جست‌وجو مثل Lucene بهره می‌برند. این افزونه‌ها کوئری‌های رشته‌های مشخص مثل «Taylor» یا «Swift» را بسیار سریع‌تر از اسکن کامل جدول با LIKE انجام می‌دهند.

چالش‌ها: ایندکس‌های متن کامل فضای ذخیره‌سازی اضافه می‌خواهند و نسبت به ایندکس‌های استاندارد ممکن است کندتر جست‌وجو کنند. نگهداری آن‌ها هم دشوارتر است، چون در کوئری‌ها و نگهداری پایگاه داده به مدیریت ویژه نیاز دارند.

رویکرد عالی: موتور جست‌وجوی تخصصی مثل Elasticsearch

Elasticsearch یا یک موتور جست‌وجوی متن کامل مشابه اضافه کنید. Elasticsearch موتور جست‌وجوی قدرتمندی است که در جست‌وجوی متن کامل، اجرای کوئری‌های پیچیده و مدیریت کارآمد ترافیک با حجم بالا عالی عمل می‌کند. هسته آن بر ایندکس معکوس (Inverted Index) استوار است، قابلیتی که عملیات جست‌وجو را بسیار کارآمد می‌کند. ایندکس معکوس هر کلمه یکتا را به اسناد یا رکوردهایی که در آن‌ها ظاهر می‌شود نگاشت می‌کند و این باعث می‌شود Elasticsearch داده را سریع پیدا و بازیابی کند.

برای اینکه داده‌های Elasticsearch همیشه با SQL DB هماهنگ باشند، می‌توانیم از Change Data Capture (CDC) برای همگام‌سازی بلادرنگ یا نزدیک به بلادرنگ داده از PostgreSQL به Elasticsearch استفاده کنیم. این تنظیم، تغییرات PostgreSQL مثل درج، به‌روزرسانی و حذف را ثبت و در ایندکس Elasticsearch تکرار می‌کند.

با Elasticsearch می‌توانیم جست‌وجوی فازی (Fuzzy Search) را فعال کنیم که در برابر اشتباه‌های تایپی تحمل‌پذیر است. مثلاً بین «Taylor Swift» و «Tayler Swift» تفاوت قائل نمی‌شود. انجام این کار فقط با SQL بسیار دشوار است.

چالش‌ها: همگام نگه‌داشتن ایندکس Elasticsearch با PostgreSQL می‌تواند پیچیده باشد و مکانیزم قابل اعتمادی برای تضمین سازگاری داده می‌خواهد. نگهداری یک کلاستر Elasticsearch هم پیچیدگی و هزینه زیرساختی بیشتری اضافه می‌کند.

5) سرعت‌دادن به جست‌وجوهای تکراری و کاهش بار زیرساخت جست‌وجو

رویکرد خوب: کش نتایج جست‌وجو

از مکانیزم‌های کش مثل Redis یا Memcached برای ذخیره نتایج جست‌وجوهای تکراری استفاده کنید. این کار با ارائه کوئری‌های مکرر از کش، به‌جای مراجعه دوباره به پایگاه داده یا موتور جست‌وجو، بار زیرساخت جست‌وجو را کم می‌کند.

  • طراحی کلید: کلیدهای کش را بر اساس پارامترهای کوئری جست‌وجو بسازید تا هر کوئری به‌طور یکتا شناسایی شود.
  • TTL: برای داده کش‌شده TTL مناسب تعیین کنید تا تازگی و مرتبط‌بودن اطلاعات حفظ شود.

برای مثال، یک ورودی کش ممکن است این شکل را داشته باشد:

1
2
3
4
5
{
  "key": "search:keyword=Taylor Swift&start=2021-01-01&end=2021-12-31",
  "value": [event1, event2, event3],
  "ttl": 60 * 60 * 24 // 24 hours
}

چالش‌ها: مدیریت کارآمد ابطال کش (Cache Invalidation) می‌تواند دشوار باشد. داده کهنه در کش می‌تواند نتایج نادرست به کاربر نشان بدهد. اگر نتایج جست‌وجوی فازی را کش کنید، این مشکل پیچیده‌تر می‌شود. می‌توانید برای سازگاری داده، ترکیبی از TTL و Trigger های ابطال کش بر پایه برچسب‌های کش (Cache Tags) استفاده کنید. Cache Miss های مکرر هم می‌تواند بار زیرساخت جست‌وجو را بالا ببرد، به‌ویژه در اوج مصرف.

رویکرد عالی: کش داخلی Elasticsearch و CDN

Elasticsearch قابلیت‌های کش داخلی دارد که می‌توان از آن برای ذخیره نتایج جست‌وجوهای مکرر استفاده کرد. این کار بار پردازش کوئری را در خود موتور جست‌وجو کم می‌کند. Node Query Cache در Elasticsearch یک کش LRU است که بین همه Shard های یک گره مشترک است و نتایج کوئری‌هایی را که در فیلتر استفاده شده‌اند نگه می‌دارد. علاوه بر این، Shard Request Cache در سطح شارد برای نگهداری پاسخ‌های جست‌وجو که شامل Aggregation هستند به کار می‌رود. این‌ها را می‌توان برای استراتژی‌های کش تطبیقی (Adaptive Caching) به کار برد، یعنی سیستم به‌مرور نتایج کوئری‌های پرتکرار را یاد می‌گیرد و ذخیره می‌کند.

می‌توانیم از CDN هم برای کش نتایج جست‌وجو نزدیک‌تر به کاربر استفاده کنیم که Latency را کم و زمان پاسخ را بهتر می‌کند. مثلاً می‌توان AWS CloudFront را یکپارچه کرد. توجه کنید این فقط وقتی معنا دارد که نتایج جست‌وجو شخصی‌سازی نشده باشند، یعنی عبارت جست‌وجوی یکسان برای همه کاربران نتیجه یکسان بدهد.

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

با پیشرفت در بررسی‌های عمیق، باید طراحی را به‌روز کنید تا تغییراتی را که اعمال کرده‌اید منعکس کند. پس از آن می‌توانید یک طراحی نهایی شبیه این داشته باشید.

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

انتظارات در هر سطح

شاید فکر کنید «واقعاً چقدر از این‌ها در مصاحبه لازم است؟» بیایید تفکیک کنیم.

سطح Mid-level

  • عرض در برابر عمق: تمرکز بیشتر روی عرض است (حدود 80 درصد عرض و 20 درصد عمق). باید بتوانید یک طراحی سطح بالا بسازید که نیازمندی‌های عملکردی تعریف‌شده را پوشش بدهد، هرچند با بسیاری از اجزا فقط آشنایی سطحی داشته باشید.
  • مرور مبانی: مصاحبه‌کننده مدتی را صرف بررسی مبانی می‌کند تا مطمئن شود می‌دانید هر جزء چه کاری می‌کند. مثلاً اگر API Gateway اضافه کنید، انتظار داشته باشید بپرسد چه کاری می‌کند و در سطح بالا چطور کار می‌کند. او هیچ چیز را بدیهی فرض نمی‌کند.
  • ترکیب هدایت و همراهی: در مراحل اولیه مصاحبه باید خودتان جلو بروید، اما مصاحبه‌کننده انتظار ندارد مشکلات طراحی را با دقت بالا پیش‌دستانه تشخیص بدهید. طبیعی است که در مراحل بعد با بررسی طراحی شما، خودش هدایت را به دست بگیرد.
  • معیار برای Ticketmaster: داوطلب E4 باید endpoint های API و مدل داده را روشن تعریف کند و طراحی سطح بالایی بسازد که دست‌کم برای مشاهده و رزرو رویدادها کارا باشد. او باید بتواند مسئله «بدون رزرو دوگانه» را دست‌کم با «راه‌حل خوب» حل کند، یعنی با فیلد وضعیت، Timeout و Cron Job. هر عمق اضافه امتیاز مثبت است، اما بررسی عمیق بیشتری از او انتظار نمی‌رود.

سطح Senior

  • عمق تخصص: انتظار به سمت دانش عمیق‌تر می‌رود، حدود 60 درصد عرض و 40 درصد عمق. باید در حوزه‌هایی که تجربه عملی دارید وارد جزئیات فنی شوید و درک عمیقی از مفاهیم و فناوری‌های کلیدی مسئله نشان بدهید.
  • طراحی سیستم پیشرفته: باید با اصول پیشرفته طراحی سیستم آشنا باشید. مثلاً دانستن اینکه برای جست‌وجوی رویداد از یک ذخیره‌ساز بهینه برای جست‌وجو مثل Elasticsearch استفاده کنید، ضروری است. همچنین انتظار می‌رود کاربرد کش توزیع‌شده برای قفل کردن بلیت را بفهمید و درباره استراتژی‌های دقیق مقیاس‌دهی بحث کنید (حتی اگر این کار نیاز به راهنمایی مصاحبه‌کننده داشته باشد)، از جمله Partitioning و Replication. توانایی شما در حرکت با اطمینان و وضوح در این موضوع‌های پیشرفته، کلیدی است.
  • تشریح تصمیم‌های معماری: باید مزایا و معایب انتخاب‌های معماری را روشن بیان کنید، به‌ویژه اثر آن‌ها روی مقیاس‌پذیری، عملکرد و قابلیت نگهداری، و تصمیم‌هایتان را توجیه کنید.
  • حل مسئله و پیش‌قدمی: باید مهارت حل مسئله قوی و رویکرد فعالانه نشان بدهید. یعنی چالش‌های احتمالی طراحی را پیش‌بینی کنید، بهبود پیشنهاد بدهید، گلوگاه‌ها را پیدا و رفع کنید، عملکرد را بهینه کنید و از قابلیت اطمینان سیستم مطمئن شوید.
  • معیار برای Ticketmaster: از داوطلب E5 انتظار می‌رود طراحی سطح بالای اولیه را سریع جلو ببرد تا بتواند وقتش را صرف بحث دقیق درباره بهینه‌سازی جست‌وجو، رزرو بدون رزرو دوگانه (رسیدن به قفل توزیع‌شده یا راه‌حل‌های باکیفیت دیگر) و حتی مدیریت رویدادهای پرطرفدار و مقیاس‌پذیری بخش مشاهده بکند.

سطح Staff به بالا

  • تأکید بر عمق: انتظار می‌رود وارد ظرافت‌های طراحی سیستم شوید، با حدود 40 درصد عرض و 60 درصد عمق. حتی اگر این مسئله خاص را قبلاً حل نکرده باشید، باید در دنیای واقعی آن‌قدر مسئله حل کرده باشید که با پشتوانه تجربه‌تان با اطمینان راه‌حل طراحی کنید.
  • دانش عملی فناوری‌ها: باید بدانید از چه فناوری‌هایی استفاده کنید، نه فقط در تئوری بلکه در عمل، و با تجربه‌های قبلی توضیح بدهید چطور برای حل مسائل خاص به کار می‌روند. مصاحبه‌کننده می‌داند مباحث ساده‌تر (REST API، نرمال‌سازی داده و غیره) را می‌دانید، پس سریع از آن‌ها عبور می‌کنید تا برای بخش‌های جذاب‌تر وقت بماند.
  • پیش‌قدمی بالا: انتظار می‌رود مسائل را مستقل شناسایی و حل کنید و چالش‌های اصلی طراحی را بشناسید. این فقط پاسخ به مشکلات پس از بروز نیست، بلکه پیش‌بینی آن‌ها و اجرای راه‌حل‌های پیشگیرانه هم هست. مصاحبه‌کننده فقط باید برای تمرکز دخالت کند، نه برای هدایت.
  • کاربرد عملی فناوری: باید در به‌کارگیری عملی فناوری‌های مختلف مسلط باشید. تجربه شما باید گفت‌وگو را هدایت کند و درک روشنی از پیکربندی ابزارها و سیستم‌ها در سناریوهای واقعی نشان بدهد.
  • حل مسئله پیچیده و تصمیم‌گیری: نه‌تنها باید چالش‌های فنی پیچیده را حل کنید، بلکه تصمیم‌های آگاهانه‌ای بگیرید که مقیاس‌پذیری، عملکرد، قابلیت اطمینان و نگهداری را در نظر می‌گیرد.
  • طراحی پیشرفته و مقیاس‌پذیری: رویکرد شما باید بر مقیاس‌پذیری و قابلیت اطمینان در بار بالا متمرکز باشد. این شامل درک کامل سیستم‌های توزیع‌شده، Load Balancing، استراتژی‌های ذخیره‌سازی و دیگر مفاهیم پیشرفته است.
  • معیار برای Ticketmaster: برای داوطلب Staff به بالا، انتظار از عمق و کیفیت راه‌حل‌ها بالاست، به‌ویژه برای سناریوهای پیچیده‌ای که بحث شد. داوطلبان عالی دست‌کم در دو یا سه حوزه کلیدی عمیق می‌شوند و نه‌تنها مهارت، بلکه تفکر خلاق و توانایی یافتن راه‌حل بهینه را هم نشان می‌دهند. یکی از شاخص‌های سطح داوطلب Staff، میزان بینش و دانشی است که به گفت‌وگو می‌آورد. معیار خوب این است که مصاحبه‌کننده با درک یا دیدگاه تازه‌ای از بحث بیرون برود.