پست

طراحی سرویس تحویل محلی مانند Gopuff

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

طراحی سرویس تحویل محلی مانند Gopuff

درک مسئله

Gopuff کالاهایی را که معمولاً در فروشگاه‌های رفاه (Convenience Store) پیدا می‌شوند، با تحویل سریع به مشتری می‌رساند. این کار از طریق بیش از 500 مرکز توزیع خرد (Distribution Center یا DC) انجام می‌شود.

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

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

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

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

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

  • پرداخت و خرید
  • مدیریت مسیریابی و تحویل توسط راننده
  • قابلیت جست‌وجو و API های کاتالوگ (تمرکز سیستم روی موجودی و سفارش است)
  • لغو سفارش و بازگشت کالا

در این مسئله تأکید بر جمع‌آوری موجودی از DC های محلی و ثبت سفارش بدون رزرو مضاعف (Double Booking) است. در مسائل دیگر ممکن است کاتالوگ محصول، جست‌وجو و موارد مشابه اهمیت بیشتری داشته باشند.

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

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

  • درخواست‌های موجودی باید سریع باشند (کمتر از 100 میلی‌ثانیه) تا از کاربردهایی مثل جست‌وجو پشتیبانی کنند.
  • ثبت سفارش باید کاملاً سازگار (Consistent) باشد. دو مشتری نباید بتوانند یک کالای فیزیکی واحد را بخرند.
  • سیستم باید از 10 هزار DC و 100 هزار کالا در کاتالوگ پشتیبانی کند.
  • حجم سفارش‌ها حدود 10 میلیون سفارش در روز است.

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

  • حریم خصوصی و امنیت
  • بازیابی از بحران (Disaster Recovery)

موارد خارج از محدوده معمولاً از سؤال‌هایی مثل «آیا باید حریم خصوصی را هم مدیریت کنیم؟» به دست می‌آیند. مصاحبه‌کننده‌ها معمولاً با جمله‌ای مثل «برای شروع، حریم خصوصی را خارج از محدوده می‌گذارم» مشکلی ندارند و اگر لازم باشد، شما را اصلاح می‌کنند.

آماده‌سازی

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

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

برای این کار «اسم‌های» سیستم را فهرست می‌کنیم، API می‌سازیم و بعد شروع می‌کنیم به کشیدن جعبه‌ها.

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

موجودیت‌های اصلی (Core Entities) همان «اسم‌های» سطح بالای سیستم‌اند. هنوز مدل داده کامل طراحی نمی‌کنیم، چون برای آن باید درک بهتری از هماهنگی اجزای سیستم داشته باشیم. اما با مشخص‌شدن موجودیت‌ها، بلوک‌های سازنده بقیه سیستم را داریم. این موضوع برای طراحی یک REST API خوب اهمیت ویژه دارد، چون منابع (Resources) در REST محور اصلی‌اند و تقریباً با موجودیت‌های ما یکی هستند.

نکته مهم این مسئله، تفاوت بین Item و Inventory است. می‌توانید آن را مثل تفاوت کلاس و نمونه (Instance) در برنامه‌نویسی شیءگرا ببینید. مصرف‌کنندگان API به آیتم‌هایی که در کاتالوگ می‌بینند اهمیت می‌دهند (مثلاً Cheetos)، ولی ما باید پیگیری کنیم که کالاهای فیزیکی واقعاً کجا هستند. Inventory یعنی یک کالای فیزیکی در یک مکان مشخص.

بقیه موجودیت‌ها روشن‌ترند. به DistributionCenter نیاز داریم تا بدانیم موجودی فیزیکی کجاست، و به Order نیاز داریم تا کالاهای سفارش‌داده‌شده را پیگیری کنیم.

  • Inventory (موجودی): یک نمونه فیزیکی از یک آیتم که در یک DC قرار دارد. برای محاسبه مقدار موجود یک آیتم برای کاربر مشخص، موجودی‌ها را جمع می‌زنیم.
  • Item (آیتم): یک نوع کالا، مثلاً Cheetos. مشتری‌ها در واقع به همین اهمیت می‌دهند.
  • DistributionCenter (مرکز توزیع): مکان فیزیکی نگهداری کالاها. با آن تعیین می‌کنیم کدام آیتم‌ها برای کاربر در دسترس‌اند. موجودی در DC ها نگهداری می‌شود.
  • Order (سفارش): مجموعه‌ای از موجودی که کاربر سفارش داده، به‌همراه اطلاعات ارسال و صورتحساب.

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

تعریف API

بعد از موجودیت‌ها سراغ API ها می‌رویم که باید دقیقاً با نیازمندی‌های عملکردی مطابقت داشته باشند. جزئیات غیرضروری زیادی می‌توان وارد کرد، اما برای اینکه زمان مصاحبه هدر نرود (اشتباهی که در عمل زیاد دیده می‌شود)، ساده شروع می‌کنیم.

فقط به دو API نیاز داریم. اولی با گرفتن موقعیت مکانی (و شاید یک کلمه کلیدی) موجودی آیتم‌ها را برمی‌گرداند. دومی ثبت سفارش را انجام می‌دهد. برای API موجودی، صفحه‌بندی (Pagination) هم می‌گذاریم تا مشتری با داده‌ای بیشتر از نیازش مواجه نشود.

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

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

1) بررسی موجودی کالا توسط مشتری

اولین نیازمندی این است که مشتری بتواند بر اساس مکان، موجودی را بپرسد. دو مرحله داریم.

مرحله اول، پیدا کردن DC هایی است که به اندازه کافی نزدیک‌اند تا تحویل در یک ساعت ممکن باشد. همه موجودی در DC ها قرار دارد، پس می‌توانیم برای بررسی همه موارد سیستم میان‌بر بزنیم. مرحله دوم این است که با فهرست DC های قابل‌سرویس، موجودی آن‌ها را بررسی کنیم و اجتماع آن‌ها را به کاربر برگردانیم. هر مرحله باید نسبتاً سریع باشد، چون Latency (تأخیر) انتها به انتها باید زیر 100 میلی‌ثانیه بماند.

برای یافتن DC های نزدیک می‌توانیم یک API داخلی ساده بسازیم که عرض و طول جغرافیایی (lat/long) را می‌گیرد و فهرست DC هایی را برمی‌گرداند که در بازه یک ساعت قرار دارند. فرض کنیم جدولی از DC ها همراه با مختصاتشان داریم. فاصله ورودی کاربر را با ریاضی ساده اندازه می‌گیریم. ساده‌ترین نسخه از فاصله اقلیدسی استفاده می‌کند و نسخه پیچیده‌تر از فرمول هاورساین (Haversine) برای در نظر گرفتن انحنای زمین. با یک آستانه ساده، DC های داخل فاصله X (به خط مستقیم) را می‌گیریم. این کار نیازمندی عملکردی را کاملاً برآورده نمی‌کند و در بررسی عمیق به آن برمی‌گردیم.

در مرحله بعد باید موجودی DC های پیداشده را بررسی کنیم. این کار با کوئری روی جدول Inventory و جدول Items انجام می‌شود. فرض کنیم از پایگاه داده Postgres برای این جدول‌ها استفاده می‌کنیم. با آن می‌توانیم جدول موجودی را با جدول آیتم‌ها Join کنیم تا نام و توضیح کالا را همراه مقدار موجودی برگردانیم.

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

تا اینجا این اجزا را داریم:

  • سرویس Availability (موجودی): درخواست‌های کاربر برای موجودی در یک مکان مشخص را پردازش می‌کند.
  • سرویس Nearby (نزدیک): با پایگاه داده DC های اطراف همگام است و برای محاسبه زمان سفر از DC ها (که ممکن است ترافیک را هم در نظر بگیرد) از یک «سرویس زمان سفر» خارجی استفاده می‌کند.
  • جدول Inventory: یک جدول SQL که مقدار موجودی هر آیتم در هر DC را نگه می‌دارد.

وقتی کاربر برای کالاهای A و B و C از موقعیت (X, Y) درخواست موجودی می‌دهد، این مراحل انجام می‌شود:

  1. درخواست همراه با موقعیت X و Y و فیلترهای مربوط به سرویس Availability می‌رسد.
  2. سرویس Availability درخواستی با همان موقعیت به سرویس Nearby می‌فرستد.
  3. سرویس Nearby فهرست DC هایی را که می‌توانند به این مکان تحویل بدهند برمی‌گرداند.
  4. سرویس Availability با شناسه آن DC ها از پایگاه داده موجودی را می‌پرسد.
  5. نتایج را جمع می‌زند و به مشتری برمی‌گرداند.

عالی است. نیازمندی موجودی ما (تا حد زیادی) برآورده شد.

2) سفارش کالا توسط مشتری

آخرین نیازمندی ثبت سفارش است. برای آن به Strong Consistency (سازگاری قوی) نیاز داریم تا دو کاربر یک کالا را سفارش ندهند. باید موجودی را بررسی کنیم، سفارش را ثبت کنیم و موجودی را به‌روز کنیم و هر سه کار به‌صورت اتمی (Atomic) انجام شوند.

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

ایده جلوگیری از «رزرو مضاعف» در مسائل طراحی سیستم رایج است. برای اینکه دو کاربر نتوانند یک موجودی را سفارش بدهند، به نوعی قفل (Locking) نیاز داریم. یعنی موجودی را هنگام بررسی و ثبت سفارش «قفل» می‌کنیم، طوری که در هر لحظه فقط یک کاربر قفل را داشته باشد.

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

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

این راه خوب است، چون برای هر کاربرد بهترین ذخیره‌ساز داده را به کار می‌گیریم. مثلاً یک Key-Value Store برای موجودی و یک پایگاه داده رابطه‌ای برای سفارش‌ها. اما…

چالش‌ها: این روش چند حالت خرابی ناخوشایند دارد.

  • اگر سرویس بعد از ساخت سفارش و قبل از کم‌کردن موجودی از کار بیفتد، چه می‌شود؟ کاربر بعدی ممکن است همان موجودی‌ای را سفارش بدهد که به کاربر اول قول داده بودیم. باید این خرابی‌ها را پاکسازی و برگشت‌پذیر کنیم.
  • اگر دو سفارش نیازمندی‌های موجودی هم‌پوشان داشته باشند چه؟ فرض کنید User1 و User2 هر دو می‌خواهند A و B را بخرند. ممکن است به بن‌بست (Deadlock) برسیم، به این صورت که User1 قفل A را دارد و User2 قفل B را، و هیچ‌کدام نمی‌توانند ادامه بدهند.

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

رویکرد عالی: تراکنش واحد در Postgres

با قرار دادن سفارش و موجودی در یک پایگاه داده، می‌توانیم از ویژگی‌های ACID در Postgres استفاده کنیم. با یک تراکنش واحد و سطح جداسازی Serializable می‌توانیم مطمئن شویم کل تراکنش اتمی است. یعنی اگر دو کاربر هم‌زمان سعی کنند یک کالا را سفارش بدهند، یکی از آن‌ها رد می‌شود. دلیلش این است که اگر موجودی کافی نباشد، تراکنش Commit نمی‌شود.

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

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

با انتخاب گزینه «عالی» و تکیه بر همان Postgres، سیستم ساده می‌ماند و نیازمندی‌ها هم برآورده می‌شوند. جریان ثبت سفارش این‌طور است:

  1. کاربر از سرویس Orders درخواست ثبت سفارش برای کالاهای A و B و C را می‌دهد.
  2. سرویس Orders یک تراکنش واحد می‌سازد و آن را به Leader پایگاه داده Postgres می‌فرستد. این تراکنش: (الف) بررسی می‌کند موجودی A و B و C بیشتر از صفر باشد. (ب) اگر هر آیتمی موجود نباشد، تراکنش شکست می‌خورد. (ج) اگر همه موجود باشند، سفارش را ثبت می‌کند و وضعیت موجودی A و B و C را به «سفارش‌داده‌شده» (ordered) تغییر می‌دهد. (د) یک ردیف جدید در جدول Orders و جدول OrderItems می‌سازد که سفارش A و B و C را ثبت می‌کند. (ه) تراکنش Commit می‌شود.
  3. اگر تراکنش موفق شد، سفارش به کاربر برگردانده می‌شود.

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

جمع‌بندی راه‌حل اولیه

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

یک پایگاه داده Postgres برای موجودی و سفارش‌ها داریم که بر اساس منطقه پارتیشن‌بندی شده است. سرویس Availability از Read Replica ها می‌خواند و سرویس Orders با تراکنش‌های اتمی روی Leader می‌نویسد تا از نوشتن مضاعف جلوگیری شود. این یک پایه عالی است.

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

1) جست‌وجوی موجودی با در نظر گرفتن ترافیک و زمان رانندگی

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

رویکرد بد: فاصله ساده در SQL

همه مختصات DC ها را در یک جدول می‌گذاریم و فاصله ورودی کاربر را با ریاضی ساده می‌سنجیم. ساده‌ترین نسخه از فاصله اقلیدسی استفاده می‌کند و نسخه پیچیده‌تر از فرمول هاورساین. با یک آستانه ساده، DC هایی را که در فاصله X (خط مستقیم) هستند به دست می‌آوریم.

چالش‌ها: این رویکرد بسیار ساده است و ترافیک و شرایط جاده را در نظر نمی‌گیرد. همچنین این واقعیت را نادیده می‌گیرد که ممکن است چند DC در یک شهر داشته باشیم.

رویکرد خوب: سرویس تخمین زمان سفر

DC ها به‌ندرت تغییر می‌کنند (ساختمان‌اند!). پس می‌توانیم به‌صورت دوره‌ای (مثلاً هر 5 دقیقه) جدول DC ها را در حافظه سرویس خودمان همگام کنیم. سپس با یک سرویس تخمین زمان سفر، زمان رسیدن از مکان ورودی به هر DC را با پیمایش همه آن‌ها به دست می‌آوریم.

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

رویکرد عالی: فیلتر کردن با شعاع ثابت، سپس زمان سفر

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

2) سریع و مقیاس‌پذیر کردن جست‌وجوی موجودی

اکنون وقتی DC های نزدیک را داریم، برای جست‌وجوی موجودی مستقیم سراغ پایگاه داده می‌رویم. این کار فشار زیادی به پایگاه داده می‌آورد. بیایید با چند برآورد ببینیم چه Throughput (نرخ گذردهی) را باید انتظار داشته باشیم.

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

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

1
Queries: 10M orders/day / (100k seconds/day) * 10 / 0.05 = 20k queries/second

20 هزار کوئری در ثانیه رقم قابل توجهی است و باید به مقیاس‌دهی آن فکر کنیم. چه کار کنیم؟

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

رویکرد خوب: کش با Redis

یک نمونه Redis به سیستم اضافه می‌کنیم. سرویس Availability ابتدا کش را برای مجموعه ورودی‌ها می‌پرسد و اگر Cache Hit شد، همان نتیجه را برمی‌گرداند. اگر Cache Miss شد، از پایگاه داده زیرین می‌خواند و نتیجه را در کش می‌نویسد. TTL کوتاه (مثلاً 1 دقیقه) تازه‌بودن نتایج را تضمین می‌کند.

چالش‌ها: باید مطمئن شویم کش همیشه با جدول موجودی همگام است. وقتی سرویس Orders در جدول Inventory می‌نویسد، باید ورودی‌های کش تحت‌تأثیر را منقضی (Invalidate) کند.

رویکرد عالی: Read Replica و پارتیشن‌بندی

چون موجودی را فقط از مجموعه‌ای از DC های نزدیک می‌خوانیم، می‌توانیم آن‌ها را با یک شناسه منطقه (Region ID) گروه‌بندی کنیم که از سه رقم اول کد پستی ساخته می‌شود. سپس موجودی را بر اساس همین شناسه پارتیشن‌بندی می‌کنیم. بنابراین بیشتر کوئری‌ها به‌جای کل داده موجودی، به یک یا دو پارتیشن می‌روند.

برای موجودی می‌توانیم از Read Replica هم استفاده کنیم، چون مقداری ناسازگاری قابل تحمل است. سفارش‌ها باید کاملاً سازگار باشند، پس تراکنش آن‌ها به Leader در Postgres می‌رود، ولی کوئری‌های موجودی می‌توانند به Read Replica ها بروند.

چالش‌ها: باید اندازه Replica ها را مدیریت کنیم تا با ترافیک متعادل باشد. پارتیشن‌بندی داده را هم باید طوری مدیریت کنیم که هیچ Replica ای بیش از حد زیر بار نرود.

با انتخاب راه‌حل‌های «عالی» برای هر دو چالش به طراحی نهایی می‌رسیم: کش Redis برای موجودی، پارتیشن‌بندی موجودی بر اساس منطقه، Read Replica برای کوئری‌های موجودی و Leader برای تراکنش‌های سفارش.

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

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

سطح Mid-level

  • عرض در برابر عمق: تمرکز بیشتر روی عرض است، حدود 80 درصد عرض و 20 درصد عمق. باید یک طراحی سطح بالا بسازید که نیازمندی‌های عملکردی تعریف‌شده را پوشش بدهد. بهینه‌بودن راه‌حل در اولویت نیست.
  • مرور مبانی: مصاحبه‌کننده مدتی را صرف بررسی مبانی می‌کند تا مطمئن شود می‌دانید هر جزء چه کاری می‌کند. مثلاً اگر از DynamoDB استفاده کنید، انتظار داشته باشید درباره Index های موجود از شما بپرسد. او هیچ چیز را بدیهی فرض نمی‌کند.
  • ترکیب هدایت و همراهی: در مراحل اولیه باید خودتان مسیر را هدایت کنید، اما مصاحبه‌کننده انتظار ندارد مشکلات طراحی را با دقت بالا پیش‌دستانه تشخیص بدهید. طبیعی است که در مراحل بعد، با بررسی طرح شما، خودش هدایت را به دست بگیرد.
  • معیار برای Gopuff: از داوطلب Mid-level انتظار می‌رود endpoint های API و مدل داده را روشن تعریف کند و هر دو مسیر موجودی و سفارش را بسازد. اگر داوطلب راه‌حل «بد» را انتخاب کند، مصاحبه‌کننده انتظار یک بحث خوب دارد، اما انتظار ندارد که داوطلب بلافاصله به راه‌حل عالی (یا گاهی حتی خوب) برسد.

سطح Senior

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

سطح Staff به بالا

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