وقتی اپلیکیشنهایی مثل اسنپفود را باز میکنید، همهچیز در ظاهر بسیار ساده به نظر میرسد: موقعیت خود را مشخص میکنید، لیست رستورانها یا سوپرمارکتهای اطرافتان را میبینید، غذای خود را انتخاب و سفارش را ثبت میکنید.
اما زیر پوسته این تجربه کاربری روان، یکی از پیچیدهترین چالشهای سیستم دیزاین (System Design) در مقیاس بالا نهفته است. در دنیای واقعی، پیدا کردن «نزدیکترین رستوران» صرفاً یک محاسبه مسافت ساده نیست. سیستم باید در کسری از ثانیه به این سؤال کلیدی پاسخ دهد:
کدام وندورهای واجد شرایط میتوانند در همین لحظه به این کاربر خاص سرویسدهی کنند و کدامیک باید در بالای لیست نمایش داده شوند؟
پاسخ به این سؤال یعنی ترکیب همزمان نقشهها، ایندکسهای جغرافیایی، سیستمهای کشینگ، وضعیت دسترسی لحظهای وندورها، زونهای تحویل، تخمین زمان رسیدن پیک (ETA)، رتبهبندی (Ranking) و ولیدیشن نهایی در زمان پرداخت.
وقتی کاربر اپلیکیشن را باز میکند، کلاینت اطلاعات زیر را به سمت سرور میفرستد:
مختصات جغرافیایی (Lat/Lng)
آدرس انتخابشده
شهر یا منطقه
فیلترهای فعال (مانند دستهبندی، رتبهبندی، بازه قیمتی)
سپس بکاند برای پردازش این درخواست مراحل زیر را طی میکند:
تشخیص محدوده: شهر یا زون تحویل کاربر را مشخص میکند.
یافتن کاندیداها: با استفاده از یک ایندکس جغرافیایی، لیستی از وندورهای نزدیک یا وندورهایی که به آن محدوده سرویس میدهند را پیدا میکند.
فیلتر وضعیت: رستورانهای بسته یا غیرفعال را حذف میکند.
بررسی قابلیت سرویسدهی: بررسی میکند که آیا آدرس دقیق کاربر در پلیگون (Polygon) تحویل هر رستوران قرار دارد یا خیر.
تخمین زمان (ETA): زمان تقریبی آمادهسازی و ارسال را محاسبه میکند.
رتبهبندی: وندورها را بر اساس الگوریتمهای بیزینسی و ترجیحات کاربر مرتب میکند.
صفحهبندی (Pagination): صفحه اول نتایج را به کاربر برمیگرداند.
مهمترین نکته این است که سیستم در هر بار باز شدن اپلیکیشن، کل دیتابیس را اسکن نمیکند؛ بلکه از ایندکسهای جغرافیایی و لیستهای کاندیدای کششده استفاده میکند.
برای پاسخگویی به میلیونها کاربر همزمان، یک ساختار معماری رایج جداسازی دیتابیس لایه پایدار (Durable Data) از لایه جستجوی سریع (Fast Lookup) است.
[ مسیر درخواست پرترافیک کاربر ] │ ▼ ┌────────────────┐ │ API Gateway │ └────────┬───────┘ │ ▼ ┌─────────────────────────────────┐ │ Vendor Discovery Service │ └────┬───────────┬───────────┬────┘ │ │ │ ┌──────────────┘ │ └──────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌───────────┐ ┌───────────┐ │ Redis │ │OpenSearch │ │ PostGIS │ │ (Cache) │ │ (Search) │ │ (Truth) │ └─────────┘ └───────────┘ └───────────┘
PostgreSQL / PostGIS: به عنوان منبع حقیقت (Source of Truth) عمل میکند. دیتای اصلی رستورانها، لوکیشنها و پلیگونهای پیچیده زونهای تحویل در اینجا به طور امن ذخیره میشوند.
Redis: به عنوان یک کش فوقسریع برای ذخیره وضعیت باز/بسته بودن وندورها، لیست کاندیداهای نزدیک و دیتای متادیتای داغ استفاده میشود.
Elasticsearch / OpenSearch: موتور جستجوی متنی و فیلترینگ است. جستجوی دستهبندیها، فیلترهای جغرافیایی اولیه و فرآیند رتبهبندی را پشتیبانی میکند.
Kafka : به محض اینکه دیتای یک وندور تغییر کند (مثلاً منو آپدیت شود یا رستوران موقتاً غیرفعال شود)، رویدادی منتشر میشود تا کشها و ایندکسهای جستجو فوراً بهروزرسانی شوند.
فاصله فیزیکی کوتاهتر همیشه به معنای تجربه کاربری بهتر نیست. این سناریو را در نظر بگیرید:
وندور الف: ۷۰۰ متر فاصله | زمان آمادهسازی: ۳۵ دقیقه | آشپزخانه بسیار شلوغ.
وندور ب: ۱.۸ کیلومتر فاصله | زمان آمادهسازی: ۸ دقیقه | پیکهای آماده به خدمت در اطراف رستوران.
با اینکه وندور ب دورتر است، اما سفارش کاربر را بسیار سریعتر به دستش میرساند. به همین دلیل الگوریتمهای رتبهبندی مدرن، ماتریسی از فاکتورها را در نظر میگیرند:
مسافت فیزیکی و زمان تحویل (ETA)
امتیاز وندور و میزان لغو سفارشهای قبلی
زمان آمادهسازی غذا در آشپزخانه
هزینه ارسال و پروموشنهای فعال
رفتار گذشته کاربر (غذاهای موردعلاقه) و وضعیت تامین پیک در آن لحظه
بنابراین، چالش فنی در پروداکشن، از «مرتبسازی بر اساس مسافت» به «رتبهبندی وندورهای واجد شرایط بر اساس بهینهترین حالت برای کاربر» تغییر میکند.
در پلتفرمهای دلیوری، لوکیشن فیزیکی رستوران تنها بخشی از ماجراست. سوال اصلی این است: آیا این وندور اجازه یا توانایی ارسال به آدرس این کاربر را دارد؟ زونهای تحویل معمولاً داینامیک هستند و بر اساس ساعت روز یا شلوغی و تعداد پیکها تغییر میکنند.
برای حل این چالش در مقیاس بزرگ، سیستمها نقشه را به سلولهای فرضی (مانند سیستم H3 شرکت Uber یا S2 گوگل) تقسیم میکنند. به جای محاسبات سنگین هندسی برای هر کاربر، سیستم رابطهها را از قبل محاسبه و ایندکس میکند:
هنگام باز شدن اپلیکیشن، کار بسیار ساده میشود:
مختصات کاربر به شناسه سلول نقشه تبدیل میشود: user_cell = h3(lat, lng)
لیست کاندیداها مستقیماً از کش خوانده میشود:
candidate_vendors = vendors_serving_cell:user_cell
نکته سیستم دیزاین: سیستم معمولاً سلولهای مجاور را نیز بررسی میکند تا کاربرانی که در مرز یک سلول قرار دارند، رستورانهای سمت دیگر مرز را از دست ندهند.
یک الگوی طراحی قدرتمند این است که شناسه (ID) وندورهای کاندید را بر اساس سلولهای نقشه کش کنیم، اما رتبهبندی نهایی را به ازای هر کاربر شخصیسازی کنیم. با این کار، دیتای پایه بین همه کاربران یک محله مشترک است، اما چیدمان رستورانها بر اساس علایق، اشتراکهای ویژه و تاریخچه سفارشهای هر فرد تغییر میکند.
اجرای محاسبات سنگین هوش مصنوعی یا ماتریسهای پیچیده ETA روی ۱۰۰۰ رستوران برای هر اسکرول کاربر، هزینه زیرساخت را به شدت بالا میبرد و سیستم را کند میکند. برای حل این مشکل، فیلترینگ و رتبهبندی در چند مرحله (Pipeline) انجام میشود:
[ مرحله ۱: لوکاپ جغرافیایی و محدوده ] ──► کاهش از ۱۰۰۰ وندور به ۳۰۰ کاندید اولیه │ ▼ [ مرحله ۲: فیلترهای سبک بیزینسی ] ──► کاهش به ۱۰۰ رستوران باز و واجد شرایط │ ▼ [ مرحله ۳: امتیازدهی اولیه و سبک ] ──► انتخاب ۳۰ وندور برتر بر اساس معیارهای کلی │ ▼ [مرحله ۴: محاسبات سنگین ETA و شخصیسازی] ──► تولید ۲۰ نتیجه نهایی و مرتبشده برای کاربر
این رویکرد گامبهگام به سیستم اجازه میدهد تا دیتای ورودی به بخشهای سنگین الگوریتم را به حداقل برساند.
وضعیت دسترسی به یک رستوران مدام تغییر میکند؛ یک آشپزخانه ممکن است به خاطر اتمام مواد اولیه یا حجم بالای سفارشات، چند دقیقهای منو را ببندد. لایه سرچ و لیست اصلی اپلیکیشن میتواند کمی تاخیر در بهروزرسانی (Eventual Consistency) را تحمل کند، اما صفحه پرداخت (Checkout) هرگز نباید دیتای منقضیشده داشته باشد.
صفحه جستجو و لیستها: اولویت با سرعت بالا و استفاده حداکثری از کش است.
صفحه پرداخت نهایی: اولویت با دقت ۱۰۰٪ است؛ در این مرحله سیستم مستقیماً موجودی منو، قیمت لحظهای، ظرفیت آشپزخانه و زون تحویل را به صورت زنده ولیدیت میکند.
لایههای مختلف داده باید TTLهای متفاوتی داشته باشند:
نوع داده زمان انقضای پیشنهادی (TTL)علت چیدمان تصاویر و لوگوها : چند روز تا چند هفته کاملاً استاتیک هستند و روی CDN کش میشوند.
پروفایل و اطلاعات کلی وندور۵ الی ۳۰ دقیقه : نام و آدرس رستورانها به ندرت تغییر میکند.
خلاصه منو و قیمتها۱ الی ۱۰ دقیقه : تغییرات منو معمولاً چند بار در روز رخ میدهد.
لیست کاندیداهای نزدیک محله۳۰ الی ۳۰۰ ثانیه : بستگی به چگالی وندورها و ترافیک منطقه دارد.
وضعیت باز/بسته بودن لحظهای۱۰ الی ۶۰ ثانیه : برای جلوگیری از نمایش رستورانهایی که تازه سفارشات را بستهاند.
تخمین زمان رسیدن (ETA)۱۰ الی ۶۰ ثانیه : به دلیل نوسانات ترافیکی و تعداد پیکهای فعال در منطقه.
در سیستمهای بزرگ، قطعی یا کندی یک سرویس جانبی نباید کل اپلیکیشن را پایین بیاورد. سیستم دیزاین اسنپفود باید به گونهای باشد که در صورت بروز مشکل،کاربری را متوقف نکند:
اگر Redis قطع شد: سیستم موقتاً کوئریها را مستقیماً به OpenSearch یا لایه هندسی PostGIS میفرستد.
اگر سرویس هوش مصنوعی و شخصیسازی لوپ داد: سیستم لیست را بر اساس یک رتبهبندی عمومی (مثل پرفروشترینهای منطقه) مرتب میکند.
اگر میکرسرویس محاسبات ETA از دسترس خارج شد: سیستم یک زمان تقریبی ثابت بر اساس میانگین مسافت نشان میدهد.
شاید کاربر لیست کاملاً ایدهآلی نبیند، اما هسته اصلی بیزینس حفظ میشود و کاربر همچنان میتواند سفارش خود را ثبت کند.
یک سیستم توزیعشده جغرافیایی موفق، باید معیارهای زیر را مدام پایش کند:
میزان تاخیر جستجو (p95/p99 Latency): سرعت پاسخدهی در ساعات پیک شام و ناهار.
نرخ خطای مغایرت وضعیت (Availability Mismatch Rate): چه تعداد کاربر رستورانی را دیدهاند اما بعد از کلیک متوجه شدهاند بسته است؟
نرخ لغو سفارش در مرحله پرداخت (Checkout Rejection Rate): چه میزان از سفارشها در گام آخر به دلیل دیتای منقضیشده لایه کش ریجکت میشوند؟ (این عدد باید نزدیک به صفر باشد).
اگر بخواهیم بزرگترین درس مهندسی در طراحی سیستمهای لوکیشنمحور را خلاصه کنیم، باید بگوییم:
تفکر خود را از «پیدا کردن رستورانهای نزدیک به لوکیشن کاربر» تغییر دهید به «پیدا کردن وندورهای فعالی که در حال حاضر اجازه و توانایی سرویسدهی به سلول نقشهای کاربر را دارند».
همین تغییر کوچک در پارادایم فکری، ساختار دیتابیسها، استراتژی کشینگ و کل معماری فنی شما را دگرگون و آماده پذیرش میلیونها درخواست در ثانیه میکند.