پست

طراحی اپلیکیشن دوستیابی مانند Tinder

بررسی گام‌به‌گام طراحی سیستم Tinder شامل ساخت پروفایل، تولید فید پیشنهادی، ثبت Swipe، تشخیص تطابق متقابل و جلوگیری از نمایش دوباره پروفایل‌ها

طراحی اپلیکیشن دوستیابی مانند Tinder

درک مسئله

Tinder یک اپلیکیشن موبایل دوستیابی است که به افراد کمک می‌کند با کشیدن انگشت به راست (لایک) یا به چپ (رد کردن) روی پروفایل‌ها، با هم آشنا شوند. این اپلیکیشن از داده‌های موقعیت مکانی و فیلترهای کاربر برای پیشنهاد افراد مناسب در نزدیکی او استفاده می‌کند.

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

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

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

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

  • آپلود تصویر
  • چت مستقیم (DM) بعد از تطابق
  • ارسال Super Swipe یا خرید قابلیت‌های ویژه (Premium)

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

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

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

  • سیستم باید برای Swipe سازگاری قوی (Strong Consistency) داشته باشد. اگر کاربر A کاربر B را که قبلاً او را «بله» زده، «بله» بزند، باید اعلان تطابق بگیرد.
  • سیستم باید برای تعداد زیادی کاربر روزانه و هم‌زمان مقیاس بگیرد (20 میلیون کاربر فعال روزانه، میانگین حدود 100 Swipe برای هر کاربر در روز).
  • سیستم باید پشته پروفایل‌های پیشنهادی را با Latency (تأخیر) کم بارگذاری کند (مثلاً کمتر از 300 میلی‌ثانیه).
  • سیستم باید از نمایش پروفایل‌هایی که کاربر قبلاً روی آن‌ها Swipe کرده خودداری کند.

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

  • محافظت در برابر پروفایل‌های جعلی
  • مانیتورینگ و هشداردهی

آماده‌سازی

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

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

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

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

برای Tinder موجودیت‌های اولیه ساده‌اند:

  • User (کاربر): هم کاربری را نشان می‌دهد که از اپلیکیشن استفاده می‌کند و هم پروفایلی را که ممکن است به کاربر دیگر نشان داده شود. معمولاً هنگام فهرست کردن موجودیت‌ها مفهوم «کاربر» را حذف می‌کنیم، اما چون کاربران روی کاربران دیگر Swipe می‌کنند، اینجا آن را آورده‌ایم.
  • Swipe (کشیدن انگشت): بیان «بله» یا «نه» روی پروفایل یک کاربر. این موجودیت متعلق به یک کاربر است (swiping_user) و به کاربر دیگری اشاره دارد (target_user).
  • Match (تطابق): ارتباط بین دو کاربر که هر دو یکدیگر را «بله» زده‌اند.

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

API

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

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

1
2
3
4
5
6
7
8
POST /profile
{
  "age_min": 20,
  "age_max": 30,
  "distance": 10,
  "interestedIn": "female" | "male" | "both",
  ...
}

بعد به endpoint ای نیاز داریم که «فید» پروفایل‌ها را برای Swipe بگیرد. به این ترتیب یک «پشته» (Stack) از پروفایل‌ها برای کاربر آماده است:

1
GET /feed?lat={}&long={}&distance={} -> User[]

لازم نیست فیلترهای دیگری مثل سن و علایق را بفرستیم، چون فرض می‌کنیم کاربر آن‌ها را قبلاً در تنظیمات اپلیکیشن مشخص کرده و می‌توانیم از سمت سرور بارگذاری‌شان کنیم.

Tinder به کاربران پروفایل‌هایی را در شعاع مشخصی از موقعیت فعلی نشان می‌دهد، مگر اینکه نسخه Premium خریده باشند (خارج از محدوده). چون این مقدار همیشه ممکن است تغییر کند، آن را از سمت کلاینت می‌فرستیم و در سمت سرور ذخیره نمی‌کنیم.

شاید وسوسه شوید برای endpoint فید صفحه‌بندی (Pagination) در نظر بگیرید. در واقع برای Tinder لازم نیست، چون ما در حال تولید پیشنهاد هستیم. اگر فهرست فعلی تمام شد، اپلیکیشن به‌جای صفحه‌بندی می‌تواند دوباره endpoint را صدا بزند تا پیشنهادهای بیشتری بگیرد.

همچنین به endpoint ای برای ثبت Swipe نیاز داریم:

1
2
3
4
5
POST /swipe/{userId}
Request:
{
  decision: "yes" | "no"
}

در همه این درخواست‌ها اطلاعات کاربر در هدرها (با Session Token یا JWT) ارسال می‌شود. این الگوی رایجی است و راه خوبی برای احراز هویت و مجاز بودن کاربر است. اطلاعات کاربر را در بدنه درخواست نفرستید، چون کلاینت به‌راحتی می‌تواند آن را دستکاری کند.

در مصاحبه می‌توانید فقط مشخص کنید کدام endpoint ها به احراز هویت کاربر نیاز دارند و کدام نه. در مورد ما همه endpoint ها احراز هویت می‌خواهند.

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

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

1) ساخت پروفایل با ترجیحات

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

درخواست POST را به /profile می‌گیریم و این ترجیحات را در پایگاه داده نگه می‌داریم. با یک معماری ساده کلاینت، سرور و پایگاه داده می‌توانیم این کار را انجام دهیم.

  • Client (مشتری): کاربران از طریق اپلیکیشن موبایل با سیستم کار می‌کنند.
  • API Gateway (دروازه API): درخواست‌های ورودی را به سرویس مناسب می‌رساند. در این مورد، Profile Service.
  • Profile Service: درخواست‌های ورودی پروفایل را با به‌روزرسانی ترجیحات پروفایل کاربر در پایگاه داده پاسخ می‌دهد.
  • پایگاه داده: اطلاعات پروفایل کاربران، ترجیحات و سایر اطلاعات مرتبط را ذخیره می‌کند.

وقتی کاربر پروفایل می‌سازد:

  1. کلاینت یک POST به /profile می‌فرستد و اطلاعات پروفایل در بدنه درخواست است.
  2. API Gateway درخواست را به Profile Service می‌رساند.
  3. Profile Service ترجیحات پروفایل کاربر را در پایگاه داده به‌روز می‌کند.
  4. نتیجه از طریق API Gateway به کلاینت برمی‌گردد.

2) مشاهده مجموعه‌ای از پروفایل‌های پیشنهادی

وقتی کاربر اپلیکیشن را باز می‌کند، باید فوراً دسته‌ای از پروفایل‌ها برای Swipe به او نشان داده شود. این پروفایل‌ها از فیلترهای کاربر (مثل سن و علایق) و همچنین موقعیت مکانی او (مثلاً کمتر از 2 مایل، کمتر از 5 مایل یا کمتر از 15 مایل) پیروی می‌کنند.

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

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

1
2
3
4
5
SELECT * FROM users
WHERE age BETWEEN 18 AND 35
AND interestedIn = 'female'
AND lat BETWEEN userLat - maxDistance AND userLat + maxDistance
AND long BETWEEN userLong - maxDistance AND userLong + maxDistance

وقتی کاربر مجموعه جدیدی از پروفایل‌ها را می‌خواهد:

  1. کلاینت یک GET به /feed می‌فرستد و موقعیت مکانی کاربر را به‌عنوان پارامتر کوئری می‌دهد.
  2. API Gateway درخواست را به Profile Service می‌رساند.
  3. Profile Service از پایگاه داده کاربران، فهرستی از کاربرانی را که با ترجیحات و موقعیت کاربر می‌خوانند کوئری می‌کند.
  4. نتایج از طریق API Gateway به کلاینت برمی‌گردند.

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

3) Swipe روی پروفایل‌ها

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

به راهی نیاز داریم که Swipe ها را نگه داریم و بررسی کنیم که تطابق وجود دارد یا نه. باز هم با یک راه ساده و ناکارآمد شروع می‌کنیم و در بررسی عمیق بهبودش می‌دهیم.

دو جزء جدید معرفی می‌کنیم:

  • Swipe Service: ثبت Swipe ها و بررسی تطابق را انجام می‌دهد.
  • Swipe DB (پایگاه داده Swipe): داده Swipe کاربران را نگه می‌دارد.

توجه کنید که این بار یک سرویس جدا و یک پایگاه داده جدا انتخاب می‌کنیم. چرا؟

توجیه من این است که مشاهده و ساخت پروفایل خیلی کمتر از نوشتن Swipe رخ می‌دهد. با جدا کردن سرویس‌ها اجازه می‌دهیم Swipe Service مستقل رشد کند. برای پایگاه داده هم همین‌طور، چون حجم داده Swipe بسیار زیاد است. با 20 میلیون کاربر فعال روزانه، 100 Swipe در روز و 100 بایت برای هر Swipe، به 200 گیگابایت داده در روز می‌رسیم! یک پایگاه داده بهینه برای نوشتن مثل Cassandra این حجم را بهتر تحمل می‌کند (هرچند شاید برای پایگاه داده پروفایل مناسب نباشد). جداسازی همچنین اجازه می‌دهد عملیات Swipe را مستقل مقیاس بدهیم و بهینه کنیم، و منطق و استراتژی‌های کش خاص خودش را بدون اثر بر Profile Service پیاده کنیم.

جداسازی برای همه سیستم‌ها انتخاب درستی نیست، اما اینجا مزایا از معایب بیشتر است.

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

Cassandra اینجا انتخاب مناسبی است. می‌توانیم بر اساس swiping_user_id پارتیشن‌بندی کنیم. این یعنی الگوی دسترسی «آیا کاربر A روی کاربر B Swipe کرده است؟» سریع است، چون می‌توانیم به‌طور قابل پیش‌بینی یک پارتیشن را برای این داده بخوانیم. علاوه بر این، Cassandra به‌خاطر موتور ذخیره‌سازی بهینه برای نوشتن (CommitLog، Memtable و SSTable) بسیار برای نوشتن حجیم مناسب است. نقطه ضعف استفاده از Cassandra، Eventual Consistency (سازگاری نهایی) داده Swipe است که از Cassandra به ارث می‌بریم. در بررسی‌های عمیق، راه‌هایی برای رفع این مشکل را بحث می‌کنیم.

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

  1. کلاینت یک POST به /swipe می‌فرستد و شناسه پروفایل و جهت Swipe (راست یا چپ) را به‌عنوان پارامتر می‌دهد.
  2. API Gateway درخواست را به Swipe Service می‌رساند.
  3. Swipe Service داده Swipe را در Swipe DB ذخیره می‌کند.
  4. Swipe Service بررسی می‌کند که Swipe معکوس در Swipe DB هست یا نه، و اگر هست، تطابق را به کلاینت برمی‌گرداند.

4) اعلان تطابق برای Swipe متقابل

وقتی تطابقی رخ می‌دهد، هر دو نفر باید از آن باخبر شوند. برای روشن‌شدن موضوع، اولین فردی را که نفر دیگر را لایک می‌کند «شخص A» و نفر دوم را «شخص B» می‌نامیم.

اطلاع‌دادن به شخص B آسان است! در واقع قبلاً این کار را کرده‌ایم. چون او دومین نفری است که Swipe می‌کند، بلافاصله بعد از Swipe به راست بررسی می‌کنیم که شخص A هم او را لایک کرده یا نه، و اگر کرده بود، یک گرافیک «تطابق پیدا کردید!» روی دستگاه شخص B نشان می‌دهیم.

اما شخص A چه؟ ممکن است هفته‌ها قبل روی شخص B Swipe کرده باشد. باید یک Push Notification برایش بفرستیم تا بداند ارتباط جدیدی منتظر اوست.

برای این کار فقط به سرویس‌های Push Notification بومی دستگاه، مثل Apple Push Notification Service (APNS) یا Firebase Cloud Messaging (FCM) تکیه می‌کنیم. APNS و FCM هر دو سرویس اعلان فشاری‌اند که می‌توانیم با آن‌ها به دستگاه کاربران Push Notification بفرستیم. هر کدام API و SDK بومی خودشان را دارند.

حالا که Push Notification وارد جریان شد، دوباره فرایند کامل Swipe را مرور می‌کنیم:

  1. مدتی قبل، شخص A روی شخص B به راست Swipe کرد و ما این Swipe را در Swipe DB ذخیره کردیم.
  2. شخص B روی شخص A به راست Swipe می‌کند.
  3. سرور Swipe معکوس را بررسی می‌کند و می‌بیند که وجود دارد.
  4. بلافاصله بعد از Swipe، پیام «تطابق پیدا کردید!» را به شخص B نشان می‌دهیم.
  5. از طریق APNS یا FCM یک Push Notification برای شخص A می‌فرستیم و به او خبر می‌دهیم تطابق جدیدی دارد.

چون این طراحی کمتر به جریان بعد از تطابق مربوط است، می‌توانیم از ورود به جزئیات ذخیره‌سازی Match خودداری کنیم. همچنین می‌توانیم فرض کنیم یک سرویس خارجی از Push Notification پشتیبانی می‌کند. حتماً این فرض‌ها را با مصاحبه‌کننده روشن کنید!

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

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

1) سازگاری Swipe با Latency کم

با یک سناریوی شکست شروع می‌کنیم. تصور کنید شخص A و شخص B تقریباً هم‌زمان یکدیگر را به راست (لایک) Swipe کنند. ترتیب عملیات می‌تواند این‌طور باشد:

  1. Swipe شخص A به سرور می‌رسد و Swipe معکوس را بررسی می‌کنیم. چیزی پیدا نمی‌شود.
  2. Swipe شخص B به سرور می‌رسد و Swipe معکوس را بررسی می‌کنیم. چیزی پیدا نمی‌شود.
  3. Swipe شخص A روی شخص B را ذخیره می‌کنیم.
  4. Swipe شخص B روی شخص A را ذخیره می‌کنیم.

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

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

چون باید فوراً به آخرین Swipe کننده تطابق را اطلاع دهیم، باید سازگاری سیستم را تضمین کنیم. چند رویکرد برای این کار داریم.

رویکرد بد: Polling دوره‌ای پایگاه داده

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

چالش‌ها: این رویکرد به‌خاطر فاصله بین Poll ها Latency ایجاد می‌کند، یعنی کاربران بعد از Swipe بازخورد فوری نمی‌گیرند. نبود رضایت فوری می‌تواند تعامل کاربر را به‌طور محسوسی کم کند، چون حس خوب لحظه‌ای اعلان تطابق بخش حیاتی تجربه کاربری است. علاوه بر این، Poll مکرر بار غیرضروری روی پایگاه داده می‌گذارد و مشکل مقیاس‌پذیری می‌سازد.

رویکرد خوب: تراکنش در Cassandra

اگر به سازگاری نیاز داریم، ذهن ما باید فوراً به تراکنش‌های پایگاه داده برود. می‌توانیم مطمئن شویم هم ذخیره Swipe و هم بررسی Swipe متقابل در یک تراکنش رخ می‌دهد، طوری که یا هر دو موفق ذخیره شوند یا هیچ‌کدام.

Cassandra پشتیبانی پایه‌ای از چیزی به نام «تراکنش‌های سبک» (Lightweight Transactions یا LWT) دارد، اما قدرتشان مثل تراکنش‌های واقعی ACID نیست. LWT ها از پروتکل اجماع Paxos برای فراهم کردن سازگاری خطی (Linearizable) برای عملیات مشخص استفاده می‌کنند، اما فقط روی یک پارتیشن. برخلاف تراکنش‌های ACID واقعی، از اتمیک بودن چندپارتیشنی، سطوح ایزوله‌سازی (Isolation) یا Rollback پشتیبانی نمی‌کنند. همچنین سربار عملکردی قابل توجهی دارند، چون برای رسیدن به اجماع به رفت‌وبرگشت‌های متعدد بین گره‌ها نیاز دارند. به همین دلیل برای به‌روزرسانی‌های شرطی ساده مناسب‌اند، نه جریان‌های تراکنشی پیچیده.

چالش‌ها: چالش اصلی به مسئله مقیاس تبدیل می‌شود. با 20 میلیون کاربر فعال روزانه و 100 Swipe در روز، یعنی 2 میلیارد Swipe در روز! هیچ راهی نیست که همه این‌ها در یک پارتیشن جا شوند. یعنی تراکنش‌ها باید چند پارتیشن را شامل شوند، که LWT ها پشتیبانی نمی‌کنند. در بررسی عمیق بعدی بحث می‌کنیم که چطور با تضمین اینکه Swipe های متقابل همیشه در یک پارتیشن باشند، این مشکل را حل کنیم.

رویکرد خوب: تراکنش تک‌پارتیشنی با کلید جفت کاربر

می‌توانیم از تراکنش‌های تک‌پارتیشنی Cassandra برای مدیریت اتمی Swipe ها استفاده کنیم. نکته کلیدی این است که همه Swipe های بین دو کاربر در یک پارتیشن ذخیره شوند.

اول جدولی با کلید اصلی مرکب (Composite Primary Key) می‌سازیم تا Swipe های بین یک جفت کاربر در یک پارتیشن بمانند:

1
2
3
4
5
6
7
8
CREATE TABLE swipes (
    user_pair text,      -- partition key: smaller_id:larger_id
    from_user uuid,      -- clustering key
    to_user uuid,        -- clustering key
    direction text,
    created_at timestamp,
    PRIMARY KEY ((user_pair), from_user, to_user)
);

وقتی کاربر Swipe می‌کند، کلید user_pair را با مرتب‌کردن شناسه‌ها می‌سازیم تا همیشه یکسان باشد:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def get_user_pair(user_a, user_b):
    # Sort IDs so (A->B) and (B->A) are in same partition
    sorted_ids = sorted([user_a, user_b])
    return f"{sorted_ids[0]}:{sorted_ids[1]}"

def handle_swipe(from_user, to_user, direction):
    user_pair = get_user_pair(from_user, to_user)

    # Both operations happen atomically in same partition
    batch = """
    BEGIN BATCH
        INSERT INTO swipes (user_pair, from_user, to_user, direction, created_at)
        VALUES (?, ?, ?, ?, ?);

        SELECT direction FROM swipes
        WHERE user_pair = ?
        AND from_user = ?
        AND to_user = ?;
    APPLY BATCH;
    """

این رویکرد مؤثر است، چون تراکنش‌های تک‌پارتیشنی Cassandra تضمین اتمی لازم را می‌دهند. با اطمینان از اینکه همه Swipe های بین دو کاربر در یک پارتیشن ذخیره می‌شوند، می‌توانیم تطابق را بدون نگرانی از پیچیدگی تراکنش توزیع‌شده به‌صورت اتمی بررسی کنیم. طراحی کلید پارتیشن نیاز به عملیات بین‌پارتیشنی را از بین می‌برد و راه‌حل را هم کارآمد و هم قابل اعتماد می‌کند.

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

رویکرد عالی: عملیات اتمی با Redis

Redis برای برآورده‌کردن نیازهای سازگاری منطق تطبیق Swipe مناسب‌تر است. Cassandra در دوام (Durability) و ذخیره حجم زیاد داده عالی است، اما برای عملیات اتمی که برای تشخیص تطابق در لحظه لازم داریم طراحی نشده است. به‌جای آن، می‌توانیم برای عملیات اتمی Swipe از Redis استفاده کنیم و Cassandra را به‌عنوان لایه ذخیره‌سازی ماندگار نگه داریم.

بینش کلیدی مثل قبل است: Swipe های بین دو کاربر یکسان باید روی یک Shard فرود بیایند. این را با ساختن کلیدهایی که شناسه هر دو کاربر را به‌شکل ثابت ترکیب می‌کنند، به دست می‌آوریم. ساختار کلید و مقدار این‌طور است:

1
2
3
4
5
Key: "swipes:123:456"
Value: {
    "123_swipe": "right", // or yes/no
    "456_swipe": "left"   // or yes/no
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def get_key(user_a, user_b):
    # Sort IDs so (A->B) and (B->A) map to same key
    sorted_ids = sorted([user_a, user_b])
    return f"swipes:{sorted_ids[0]}:{sorted_ids[1]}"

def handle_swipe(from_user, to_user, direction):
    key = get_key(from_user, to_user)

    # Use Redis hash to store both users' swipes
    # Each hash has two fields: user1_swipe and user2_swipe
    script = """
    redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
    return redis.call('HGET', KEYS[1], ARGV[3])
    """

    # Execute atomically using Lua script
    other_swipe = redis.eval(
        script,
        keys=[key],
        args=[
            f"{from_user}_swipe",  # field to set
            direction,             # our swipe
            f"{to_user}_swipe"     # field to check
        ]
    )

    # If other user swiped right too, it's a match!
    if direction == 'right' and other_swipe == 'right':
        create_match(from_user, to_user)

با استفاده از عملیات اتمی Redis و اسکریپت‌های Lua، می‌توانیم مطمئن شویم ثبت Swipe و بررسی تطابق به‌صورت یک عملیات واحد انجام می‌شود. این سازگاری لازم را می‌دهد و چون Redis درون‌حافظه‌ای است، Latency پایین هم حفظ می‌شود. سیستم افقی مقیاس می‌گیرد، چون می‌توانیم گره‌های Redis بیشتری اضافه کنیم و با Consistent Hashing (هشینگ پایدار) تضمین می‌کنیم Swipe های مرتبط کنار هم می‌مانند.

چالش‌ها: چالش اصلی این رویکرد، مدیریت مؤثر کلاستر Redis است. Redis برای عملیات اتمی عملکرد عالی دارد، اما باید خرابی گره‌ها و بالانس مجدد حلقه Consistent Hashing را با دقت مدیریت کنیم. با این حال، این چالش‌های عملیاتی از تلاش برای رسیدن به سازگاری در Cassandra قابل مدیریت‌ترند.

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

این رویکرد ترکیبی مزیت هر دو را دارد: سازگاری قوی و عملیات اتمی Redis برای تشخیص لحظه‌ای تطابق، همراه با دوام و ظرفیت ذخیره‌سازی Cassandra برای داده‌های تاریخی. سیستم بسیار در دسترس و مقیاس‌پذیر می‌ماند و نیازمندی اصلی ما یعنی تشخیص فوری و سازگار تطابق را برآورده می‌کند.

2) تولید فید و پشته با Latency کم

وقتی کاربر اپلیکیشن را باز می‌کند، می‌خواهد فوراً Swipe کردن را شروع کند. نمی‌خواهد منتظر بماند تا ما فید را بسازیم.

همان‌طور که در طراحی سطح بالا گفتیم، طراحی فعلی باعث می‌شود هر بار که دسته جدیدی از کاربران لازم است، یک جست‌وجوی کند اجرا کنیم:

1
2
3
4
5
SELECT * FROM users
WHERE age BETWEEN 18 AND 35
AND interestedIn = 'female'
AND lat BETWEEN userLat - maxDistance AND userLat + maxDistance
AND long BETWEEN userLong - maxDistance AND userLong + maxDistance

این قطعاً نیازمندی غیرعملکردی ما یعنی تولید پشته با Latency کم را برآورده نمی‌کند. ببینیم چه کارهای دیگری می‌توانیم بکنیم.

رویکرد خوب: پایگاه داده ایندکس‌شده برای کوئری بلادرنگ

یکی از راه‌های رسیدن به Latency کم، استفاده از پایگاه داده‌های ایندکس‌شده برای کوئری بلادرنگ است. با ساختن ایندکس روی فیلدهایی که معمولاً در تولید فید استفاده می‌شوند، مثل ترجیحات کاربر، بازه سنی و به‌ویژه داده‌های مکانی، می‌توانیم زمان پاسخ کوئری را به‌طور قابل توجهی بهتر کنیم. پیاده‌سازی ایندکس جغرافیایی (Geospatial Index) به سیستم اجازه می‌دهد کاربران یک منطقه جغرافیایی مشخص را کارآمد بازیابی کند.

برای تأمین نیازهای مقیاس و عملکرد اپلیکیشنی مثل Tinder می‌توان از پایگاه داده بهینه برای جست‌وجو مثل Elasticsearch یا OpenSearch استفاده کرد. این پایگاه‌های داده برای جست‌وجوی سریع، متن کامل و کوئری پیچیده طراحی شده‌اند و برای مدیریت حجم زیاد داده با حداقل Latency مناسب‌اند. با بهره‌گیری از قابلیت‌های قدرتمند ایندکس‌گذاری و کوئری آن‌ها، می‌توانیم فید کاربران را بلادرنگ بسازیم و زمان پاسخ را پایین نگه داریم. این رویکرد تضمین می‌کند کاربران پیشنهادهای به‌روزی بگیرند که دقیقاً با ترجیحات و موقعیت فعلی‌شان می‌خواند.

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

این مشکل را می‌توان با مکانیزم Change Data Capture (CDC) حل کرد که پایگاه داده ایندکس‌شده را با پایگاه داده تراکنشی اصلی همگام نگه می‌دارد. بسته به حجم به‌روزرسانی‌ها، ممکن است بخواهیم از استراتژی دسته‌ای (Batch) استفاده کنیم تا تعداد نوشتن‌ها در پایگاه داده ایندکس‌شده کم شود، چون Elasticsearch برای بارکاری خواندن‌محور بهینه شده، نه نوشتن‌محور.

رویکرد خوب: پیش‌محاسبه و کش فید

استراتژی دیگر این است که فید کاربران را به‌صورت ناهمگام (Async) از پیش محاسبه و کش کنیم. کارهای پس‌زمینه دوره‌ای می‌توانند فیدها را بر اساس ترجیحات و موقعیت کاربران بسازند و وقتی کاربر اپلیکیشن را باز می‌کند، برای بازیابی فوری در کش باشند. این تضمین می‌کند فید بدون نیاز به محاسبه بلادرنگ، آماده است.

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

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

رویکرد عالی: ترکیب پیش‌محاسبه و پایگاه داده ایندکس‌شده

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

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

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

با ترکیب این دو روش، Latency کم را در کل جلسه کاربر حفظ می‌کنیم. فید کش‌شده اولیه دسترسی فوری می‌دهد و پایگاه داده ایندکس‌شده تضمین می‌کند حتی فعال‌ترین کاربران هم پیشنهادهای جدید و مرتبط را بدون تأخیر قابل توجه می‌گیرند.

همچنین می‌توانیم وقتی چند پروفایل مانده به پایان پشته رسیده است، به‌روزرسانی پشته را فعال کنیم. به این ترتیب از دید کاربر پشته بی‌نهایت به نظر می‌رسد.

جلوگیری از فیدهای کهنه (Stale Feeds)

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

ذخیره فید کاربران می‌تواند به پیشنهاد پروفایل‌های «کهنه» برسد. پروفایل کهنه پروفایلی است که دیگر با معیارهای فیلتر کاربر نمی‌خواند. چند نمونه از حالت‌هایی که پروفایل در فید کهنه می‌شود:

  • کاربری که در فید پیشنهاد شده، موقعیتش را تغییر داده و دیگر به‌اندازه کافی به معیارهای فیلتر فید نزدیک نیست.
  • کاربری که در فید پیشنهاد شده، پروفایلش را تغییر داده (مثلاً علایقش عوض شده) و دیگر با معیارهای فیلتر فید نمی‌خواند.

این‌ها مشکلات واقعی‌اند و اگر کاربر پروفایلی ببیند که با فیلترهای ترجیحی‌اش نمی‌خواند، ممکن است تجربه کاربری بدی (UX) بسازد. برای حل این مسئله می‌توانیم برای فیدهای کش‌شده TTL سخت‌گیرانه (کمتر از 1 ساعت) در نظر بگیریم و فید را با یک Background Job زمان‌بندی‌شده دوباره محاسبه کنیم. همچنین می‌توانیم فید را فقط برای کاربران واقعاً فعال از پیش محاسبه کنیم، نه برای همه. انجام کار اولیه برای فید کاربر چند بار در روز در مقیاس بالا گران است، پس می‌توانیم کش‌ها را فقط برای کاربرانی «گرم» کنیم که می‌دانیم احتمالاً از پروفایل‌های کش‌شده استفاده می‌کنند. یکی از مزایای این رویکرد این است که پارامترهای قابل تنظیم زیادی دارد: TTL پروفایل‌های کش‌شده، تعداد پروفایل‌های کش‌شده، مجموعه کاربرانی که برایشان فید کش‌شده نگه می‌داریم و موارد مشابه.

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

بعضی اقدام‌های خود کاربر هم ممکن است به پروفایل‌های کهنه در فید برسد:

  • کاربری که فید به او ارائه می‌شود، معیارهای فیلتر خودش را تغییر می‌دهد و در نتیجه پروفایل‌های فید کش‌شده کهنه می‌شوند.
  • کاربری که فید به او ارائه می‌شود، موقعیتش را به‌طور قابل توجهی تغییر می‌دهد (مثلاً به محله یا شهر دیگری می‌رود) و در نتیجه پروفایل‌های فید کش‌شده کهنه می‌شوند.

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

3) جلوگیری از نمایش دوباره پروفایل‌هایی که قبلاً Swipe شده‌اند

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

رویکرد خوب: بررسی Contains در پایگاه داده Swipe

با طراحی قبلی، می‌توانیم سرویس سازنده فید را طوری بسازیم که از Swipe DB کوئری بگیرد و برای فیلتر کردن کاربرانی که قبلاً روی آن‌ها Swipe شده، یک بررسی «Contains» انجام دهد. کوئری گرفتن همه پروفایل‌های Swipe شده کارآمد است، چون بر اساس swiping_user_id به پارتیشن درست هدایت می‌شود.

چالش‌ها: این رویکرد دو چالش دارد:

  • اگر با سیستمی سروکار داریم که Availability را بر Consistency مقدم می‌داند، ممکن است تا زمان تولید فید، همه داده Swipe در همه Replica های یک پارتیشن نرسیده باشد. یعنی در معرض خطر از دست‌دادن Swipe ها و نمایش دوباره پروفایل‌ها هستیم.
  • اگر کاربری تاریخچه Swipe گسترده داشته باشد، ممکن است تعداد زیادی شناسه کاربر برگردد و بررسی Contains به‌تدریج گران‌تر شود.

رویکرد خوب: کش Swipe های اخیر در کلاینت

با رویکرد قبلی، می‌توانیم کوئری سمت باطن را نگه داریم و یک کش از Swipe های اخیر کاربر هم اضافه کنیم تا مشکل ناشی از Eventual Consistency را کم کنیم. اما این کش را در باطن مدیریت نمی‌کنیم، بلکه در سمت کلاینت نگه می‌داریم.

نگهداری کش در باطن فقط برای ذخیره داده پیش از رسیدن آن به همه پارتیشن‌های سیستم NoSQL گران است. می‌توانیم از این واقعیت استفاده کنیم که کلاینت هم بخشی از سیستم است و از کلاینت بخواهیم داده Swipe های اخیر را نگه دارد (مثلاً K تا از جدیدترین Swipe ها). این به کلاینت اجازه می‌دهد پروفایل‌هایی را که اخیراً Swipe شده‌اند از فید پیشنهادی فیلتر کند.

این کش در شرایطی که کاربر نزدیک است پشته اولیه‌اش را تمام کند هم کاربرد دوگانه دارد. تصور کنید کاربر در حال Swipe کردن روی 200 پروفایل از پیش واکشی‌شده است. وقتی کاربر به پروفایل حدود 150 می‌رسد، کلاینت می‌تواند:

  1. باطن را صدا بزند تا فید جدیدی برای کاربر بسازد.
  2. بعد از آماده‌شدن فید در باطن، آن فید را بگیرد.
  3. همه پروفایل‌هایی را که کاربر در این فاصله Swipe کرده فیلتر کند.

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

چالش‌ها: این راه‌حل هنوز در برابر مشکل کاربرانی با تاریخچه Swipe گسترده آسیب‌پذیر است و بررسی‌های Contains روی فهرست‌های بزرگ شناسه، با Swipe بیشتر کاربر کندتر می‌شوند.

رویکرد عالی: Bloom Filter

این رویکرد کمی «بیش‌ازحد مهندسی‌شده» است، اما یک کاربرد موجه برای Bloom Filter برای پشتیبانی از ساخت فید برای کاربرانی با تاریخچه Swipe بزرگ است. می‌توانیم روی رویکرد قبلی بنا کنیم. برای کاربرانی که تاریخچه Swipe زیاد دارند، می‌توانیم یک Bloom Filter نگه داریم. اگر تاریخچه Swipe کاربر از اندازه مشخصی بیشتر شود (اندازه‌ای که نگهداری در کش را سخت می‌کند یا بررسی‌های Contains در کوئری را کند می‌کند)، می‌توانیم برای آن کاربر یک Bloom Filter بسازیم، در کش نگه داریم و در فرایند فیلتر کردن به کار ببریم.

Bloom Filter ساختار داده احتمالاتی و کم‌حجمی است که برای بررسی عضویت یک عنصر در یک مجموعه به کار می‌رود. گاهی نتیجه مثبت کاذب (False Positive) می‌دهد، یعنی ممکن است فرض کنیم کاربر روی پروفایلی Swipe کرده، در حالی که نکرده است. اما هرگز منفی کاذب (False Negative) نمی‌دهد، یعنی هرگز نمی‌گوید کاربر روی پروفایلی Swipe نکرده، در حالی که واقعاً کرده است [web:3]. این یعنی با موفقیت از نمایش دوباره پروفایل‌ها جلوگیری می‌کنیم، اما ممکن است تعداد کمی پروفایل به‌خاطر مثبت کاذب هرگز به کاربر نشان داده نشوند. Bloom Filter ها درصد خطای قابل تنظیم دارند که معمولاً به میزان فضایی که اشغال می‌کنند بستگی دارد. پس می‌توان آن را طوری تنظیم کرد که مثبت کاذب کم، مصرف فضای مناسب و فیلتر سریع پروفایل‌ها هنگام ساخت فید داشته باشیم.

چالش‌ها: چالش اصلی، مدیریت کش Bloom Filter است. اگر کش از کار بیفتد، باید دوباره بازسازی شود. ساخت دوباره Bloom Filter از داده Swipe آسان است، اما اگر گره از کار بیفتد، در مقیاس بالا گران خواهد بود.

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

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

سطح Mid-level

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

سطح Senior

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

سطح Staff به بالا

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