<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>نوشته های افشین یاری نیا</title><link>https://virgool.ir/feed/@m_58197875</link><description></description><language>fa</language><pubDate>2026-11-10 15:22:48</pubDate><image><url>https://files.virgool.ir/upload/users/1195995/avatar/yxDOrY.png</url><title>افشین یاری نیا</title><link>https://virgool.ir/@m_58197875</link></image><item><title>طراحی پرداخت و کیف پول در مقیاس بالا؛ وقتی پول جابه‌جا شده، اما پاسخ نرسیده است</title><link>https://virgool.ir/@m_58197875/طراحی-پرداخت-و-کیف-پول-در-مقیاس-بالا-وقتی-پول-جابه-جا-شده-اما-پاسخ-نرسیده-است-qphrvpr5jqlg</link><description>کاربر یک میلیون تومان پرداخت می‌کند. بانک مبلغ را از حسابش کم می‌کند، اما پاسخ درگاه به سرور ما نمی‌رسد. صفحه همچنان در حال بارگذاری است و کاربر دوباره روی «پرداخت» می‌زند.چند ثانیه بعد، یک رویداد موفقیت از درگاه می‌رسد. Worker پردازش را شروع می‌کند، تغییرات دیتابیس را ثبت می‌کند و درست قبل از اعلام پایان کار از دسترس خارج می‌شود. درگاه هم چون پاسخ روشنی نگرفته، همان رویداد را دوباره می‌فرستد.از نگاه کاربر، فقط یک خرید اتفاق افتاده است. از نگاه backend، چند درخواست، چند پیام و چند نتیجه نامشخص داریم.چطور مطمئن شویم این تلاش‌های تکراری، پول را دوبار جابه‌جا نمی‌کنند و هر مبلغی که واقعاً منتقل شده، در سیستم ما قابل پیگیری است؟طراحی پرداخت از همین سؤال شروع می‌شود. داشتن APIهای pay، refund و transfer کافی نیست. باید بدانیم وقتی پاسخ گم می‌شود، دو درخواست هم‌زمان می‌رسند یا واقعیت درگاه با دیتابیس ما اختلاف پیدا می‌کند، سیستم چه رفتاری دارد.در این مقاله یک پلتفرم فرضی با پرداخت آنلاین، کیف پول و تسویه با فروشندگان طراحی می‌کنیم. معماری و اعداد، پیشنهاد و فرض طراحی‌اند و زیرساخت داخلی هیچ شرکت مشخصی را توصیف نمی‌کنند. منظور از «پول» در ادامه، هم حرکت واقعی وجه در سیستم بیرونی است و هم ثبت بدهی‌ها و مطالبات متناظر در دفترکل داخلی؛ این دو باید با یکدیگر تطبیق داده شوند.۱. اول مشخص کنیم چه قولی به کاربر می‌دهیمسیستم باید به کاربر اجازه دهد پرداخت ایجاد کند، کیف پولش را شارژ کند، موجودی و تاریخچه را ببیند، پول منتقل کند و درخواست برداشت یا بازپرداخت بدهد. فروشنده هم باید بداند چه مبلغی به او تعلق دارد، چه بخشی هنوز قابل تسویه نیست و کدام انتقال بانکی انجام شده است.اما پشت این قابلیت‌ها چند تعهد مهم‌تر وجود دارد:یک درخواست تکراری نباید اثر مالی تازه‌ای ایجاد کند.خرج‌کردن نباید از موجودی مجاز بیشتر شود.بازپرداخت‌های موفق و در حال انجام نباید بیش از مبلغ قابل بازپرداخت تعهد ایجاد کنند.هر ثبت مالی باید مبلغ، واحد پول، دلیل و مرجع مشخص داشته باشد.وضعیت نامشخص باید قابل بازیابی باشد و به‌اشتباه «ناموفق» نمایش داده نشود.اصلاح اشتباه باید تاریخچه را حفظ کند.این‌ها invariantهای دامنه‌اند؛ قانون‌هایی که شکستنشان به معنای رفتار مالی نادرست است. سرعت و دسترس‌پذیری را باید در چارچوب همین قانون‌ها بهبود دهیم.برای مثال، اگر هنگام failover دیتابیس نمی‌توانیم موجودی معتبر را بررسی کنیم، متوقف‌کردن موقت انتقال تصمیم قابل دفاعی است. پذیرفتن انتقال با یک موجودی قدیمی، صرفاً برای اینکه API همچنان پاسخ موفق بدهد، مسئله را به آینده منتقل می‌کند.این انتخاب به معنی کم‌اهمیت‌بودن availability نیست. طراحی باید مسیرهای خواندن وضعیت، پیگیری سفارش و نمایش پیام مناسب را تا جای ممکن حفظ کند؛ حتی زمانی که ثبت حرکت مالی جدید موقتاً محدود شده است.۲. مقیاس، شکل بار را مشخص می‌کندفرض کنیم پلتفرم ۱۰ میلیون کاربر ثبت‌شده، ۲ میلیون کاربر فعال ماهانه و روزانه ۵۰۰ هزار عملیات مالی دارد. میانگین تعداد عملیات در ثانیه چندان بزرگ نیست:500,000 / 86,400 ≈ 5.8 business operations / secondاما میانگین، ساعت شلوغ سیستم را توضیح نمی‌دهد. فروش ویژه یا روز تسویه ممکن است بار را در یک بازه کوتاه متمرکز کند. برای این طراحی، پیک عادی ۱۰۰ تا ۲۵۰ عملیات در ثانیه و یک سناریوی آزمون فشار بالاتر از ۱۰۰۰ عملیات در ثانیه در نظر می‌گیریم. این اعداد فرض‌اند و باید با داده واقعی اصلاح شوند.یک عملیات کسب‌وکار هم لزوماً یک write نیست. اگر هر عملیات به‌طور متوسط چهار سطر دفترکل بسازد، روزانه حدود دو میلیون Ledger Entry خواهیم داشت. با فرض ۳۰۰ تا ۷۰۰ بایت برای هر سطر و سهم ایندکس‌های آن، حجم پایه روزانه حدود ۰٫۶ تا ۱٫۴ گیگابایت می‌شود؛ WAL، replica، backup و داده‌های جانبی جداگانه محاسبه می‌شوند.بار خواندن وضعیت و تاریخچه نیز با بار ثبت پول یکسان نیست. Callbackهای تکراری، بررسی‌های دوره‌ای و گزارش‌های تسویه هرکدام الگوی متفاوتی دارند. به همین دلیل، صرف دانستن TPS برای انتخاب معماری کافی نیست؛ باید hot accountها، تعداد ثبت‌های هر عملیات و زمان نگه‌داشتن lockها را هم بشناسیم.چند هدف اولیه برای آزمون طراحی می‌تواند این‌ها باشد:ایجاد Payment Intent داخلی: تأخیر p95 کمتر از ۲۵۰ میلی‌ثانیه، بدون احتساب زمان پاسخ درگاه.خواندن موجودی: p95 کمتر از ۱۰۰ میلی‌ثانیه.انتقال داخلی کیف پول: p95 کمتر از ۴۰۰ میلی‌ثانیه در بار تعریف‌شده.دریافت و ذخیره پایدار Webhook معتبر: p95 کمتر از ۲۰۰ میلی‌ثانیه.نهایی‌شدن پرداخت یا تسویه به رفتار provider هم وابسته است. این بخش را باید با سنجه‌هایی مثل عمر پرداخت نامشخص و زمان تطبیق اندازه گرفت، نه با یک وعده سراسری که وابستگی بیرونی را نادیده می‌گیرد.۳. معماری را از مرزهای مالی بسازیمبرای شروع، یک Modular Monolith با ماژول‌های روشن و PostgreSQL مشترک انتخاب مناسبی است. Payment، Wallet، Ledger، Refund و Settlement مسئولیت‌های جدا دارند، اما تغییراتی که باید با هم commit شوند، هنوز داخل یک تراکنش محلی باقی می‌مانند.شکل ۱ — تأیید بیرونی پیش از ثبت نهایی بررسی می‌شود. تغییر وضعیت پرداخت، ثبت دفترکل، موجودی مرتبط و Outbox داخل یک تراکنش PostgreSQL قرار می‌گیرند؛ تماس شبکه‌ای جزو این تراکنش نیست.Payment مالک تلاش‌های پرداخت و هماهنگی با درگاه است. Wallet سیاست موجودی قابل خرج و رزرو را کنترل می‌کند. Ledger ثبت مالی را انجام می‌دهد. Refund و Settlement هم چرخه عمر مستقل خودشان را دارند.Gateway Adapter تفاوت providerها را از هسته جدا می‌کند. یک درگاه ممکن است authorization و capture جدا داشته باشد؛ دیگری فقط پرداخت و verify ارائه کند. وجود Webhook امضاشده، امکان inquiry و پشتیبانی از idempotency نیز باید جزو مدل قابلیت‌های هر provider باشد.Reconciliation اختلاف میان داده داخلی و گزارش بیرونی را پیدا می‌کند. این ماژول فقط مصرف‌کننده رویدادهای موفق نیست؛ باید بتواند مستقل از پیام‌ها، پرداخت‌های متوقف‌شده و گزارش‌های دوره‌ای را بررسی کند.Redis برای cache، rate limit و داده موقت مناسب است. Kafka یا یک broker دیگر، انتشار رویدادها و پردازش‌های جانبی را جدا می‌کند. هیچ‌کدام جای مرجع مالی PostgreSQL را نمی‌گیرند.استخراج یک سرویس زمانی ارزش دارد که بار، مالکیت تیم، نیاز امنیتی یا چرخه انتشار مستقل آن روشن شده باشد. جداکردن Ledger از دیتابیس بقیه ماژول‌ها، مرز atomic فعلی را تغییر می‌دهد و به طراحی تازه‌ای نیاز دارد؛ صرفاً جابه‌جایی چند کلاس به یک پروژه دیگر نیست.۴. Payment Intent، هویت عملیات پیش از تماس با درگاهقبل از فرستادن کاربر به درگاه، باید بدانیم او برای چه چیزی، به چه کسی و با چه مبلغی پرداخت می‌کند. Payment Intent این تصمیم داخلی را نگه می‌دارد.مبلغ از سفارش یا محاسبه معتبر سرور استخراج می‌شود. amount ارسالی مرورگر به‌تنهایی مبنای دریافت پول نیست. شناسه کاربر، فروشنده، سفارش، واحد پول و نسخه مبلغ پذیرفته‌شده نیز به این تصمیم متصل می‌شوند.در کنار Intent، بهتر است تلاش‌های ارتباط با provider را در payment_attempts نگه داریم. یک Intent ممکن است در طول عمرش چند تلاش داشته باشد، اما هر attempt باید مرجع بیرونی و نتیجه خودش را حفظ کند.Payment Intent&#xA;  id, order_id, user_id, merchant_id&#xA;  amount, currency, status, version&#xA;&#xA;Payment Attempt&#xA;  id, payment_id, provider, provider_account_id&#xA;  merchant_reference, external_reference&#xA;  status, requested_at, last_checked_atوجود Intent به ما اجازه نمی‌دهد موفقیت درگاه را حدس بزنیم. درگاه مرجع وضعیت عملیات خودش است؛ ما هم مالک وضعیت کسب‌وکار و ثبت داخلی هستیم. اتصال این دو حقیقت با reference معتبر و تطبیق انجام می‌شود.همین نکته در retry اهمیت پیدا می‌کند. اگر ساخت session نامشخص شد، نباید فوراً attempt تازه‌ای روی درگاه دیگری بسازیم. ابتدا باید بفهمیم تلاش قبلی قابل بازیابی است یا نه؛ وگرنه یک خرید می‌تواند به دو پرداخت واقعی تبدیل شود.۵. وضعیت نامشخص، یک state واقعی استیک ستون آزاد به نام status که هر بخش برنامه آن را تغییر دهد، مدل قابل اتکایی برای پرداخت نیست. هر transition باید command، پیش‌شرط و شواهد مشخص داشته باشد.برای یک جریان ساده می‌توان وضعیت‌های زیر را در نظر گرفت:CREATED -&gt; PENDING -&gt; CAPTURED&#xA;                 -&gt; FAILED_CONFIRMED&#xA;                 -&gt; UNKNOWN&#xA;&#xA;UNKNOWN -&gt; CAPTURED          [verified success]&#xA;UNKNOWN -&gt; FAILED_CONFIRMED  [verified final failure]این مدل نمونه است. درگاهی که authorization مستقل دارد به stateهای بیشتری نیاز دارد. نام‌های داخلی هم نباید بدون توجه به قرارداد provider، معادل نام‌های API او فرض شوند.Timeout فقط می‌گوید پاسخ به ما نرسیده است. ممکن است عملیات اصلاً شروع نشده باشد، هنوز ادامه داشته باشد یا کامل شده باشد. بنابراین نتیجه نامشخص را با شکست قطعی یکی نمی‌کنیم.انقضای صفحه پرداخت نیز به‌تنهایی ثابت نمی‌کند پول جابه‌جا نشده است. موفقیت دیرهنگام باید از مسیر recovery پردازش شود و سیاست سفارش تعیین کند که خدمت ارائه شود یا وجه بازگردد.Refund و تسویه را هم در یک زنجیره خطی با Payment جا نمی‌دهیم. پرداختی که قبلاً تسویه شده، ممکن است بعداً بازپرداخت بخورد. در این طراحی، وضعیت دریافت وجه، چرخه بازپرداخت و چرخه تسویه جدا هستند و وضعیت نمایشی کاربر از ترکیب آن‌ها ساخته می‌شود.۶. Idempotency باید قرارداد داشته باشدفرض کنید کاربر همان دکمه پرداخت را دوباره می‌زند. اگر دو درخواست به یک تصمیم کسب‌وکار مربوط باشند، باید به همان عملیات برسند.POST /v1/payments&#xA;Idempotency-Key: order-123-payment-v1کلید به‌تنهایی کافی نیست. باید scope آن مشخص باشد: مثلاً tenant، کاربر و نوع عملیات. یک fingerprint از ورودی‌های معنادار نیز نگه می‌داریم تا استفاده دوباره از همان کلید با مبلغ یا مقصد متفاوت، conflict بدهد.UNIQUE (scope, idempotency_key)این constraint رقابت دو درخواست هم‌زمان را در دیتابیس کنترل می‌کند. بررسی قبلی با یک SELECT یا گرفتن lock موقت در Redis، جای آن را نمی‌گیرد.رکورد idempotency باید وضعیت در حال انجام را هم بشناسد. اگر درخواست دوم زمانی برسد که اولی هنوز تمام نشده، پاسخ کاملِ ساختگی برنمی‌گردانیم. قرارداد API می‌تواند همان resource و وضعیت جاری را بدهد یا پاسخ مشخص «در حال پردازش» داشته باشد.برای عملیات محلی، ثبت کلید و اثر مالی باید در همان تراکنش انجام شوند. برای تماس بیرونی، ابتدا attempt پایدار ساخته می‌شود و نتیجه تماس بعداً به همان attempt برمی‌گردد. دوره نگهداری کلید نیز باید با طول عمر retry هماهنگ باشد؛ حذف زودهنگام آن می‌تواند یک تلاش قدیمی را دوباره قابل اجرا کند.Idempotency در سطح API با یکتایی عملیات مالی یکسان نیست. حتی اگر کلاینت کلید تازه‌ای بسازد، قاعده کسب‌وکار باید جلوی دریافت غیرمجاز دوباره برای همان سفارش را بگیرد. همین موضوع برای reference درگاه و عملیات Ledger نیز constraint مستقل می‌خواهد.مستندات Idempotent requests در Stripe نمونه‌ای از تعریف دقیق این قرارداد است. رفتار نگهداری کلید و پاسخ در providerهای دیگر باید جداگانه بررسی شود.۷. Callback خبر می‌دهد؛ تأیید مالی به شواهد معتبر نیاز داردبازگشت مرورگر از درگاه برای هدایت کاربر مفید است، اما دلیل کافی برای ثبت موفقیت نیست. کاربر ممکن است tab را ببندد، redirect نرسد یا پارامترهای callback دست‌کاری شوند.درگاه‌های مختلف روش‌های متفاوتی دارند. اگر Webhook امضاشده ارائه می‌شود، امضا باید مطابق مستندات provider و روی محتوای مورد انتظار او بررسی شود. اگر قرارداد به verify یا inquiry سمت سرور نیاز دارد، همان مسیر اجرا می‌شود. نبود امضا در یک provider به معنی اعتماد به داده مرورگر نیست.در هر دو حالت، reference، مبلغ، واحد پول و حساب پذیرنده باید با attempt داخلی مطابقت داشته باشند.برای Webhook معتبر، مسیر دریافت را کوتاه نگه می‌داریم: اعتبارسنجی، ذخیره پایدار در Inbox و سپس پاسخ مناسب. پردازش مالی سنگین بعداً انجام می‌شود. اگر ذخیره‌سازی شکست خورد، نباید به provider وانمود کنیم پیام را پذیرفته‌ایم.در مستندات Webhookهای Stripe هم دریافت تکراری و نامرتب رویدادها مطرح شده است. بنابراین ترتیب رسیدن پیام‌ها را جایگزین قواعد state machine نمی‌کنیم.دو تفاوت مهم وجود دارد:received یعنی پیام را داریم؛ processed یعنی اثر آن با موفقیت ثبت شده است.یکتایی event با یکتایی اثر مالی فرق دارد. دو event متفاوت ممکن است درباره یک capture واحد خبر بدهند.برای اولی، شناسه رویداد در scope درست provider و حساب پذیرنده یکتا می‌شود. برای دومی، reference عملیات بیرونی و هویت ثبت Ledger محافظت مستقل دارند. رویداد معتبر با مرجع ناشناخته نیز در صف بررسی نگه داشته می‌شود؛ نه اینکه از روی آن خودکار یک پرداخت تازه بسازیم.۸. کیف پول یک عدد نیستفرض کنیم کاربر یک میلیون تومان موجودی ثبت‌شده دارد و ۲۰۰ هزار تومان آن برای یک برداشت رزرو شده است. هم‌زمان، شارژ دیگری به مبلغ ۱۵۰ هزار تومان هنوز تأیید نهایی نشده است.در این طراحی:Posted balance:      1,000,000 toman&#xA;Active holds:          200,000 toman&#xA;Available balance:     800,000 toman&#xA;Pending incoming:      150,000 tomanمبلغ pending هنوز قابل خرج نیست. رزرو هم باید با یک عملیات مشخص، وضعیت و مسیر آزادسازی مرتبط باشد. کم‌کردن دستی یک ستون reserved_balance بدون داشتن رکورد hold، امکان توضیح و بازیابی را از بین می‌برد.مدل دقیق می‌تواند متفاوت باشد. در این مقاله، posted_balance از ثبت‌های نهایی Ledger به دست می‌آید، holdهای فعال جدا نگه داشته می‌شوند و موجودی قابل خرج از ترکیب آن‌ها و محدودیت‌های دامنه محاسبه می‌شود.این تعریف باید در API، دیتابیس و ابزار پشتیبانی یکسان باشد. وقتی هر تیم معنای متفاوتی برای «موجودی» داشته باشد، یک عدد ظاهراً درست هم می‌تواند تصمیم اشتباه ایجاد کند.۹. Ledger، توضیح پایدار حرکت پولدفترکل به سه مفهوم اصلی نیاز دارد: Account، Ledger Transaction و Ledger Entry. Account حسابی با نوع و واحد پول مشخص است. Transaction یک ثبت مالی واحد را گروه‌بندی می‌کند و Entry اثر آن را روی یک حساب نشان می‌دهد.در ثبت دوطرفه، مجموع debit و credit هر تراکنش برای هر واحد پول برابر است. اما debit همیشه به معنی کاهش موجودی نیست؛ اثر آن به نوع حساب بستگی دارد.از نگاه پلتفرم، موجودی کیف پول کاربر معمولاً بدهی پلتفرم به کاربر است. حساب دارایی بانکی ماهیت دیگری دارد. نمونه Digital Wallet در Modern Treasury همین تفاوت حساب دارایی و بدهی را نشان می‌دهد.برای مثال، فرض کنیم کاربر از کیف پول خود یک میلیون تومان خرید می‌کند و کارمزد پلتفرم ۲۰ هزار تومان است. در مدل ساده این مقاله، ثبت زیر داریم:Account                       Debit         Credit&#xA;User wallet liability       1,000,000              0&#xA;Merchant payable                    0        980,000&#xA;Platform fee revenue                0         20,000&#xA;---------------------------------------------------&#xA;Total                       1,000,000      1,000,000&#xA;&#xA;Display unit: tomanبدهی ما به کاربر کم شده، بدهی به فروشنده افزایش یافته و سهم کارمزد جدا ثبت شده است. این مثال مالیات، ذخایر و قراردادهای پیچیده را مدل نمی‌کند؛ سرفصل‌ها و زمان شناسایی درآمد باید با مدل مالی واقعی محصول هماهنگ شوند.شارژ کیف پول ثبت متفاوتی دارد: افزایش دارایی یا مطالبه از درگاه، در برابر افزایش بدهی به کاربر. بنابراین نباید یک الگوی علامت مثبت و منفی را بدون توجه به نوع حساب به همه عملیات تعمیم دهیم.توازن لازم است، اما کافی نیست. تراکنشی که حساب اشتباه را با مبلغ اشتباه debit و credit کند، هنوز می‌تواند متوازن باشد. کنترل مبلغ، مالکیت حساب، واحد پول و reference کسب‌وکار در کنار توازن قرار می‌گیرند.۱۰. داده مالی باید قابل بازسازی باشدهسته مدل داده را می‌توان این‌طور خلاصه کرد:ledger_accounts&#xA;  id, owner_id, account_type, currency, status&#xA;&#xA;ledger_transactions&#xA;  id, operation_id, posting_type, occurred_at, posted_at&#xA;&#xA;ledger_entries&#xA;  id, transaction_id, account_id, direction, amount&#xA;&#xA;account_balances&#xA;  account_id, posted_balance, reserved_balance, version&#xA;&#xA;wallet_holds&#xA;  id, account_id, operation_id, amount, status, expires_atکنار این هسته، جدول‌های پرداخت، attempt، بازپرداخت، برداشت، تسویه، Inbox، Outbox و پرونده‌های تطبیق قرار می‌گیرند. هر ثبت مالی باید به عملیاتی مشخص برسد و هر account در یک واحد پول تعریف شود.مبلغ را با integer در واحد پایه یا نوع decimal دقیق مانند NUMERIC نگه می‌داریم. در این طراحی، ذخیره و تبادل داخلی بر اساس ریال و currency = IRR است؛ تومان فقط در مثال‌های متنی و نمایش کاربر استفاده می‌شود. تبدیل واحد باید در مرز مشخص و فقط یک بار انجام شود.رکوردهای Ledger پس از نهایی‌شدن تغییرناپذیرند. اشتباه با تراکنش جبرانی اصلاح می‌شود و reference ثبت قبلی حفظ می‌ماند. محدودیت UPDATE و DELETE در مسیر برنامه، کنترل دسترسی مدیریتی و ثبت اقدام‌های پشتیبانی از این اصل محافظت می‌کنند.توازن چند Entry را نمی‌توان با یک CHECK ساده روی هر سطر تضمین کرد. مسیر posting باید تنها راه ثبت نهایی باشد و توازن، نوع حساب‌ها و یکتایی عملیات را در مرز تراکنش کنترل کند؛ بسته به پیاده‌سازی، این کنترل با routine دیتابیس یا constraint trigger مناسب و دسترسی محدود تقویت می‌شود.۱۱. Balance یک projection است؛ اما projection حساسمحاسبه دوباره کل تاریخچه برای هر درخواست موجودی، در حساب‌های پرترافیک پرهزینه می‌شود. بنابراین یک مقدار materialized نگه می‌داریم تا خواندن و کنترل خرج سریع باشد.برای موجودی قابل خرج، به‌روزرسانی projection را به یک consumer با تأخیر نامشخص نمی‌سپاریم. ثبت Ledger، تغییر موجودی مرتبط و تغییر hold در یک تراکنش محلی انجام می‌شوند. گزارش تحلیلی می‌تواند دیرتر به‌روز شود؛ مجوز خرج نمی‌تواند از همان قرارداد استفاده کند.اگر اختلافی پیدا شد، rebuild فقط جمع‌زدن Ledger نیست. موجودی posted از دفترکل بازسازی می‌شود، اما holdهای فعال و محدودیت‌های حساب هم باید در محاسبه available دیده شوند.بازیابی باید روی snapshot سازگار انجام شود. یا حساب موقتاً برای write متوقف می‌شود، یا فرآیند بازسازی با version و watermark مشخص، تغییرات هم‌زمان را پوشش می‌دهد. بازنویسی projection با نتیجه یک محاسبه قدیمی می‌تواند همان خطایی را ایجاد کند که قصد اصلاحش را داشتیم.۱۲. دو درخواست هم‌زمان، یک موجودی مشترککاربر ۱۰۰ هزار تومان دارد. درخواست اول می‌خواهد ۸۰ هزار تومان خرج کند و درخواست دوم ۷۰ هزار تومان. اگر هر دو موجودی اولیه را بخوانند، هر دو ممکن است مجاز به نظر برسند.مسئله با خواندن موجودی و سپس save() حل نمی‌شود. بررسی و تغییر باید تحت کنترل concurrency یکسان انجام شوند.یک گزینه، قفل سطر با SELECT ... FOR UPDATE است. گزینه دیگر، به‌روزرسانی شرطی است:UPDATE account_balances&#xA;SET posted_balance = posted_balance - :amount,&#xA;    version = version + 1&#xA;WHERE account_id = :account_id&#xA;  AND :amount &gt; 0&#xA;  AND posted_balance - reserved_balance &gt;= :amount&#xA;RETURNING posted_balance, reserved_balance, version;این قطعه فقط شرط موجودی در یک خرج مستقیم را نشان می‌دهد. به‌تنهایی پیاده‌سازی انتقال نیست. یکتایی عملیات، ثبت متوازن Ledger، تغییر حساب مقابل و Outbox باید در همان تراکنش تکمیل شوند؛ شکست هر بخش، همه تغییرات را rollback می‌کند.در انتقال بین دو کیف پول، حساب‌ها را با ترتیب ثابتی، مثلاً بر اساس شناسه، lock می‌کنیم تا ریسک deadlock کاهش یابد. خود deadlock و serialization failure هم باید با retry محدودِ کل تراکنش و همان هویت عملیات مدیریت شوند. رفتار قفل‌های سطری در مستندات PostgreSQL توضیح داده شده است.Optimistic locking برای contention پایین مفید است، اما روی حساب بسیار داغ می‌تواند retry زیادی بسازد. سطح SERIALIZABLE هم ممکن است برای invariantهای پیچیده مناسب باشد، ولی نیاز به رسیدگی به serialization failure را حذف نمی‌کند.۱۳. رزرو، برداشت و خرج نهایی را جدا کنیمبرداشت از کیف پول یک عملیات بیرونی است. نمی‌توانیم موجودی را آزاد بگذاریم تا بانک نتیجه بدهد، چون کاربر ممکن است همان مبلغ را جای دیگری خرج کند.ابتدا مبلغ در یک hold پایدار رزرو می‌شود. ساخت hold، بررسی موجودی و تغییر projection داخل تراکنش انجام می‌شوند. سپس درخواست انتقال با reference ثابت و خارج از lock دیتابیس به provider ارسال می‌شود.اگر موفقیت تأیید شد، hold مصرف می‌شود و ثبت مالی نهایی انجام می‌گیرد. اگر شکست قطعی بود، hold آزاد می‌شود. در نتیجه نامشخص، مبلغ همچنان رزرو می‌ماند تا inquiry یا بررسی عملیاتی وضعیت را روشن کند.انقضای زمانی hold به‌تنهایی مجوز آزادکردن پولی نیست که شاید انتقالش موفق شده باشد. پس job انقضا باید وضعیت attempt بیرونی را در نظر بگیرد. Timer برای کشف عملیات متوقف‌شده مفید است؛ جای شواهد نتیجه انتقال را نمی‌گیرد.۱۴. مرز تراکنش، جایی است که صحت را حفظ می‌کنیمفرض کنیم شواهد معتبر capture دریافت شده و حالا باید نتیجه را داخل سیستم ثبت کنیم. ترتیب کلی چنین است:BEGIN&#xA;  claim business operation / check prior result&#xA;  lock payment and affected balance rows&#xA;  validate evidence, amount, currency and transition&#xA;  post balanced ledger transaction&#xA;  update related balance projections&#xA;  update payment state&#xA;  mark input processed&#xA;  insert outbox event&#xA;COMMITاین شبه‌کد مرز atomic را توضیح می‌دهد. شواهدی که پیش‌تر از provider گرفته‌ایم، هنگام ثبت با وضعیت فعلی دوباره تطبیق داده می‌شوند تا یک worker قدیمی نتواند تصمیم ناسازگار اعمال کند.تماس با درگاه، بانک، broker یا سرویس پیامک را داخل این تراکنش طولانی نگه نمی‌داریم. قطع ارتباط بیرونی نباید lockهای مالی را برای چند ثانیه یا چند دقیقه اشغال کند.اگر نتیجه خود COMMIT هم به دلیل قطع اتصال نامشخص شد، فرض نمی‌کنیم rollback رخ داده است. عملیات را با شناسه پایدارش جست‌وجو می‌کنیم و retry از همان قرارداد idempotency عبور می‌کند.۱۵. Outbox و Inbox دو شکاف متفاوت را می‌بندندبعد از commit، باید خبر پرداخت را به سرویس‌های دیگر برسانیم. اگر ابتدا دیتابیس را تغییر دهیم و سپس پیام منتشر کنیم، crash میان این دو کار می‌تواند پیام را گم کند.Outbox قصد انتشار را همراه تغییر مالی در همان تراکنش ثبت می‌کند. Dispatcher بعداً پیام را می‌فرستد و پس از تأیید broker، آن را منتشرشده علامت می‌زند.اما اگر ارسال موفق شود و Dispatcher پیش از ثبت علامت crash کند، پیام دوباره منتشر خواهد شد. بنابراین Outbox به‌تنهایی تحویل یک‌باره نمی‌سازد. این رفتار در شرح Transactional Outbox نیز مطرح شده است.سمت مصرف‌کننده، شناسه پیام و اثر محلی آن در یک تراکنش ثبت می‌شوند. برای consumerهای مستقل می‌توان از هویت ترکیبی استفاده کرد:UNIQUE (consumer_name, message_id)اگر consumer بعد از commit و قبل از ack از دسترس خارج شود، اجرای بعدی متوجه ثبت قبلی می‌شود. ولی اگر اثر اصلی تماس با سرویس بیرونی باشد، Inbox محلی به‌تنهایی کافی نیست؛ آن تماس هم به attempt پایدار، idempotency یا inquiry نیاز دارد.هدف عملی، اعمال یک‌باره اثر مالی در مرزی است که کنترلش می‌کنیم. کد ممکن است چند بار اجرا شود و پیام ممکن است چند بار برسد. این تفاوت را باید در طراحی و ادعاهای مربوط به exactly-once روشن نگه داریم.۱۶. بازپرداخت جزئی، یک race پنهان داردفرض کنیم از یک پرداخت یک‌میلیون‌تومانی، دو درخواست بازپرداخت ۶۰۰ هزار تومانی هم‌زمان برسند. اگر فقط مجموع refundهای موفق را بررسی کنیم، هر دو ممکن است پذیرفته شوند؛ چون هنوز هیچ‌کدام موفق نشده‌اند.بنابراین ظرفیت بازپرداخت فقط با نتیجه‌های نهایی کنترل نمی‌شود. درخواست‌های پذیرفته‌شده و حل‌نشده هم تعهد ایجاد کرده‌اند:successful_refunds&#xA;+ reserved_nonfinal_refunds&#xA;&lt;= refundable_limitهر refund دقیقاً در یکی از این دو گروه قرار می‌گیرد؛ یک مبلغ را هم‌زمان در هر دو جمع نمی‌زنیم. refundable_limit نیز از capture معتبر و قواعد محصول به دست می‌آید.در تراکنش محلی، Payment را lock می‌کنیم، ظرفیت را می‌سنجیم، مبلغ refund را رزرو می‌کنیم و فرمان بیرونی را پایدار ثبت می‌کنیم. تماس provider بعد از commit انجام می‌شود.نتیجه موفق، ثبت جبرانی تازه می‌سازد. شکست قطعی، رزرو را آزاد می‌کند. Timeout، refund را به وضعیت UNKNOWN یا RECONCILING می‌برد و رزرو را تا روشن‌شدن نتیجه نگه می‌دارد.اگر وجه قبلاً با فروشنده تسویه شده باشد، بازپرداخت ممکن است از ذخیره، موجودی قابل پرداخت بعدی یا یک مطالبه از فروشنده تأمین شود. این تصمیم باید سیاست مالی روشن داشته باشد؛ تغییر پنهانی تاریخچه تسویه راه‌حل نیست.Reversal، لغو authorization و Refund نیز در همه providerها معنای یکسانی ندارند. آزادکردن مجوز پرداختِ captureنشده با بازگرداندن وجه دریافت‌شده دو جریان متفاوت است و Adapter باید تفاوت قراردادها را حفظ کند.۱۷. تسویه یعنی انتخاب دقیقِ پولی که هنوز پرداخت نشده استفرض کنیم فروشنده ۱۰۰ میلیون تومان فروش، ۵ میلیون تومان بازپرداخت و ۲ میلیون تومان کارمزد دارد. در مدل ساده، مبلغ قابل پرداخت ۹۳ میلیون تومان است. اما این عدد به‌تنهایی برای ساخت انتقال بانکی کافی نیست.باید بدانیم کدام اقلام واجد شرایط‌اند، چه چیزهایی قبلاً تسویه شده‌اند و چه مبالغی به دلیل رزرو یا اختلاف مالی فعلاً کنار گذاشته شده‌اند. محاسبه نیز نباید کارمزدی را که قبلاً از payable کم شده، دوباره کسر کند.در این طراحی، batch از اقلام مشخص ساخته می‌شود. settlement_items هر قلم را به batch مربوط وصل می‌کند و constraint اجازه تخصیص دوباره همان قلم را نمی‌دهد. مبلغ و محتوای batch پس از تأیید freeze می‌شوند.CALCULATED -&gt; APPROVED -&gt; TRANSFER_REQUESTED&#xA;                      -&gt; PROCESSING -&gt; PAID&#xA;                                    -&gt; FAILED_CONFIRMED&#xA;                                    -&gt; UNKNOWNپاسخ «درخواست پذیرفته شد» از بانک، لزوماً به معنای انتقال نهایی نیست. در نتیجه نامشخص، قبل از ارسال دوباره انتقال، با همان reference استعلام می‌گیریم. اگر provider امکان تشخیص امن را ندهد، پرونده به بررسی عملیاتی می‌رود.تسویه باید از اقلام واجد شرایط دفترکل و تخصیص‌های پایدار ساخته شود، نه از یک query متغیر که هر بار نتیجه دیگری برای همان batch برمی‌گرداند.۱۸. Reconciliation، راه پیدا کردن واقعیت بعد از اختلافبرگردیم به کاربر ابتدای مقاله. درگاه پرداخت را موفق می‌داند، اما Payment داخلی هنوز UNKNOWN است. منتظرماندن همیشگی برای Webhook، مسیر بازیابی محسوب نمی‌شود.Reconciliation وضعیت داخلی را با inquiry، گزارش provider و در صورت نیاز گزارش بانکی مقایسه می‌کند. برخی بررسی‌ها پیوسته و برخی وابسته به زمان آماده‌شدن فایل‌های تسویه‌اند.نمونه اختلاف‌ها شامل پرداخت بیرونی بدون ثبت داخلی، مبلغ نامنطبق، refund حل‌نشده، انتقال بانکی بدون تأیید نهایی و اختلاف projection با Ledger است. همه این موارد یک علت و یک راه‌حل ندارند.برای هر اختلاف، یک پرونده پایدار می‌سازیم:internal_reference, external_reference&#xA;expected_amount, observed_amount, currency&#xA;discrepancy_type, evidence_reference&#xA;status, first_seen_at, last_checked_at&#xA;resolution, resolved_by, resolved_atاگر موفقیت بیرونی تأیید شده، هویت و مبلغ دقیقاً مطابق‌اند و اثر داخلی وجود ندارد، recovery از همان command کنترل‌شده posting عبور می‌کند. مسیر تطبیق نباید راه دومی برای دورزدن invariantها بسازد.اختلاف مبلغ، مرجع ناشناخته یا شواهد متناقض به review می‌رود. Operator باید دلیل، مدارک و اقدام مجاز را ببیند. در صورت نیاز به اصلاح، ثبت جبرانی با reference پرونده ساخته می‌شود؛ سطر قبلی دفترکل دست‌کاری نمی‌شود.۱۹. Saga کجا کمک می‌کند؟سفارش، موجودی کالا، درگاه و بانک معمولاً در یک دیتابیس نیستند. در این مرزها به workflow نیاز داریم: مثلاً رزرو کالا، گرفتن مجوز پرداخت، تأیید سفارش و capture، البته فقط اگر provider چنین قابلیت‌هایی داشته باشد.اگر یک مرحله شکست خورد، باید اقدام جبرانی متناسب انجام شود. این اقدام همیشه rollback فوری نیست؛ آزادسازی رزرو، لغو authorization یا refund می‌تواند زمان ببرد یا خودش نامشخص شود.Saga همین مراحل و جبران‌ها را هماهنگ می‌کند. اما ثبت Ledger و تغییر موجودی داخل یک PostgreSQL را بی‌دلیل به چند مرحله غیرهم‌زمان تقسیم نمی‌کنیم. آنجا تراکنش محلی همچنان ابزار مناسب است.Workflow engine برای برداشت، refund یا تسویه طولانی می‌تواند timer، retry و وضعیت پایدار را سامان دهد. با این حال، اجرای دوباره activity هنوز باید امن باشد. داشتن موتور workflow، یک تماس بانکی غیرقابل‌تکرار را خودکار idempotent نمی‌کند.۲۰. همان پرداخت، این بار با crash و پیام تکراریحالا ببینیم اجزای طراحی در سناریوی اولیه چگونه کنار هم قرار می‌گیرند.شکل ۲ — Timeout به وضعیت نامشخص می‌رسد. استعلام، شواهد بیرونی را روشن می‌کند و موفقیت تأییدشده از مسیر ثبت یکتای داخلی عبور می‌کند؛ نتیجه مبهم به retry کورکورانه تبدیل نمی‌شود.۱. پیش از تماس با درگاه، Intent و attempt پایدار ساخته شده‌اند؛ بنابراین درخواست بی‌هویت نداریم.۲. پاسخ درگاه گم می‌شود. attempt نامشخص می‌ماند و کاربر به صفحه پیگیری همان Payment برمی‌گردد.۳. رویداد معتبر می‌رسد و در Inbox ذخیره می‌شود. Processor شواهد را با پرداخت تطبیق می‌دهد.۴. Ledger، موجودی مرتبط، وضعیت Payment و Outbox با هم commit می‌شوند. Worker قبل از ack از دسترس خارج می‌شود.۵. پیام دوباره می‌رسد. Inbox و هویت مستقل ثبت مالی مانع اثر دوباره می‌شوند.۶. Kafka موقتاً در دسترس نیست. Outbox باقی می‌ماند تا Dispatcher بعداً انتشار را انجام دهد. اگر انتشار تکرار شود، مصرف‌کننده‌ها هم باید تکرار را تحمل کنند.۷. Reconciliation نتیجه درگاه، Payment و ثبت Ledger را با هم تطبیق می‌دهد. پیامک ناموفق می‌تواند جداگانه retry شود؛ موفقیت مالی به موفقیت پیامک وابسته نیست.این جریان تضمین نمی‌کند هیچ خرابی رخ ندهد. تضمین مورد نظر این است که اثر مالی در مرز تعریف‌شده محافظت شود و کار باقی‌مانده هویت، وضعیت و مسیر بازیابی داشته باشد.۲۱. خرابی باید به اقدام مشخص برسدچند سناریوی دیگر طراحی را دقیق‌تر می‌کنند:دیتابیس وسط commit قطع می‌شود: نتیجه را با شناسه عملیات بررسی می‌کنیم. قطع اتصال را معادل rollback فرض نمی‌کنیم.Ledger نوشته می‌شود، اما به‌روزرسانی موجودی خطا می‌دهد: چون در یک تراکنش‌اند، هیچ‌کدام به‌تنهایی commit نمی‌شوند.یک event قدیمی پس از موفقیت می‌رسد: با قواعد transition و شواهد provider بررسی می‌شود؛ وضعیت موفق به‌خاطر ترتیب دریافت بازنویسی نمی‌شود.درگاه refund را انجام داده، اما پاسخ نرسیده است: ظرفیت refund رزرو می‌ماند و inquiry با reference قبلی ادامه پیدا می‌کند.Outbox عقب افتاده است: عمر قدیمی‌ترین پیام منتشرنشده اندازه‌گیری می‌شود. اگر backlog ظرفیت ذخیره‌سازی یا زمان قابل قبول را تهدید کند، محدودسازی ورودی بخشی از پاسخ عملیاتی است.Operator اصلاح اشتباه درخواست می‌کند: نقش مجاز، دلیل، reference و برای اقدام‌های حساس تأیید دوم لازم است. ابزار پشتیبانی حق تغییر مستقیم ثبت‌های نهایی را ندارد.برای این مسیرها، آزمون فقط happy path نیست. باید درخواست هم‌زمان با کلید یکسان، refundهای هم‌زمان، crash قبل و بعد از commit، پیام تکراری، پاسخ دیرهنگام و failover را آزمایش کنیم. این‌ها معیارهای پیشنهادی ارزیابی طراحی‌اند؛ کدهای مقاله پیاده‌سازی کامل یا آزموده‌شده یک محصول نیستند.۲۲. امنیت و تقلب، با مسئولیت‌های روشنAuthorization باید روی هر عملیات اعمال شود. داشتن payment_id نباید به کاربر دیگری اجازه دیدن پرداخت بدهد و فروشنده نباید به تسویه فروشنده دیگر دسترسی پیدا کند.مسیر دریافت داده کارت را تا حد امکان به provider معتبر بسپاریم و فقط token یا reference لازم را نگه داریم. این انتخاب به‌تنهایی اثبات انطباق با الزامات پرداخت نیست؛ دامنه الزامات به جریان واقعی داده و قراردادها بستگی دارد.کلیدهای درگاه باید با دسترسی محدود، rotation و سازوکار مدیریت secrets نگهداری شوند. داده‌های حساس نباید وارد log، payload رویدادهای عمومی یا خروجی ابزار پشتیبانی شوند. نگهداری Webhook و گزارش خام نیز به حداقل‌سازی داده، دسترسی کنترل‌شده و retention مشخص نیاز دارد.برای جلوگیری از سوءاستفاده، ایجاد پرداخت، انتقال و برداشت محدودیت متناسب می‌خواهند. در Webhook هم کنترل ورودی لازم است، اما محدودسازی باید با الگوی retry provider سازگار باشد تا رویدادهای معتبر را به‌طور گسترده از دست ندهیم.Risk engine می‌تواند برای گردش سریع شارژ و برداشت، مقصد مشکوک یا نشانه‌های تصاحب حساب، تصمیم REVIEW، احراز هویت تکمیلی یا تأخیر برداشت بدهد. اما تصمیم ریسک، اجازه شکستن invariantهای Ledger نیست. حتی مسدودکردن وجه هم باید وضعیت و دلیل قابل پیگیری داشته باشد.۲۳. تیم عملیات باید بداند چه چیزی در خطر استنمودار CPU برای عیب‌یابی مفید است، اما به‌تنهایی وضعیت مالی را توضیح نمی‌دهد. تیم باید بداند چه تعداد کاربر منتظر نتیجه‌اند، چه مبلغی در وضعیت نامشخص است و کدام provider بیشترین تأخیر را ایجاد کرده است.سنجه‌های مهم این طراحی عبارت‌اند از:تعداد، مبلغ و عمر Payment، Refund و Withdrawalهای نامشخص.اختلاف‌های Reconciliation به تفکیک علت و provider.شکست invariantهای Ledger و اختلاف projection.عمر قدیمی‌ترین Inbox پردازش‌نشده و Outbox منتشرنشده.نرخ timeout و خطای verify درگاه.lock wait، deadlock و تعارض‌های موجودی.اقلام تسویه‌ای که از پنجره مورد انتظار عبور کرده‌اند.Alert باید اقدام‌پذیر باشد. «پرداخت‌های نامشخص بیش از ده دقیقه با مبلغ در معرض ریسک رو به افزایش» مسئله مشخص‌تری از «تعداد خطا زیاد شده» نشان می‌دهد. آستانه‌ها هم باید بر اساس رفتار provider و حساسیت عملیات تعیین شوند.Application log برای تشخیص خطای اجراست. Audit trail برای توضیح اقدام مالی و مسئول آن است. یک correlation_id می‌تواند درخواست، attempt، رویداد و ثبت مالی را به هم وصل کند، اما به‌تنهایی جای تاریخچه مالی را نمی‌گیرد.دوام داده نیز به backup و بازیابی آزموده‌شده نیاز دارد. replication غیرهم‌زمان ممکن است هنگام failover آخرین writeها را از دست بدهد؛ بنابراین ادعای دوام باید با تنظیمات واقعی، مدل خرابی، RPO/RTO و تمرین restore سازگار باشد.۲۴. رشد سیستم از اندازه‌گیری شروع می‌شودپیش از sharding، queryهای واقعی، ایندکس‌ها، مدت تراکنش، connection pooling و حساب‌های پرترافیک را بررسی می‌کنیم. تاریخچه کاربر، پیدا کردن reference درگاه و خواندن Outbox هرکدام ایندکس متفاوتی می‌خواهند.Read replica برای گزارش و تاریخچه مناسب است، اگر تأخیر آن در قرارداد خواندن پذیرفته شده باشد. تصمیم خرج یا بررسی نتیجه بلافاصله پس از write نباید بی‌اطلاع از lag به replica فرستاده شود.Partitioning زمانی Ledger می‌تواند نگهداری و آرشیو را ساده‌تر کند، اما query طولانی یک account ممکن است همچنان چند partition را بخواند. یکتایی referenceهای مالی نیز باید با محدودیت‌های topology جدید سازگار بماند؛ partition کردن نباید محافظت در برابر ثبت تکراری را تضعیف کند.در Kafka، event_id برای deduplication، aggregate_id برای هویت موجودیت، aggregate_version برای تشخیص ترتیب منطقی و correlation_id برای پیگیری جریان مفیدند. Ordering درون partition را نباید ترتیب سراسری فرض کنیم؛ Publisher و consumer هم باید نسخه‌ها و ترتیب مورد نیاز دامنه را رعایت کنند.اگر روزی حساب‌ها روی shardهای جدا قرار گرفتند، انتقال بین دو حساب دیگر همان تراکنش محلی ساده نیست. به مدل رزرو، حساب واسط، هماهنگی و بازیابی تازه نیاز داریم. همچنین تمرکز همه فروش‌ها روی یک حساب درآمد مرکزی می‌تواند گلوگاه بسازد؛ جزئیات حسابداری و تجمیع باید این فشار را در نظر بگیرند.مسیر تکامل پیشنهادی چنین است: ابتدا هسته مالی درست با PostgreSQL، Inbox، Outbox و worker؛ سپس چند provider، ابزار تطبیق و تسویه قوی‌تر؛ و بعد، در صورت وجود فشار اندازه‌گیری‌شده، استخراج سرویس یا تغییر توزیع داده.۲۵. قرارداد API باید وضعیت واقعی را نشان دهدAPI کاربر را به نام‌های داخلی state machine وابسته نمی‌کند، اما نباید ابهام مالی را هم پنهان کند. ایجاد درخواست برداشت با انتقال موفق وجه تفاوت دارد.POST /v1/payments&#xA;GET  /v1/payments/{id}&#xA;POST /v1/payments/{id}/refunds&#xA;&#xA;GET  /v1/wallets/{id}&#xA;GET  /v1/wallets/{id}/transactions&#xA;POST /v1/wallets/{id}/transfers&#xA;POST /v1/withdrawals&#xA;&#xA;GET  /v1/merchants/{id}/settlements&#xA;POST /v1/webhooks/{provider}عملیات تغییردهنده مالی قرارداد idempotency دارند. اگر endpoint تأیید لازم است، کلاینت فقط درخواست بررسی می‌دهد؛ نمی‌تواند خودش CAPTURED را انتخاب کند.پاسخ موجودی می‌تواند زمان و نسخه داده را نیز نشان دهد:{&#xA;  &#34;posted_balance&#34;: 10000000,&#xA;  &#34;reserved_balance&#34;: 2000000,&#xA;  &#34;available_balance&#34;: 8000000,&#xA;  &#34;pending_incoming&#34;: 1500000,&#xA;  &#34;currency&#34;: &#34;IRR&#34;,&#xA;  &#34;version&#34;: 42,&#xA;  &#34;as_of&#34;: &#34;2026-09-11T10:00:00Z&#34;&#xA;}این نمونه همان اعداد مثال کیف پول است، اما در واحد ذخیره‌سازی ریال. برای انتقالی که هنوز بررسی می‌شود، پاسخ پذیرش به همراه شناسه و مسیر پیگیری مناسب‌تر از اعلام موفقیت زودهنگام است.جمع‌بندیکاربر ابتدای مقاله یک میلیون تومان پرداخت کرده بود و پاسخ روشنی ندیده بود. طراحی خوب باید بتواند بعداً توضیح دهد آن پرداخت به کدام Intent مربوط بوده، درگاه چه شواهدی داده، کدام ثبت مالی ایجاد شده و آیا وجه با فروشنده تسویه یا به کاربر بازگردانده شده است.برای رسیدن به این پاسخ، Ledger متوازن و تغییرناپذیر، موجودی قابل بازسازی، کنترل concurrency، هویت پایدار عملیات و stateهای قابل بازیابی لازم‌اند. Outbox و Inbox فاصله میان commit و پیام‌رسانی را مدیریت می‌کنند و Reconciliation اختلاف با سیستم‌های بیرونی را دنبال می‌کند.ارزش این معماری زمانی مشخص می‌شود که پاسخ گم شده، پیام دوباره رسیده یا Worker از دسترس خارج شده است. در چنین شرایطی، سیستم باید هنوز بتواند حرکت پول را توضیح دهد و اقدام بعدی را با شواهد انتخاب کند.همین قابلیت، یک مجموعه API پرداخت را به هسته مالی قابل اتکا نزدیک می‌کند.منابع فنیStripe — Webhooks: امضا، پردازش رویدادهای تکراری و ترتیب دریافت.Stripe — Idempotent requests: قرارداد کلید و پارامترهای درخواست.PostgreSQL — Explicit Locking: قفل سطری، تعارض و deadlock.Modern Treasury — Digital Wallet: حساب‌های دارایی و بدهی در مثال کیف پول.Microservices.io — Transactional Outbox: ثبت قصد انتشار همراه تراکنش و احتمال ارسال تکراری.</description><category>افشین یاری نیا</category><author>افشین یاری نیا</author><pubDate>Fri, 11 Sep 2026 20:39:46 +0330</pubDate></item><item><title>طراحی High-Level یک پلتفرم AdTech شبیه تپسل</title><link>https://virgool.ir/@m_58197875/طراحی-high-level-یک-پلتفرم-adtech-شبیه-تپسل-ile5k9rhswub</link><description>Scope disclaimer: این مقاله یک design آموزشی و پیشنهادی برای یک mobile/web ad network در مقیاس بالا است. هرجا از «تپسل‌مانند» استفاده می‌شود، منظور قابلیت‌هایی است که عموماً به‌صورت عمومی برای محصولات AdTech شناخته می‌شوند؛ این متن ادعایی دربارهٔ internal architecture، فناوری‌های دقیق، حجم ترافیک، قراردادها یا فرایندهای عملیاتی شرکت تپسل ندارد.محتوای تبلیغاتی == Creativeمقدمه — Introductionیک Ad Network در ظاهر ساده است: اپلیکیشن یک جایگاه تبلیغاتی دارد، درخواست می‌فرستد و یک تبلیغ نمایش داده می‌شود. اما پشت این پاسخ چند ده‌میلی‌ثانیه‌ای، یک سیستم تصمیم‌گیری بلادرنگ قرار دارد که باید هم‌زمان بهترین کمپین را انتخاب کند، بودجه را بی‌رویه مصرف نکند، یک کاربر را با تبلیغ تکراری خسته نکند، فایل خلاقه را سریع تحویل دهد و رویدادهای مالی قابل اتکا ثبت کند.هدف این طراحی، ساخت یک پلتفرم چندطرفه است:تبلیغ‌دهنده کمپین، مخاطب، بودجه و هدف خود را تعریف می‌کند.ناشر با SDK یا تگ وب، جایگاه تبلیغ را در محصولش عرضه می‌کند.کاربر نهایی تبلیغ را دریافت، مشاهده یا روی آن کلیک می‌کند.پلتفرم میان عرضه و تقاضا تصمیم می‌گیرد، تحویل را اندازه می‌گیرد و تسویه را انجام می‌دهد.تمرکز ما روی مسیر Serving است، نه روی UI پنل. هر تصمیم باید کم‌تاخیر، قابل حسابرسی و مقاوم در برابر شکست باشد1. Domain و Requirementsقابلیت‌های عمومی محصول — Public product capabilitiesدر یک پلتفرم AdTech معمولاً انتظار می‌رود این قابلیت‌ها وجود داشته باشد:ایجاد کمپین CPC، CPM، CPA یا CPI؛ زمان‌بندی، بودجه و سقف هزینهفرمت‌های banner، interstitial، native و rewarded videoهدف‌گیری بر اساس کشور، شهر، دستگاه، سیستم‌عامل، نسخهٔ اپ، دسته‌بندی اپ و سگمنت مخاطبگزارش impression، click، conversion، هزینه و درآمد ناشرکنترل frequency capping و budget pacingمدیریت لینک مقصد و tracking URLmediation یا waterfall/biddingbilling تبلیغ‌دهنده و تسویه با ناشرمعماری پیشنهادیمواردی مانند Kafka، Redis، ClickHouse، نحوهٔ امتیازدهی، schema رویدادها و مدل consistency، پیشنهاد معماری هستند. پیاده‌سازی واقعی می‌تواند با ابزارها و مرزهای سرویس کاملاً متفاوت باشد.نیازمندی‌های عملکردی — Functional requirementsپاسخ به ad request با یک adیا پاسخ no-fill.اعمال eligibility: وضعیت کمپین، تارگتینگ، budget، cap و محدودیت جایگاه.ثبت قابل‌اعتماد impression، click، conversion و رویدادهای rewarded.نگهداری هزینهٔ تبلیغ‌دهنده و درآمد ناشر به‌صورت قابل حسابرسی.پشتیبانی از reporting نزدیک به real-time و گزارش مالی نهایی.امکان pause/resume، تغییر budget و آپلود ad در پنل.نیازمندی‌های غیرعملکردی — Non-functional requirementsتاخیر پاسخ Serving- هدف پیشنهادی: p99 کمتر از 150ms در منطقهٔ نزدیک کاربر، بدون احتساب دانلود creativeدسترس‌پذیری- هدف پیشنهادی: 99.95% برای مسیر Servingصحت مالی- هدف پیشنهادی: بدون دوبارشماریِ قابل پرداخت؛ reconciliable ledgerمقیاس‌پذیری- هدف پیشنهادی: رشد افقی serving و ingestion مستقل از پنلحریم خصوصی- هدف پیشنهادی: حداقل‌سازی داده، consent-aware targeting و retention محدودقابلیت مشاهده- هدف پیشنهادی: trace انتهابه‌انتها برای نمونه‌برداری‌شده و metric2. فرض‌های مقیاس — Scale assumptionsاین اعداد صرفاً برای شکل‌دادن طراحی‌اند، نه آمار هیچ محصول واقعی:50 هزار درخواست تبلیغ در ثانیه در اوج (50k RPS)15 هزار event در ثانیه برای impression/click، با burst بالاتر10 میلیون کاربر فعال روزانه و صدها هزار کمپین فعال2 تا 5 میلیون creative در CDN، اما تعداد محدودی hotretention خام event: 30 روز؛ دادهٔ تجمیعی: 24 ماه یا مطابق سیاست حقوقیدر 50k RPS، حتی 2ms اضافه در چند dependency مهم است. بنابراین مسیر تصمیم نباید برای هر درخواست به دیتابیس transactional متکی باشد.3. معماری سطح‌بالا — High-level architectureاصل مهم این است که control plane و data plane جدا باشند:Control plane: پنل، تعریف کمپین، approval creative، قرارداد، صورتحساب و تغییر تنظیمات. این بخش consistency قوی‌تری می‌خواهد اما latency آن حساس نیست.Data plane: انتخاب و تحویل تبلیغ و ingestion event. این بخش بسیار پرترافیک است و بر cache، index در حافظه و صف رویداد تکیه دارد.4. مسئولیت اجزا — Component responsibilities5. جریان Ad requestSDK اطلاعات حداقلی موردنیاز را می‌فرستد: شناسهٔ app و placement، قالب موردنیاز، context، شناسهٔ session/کاربرِ مجاز، consent، device capabilities و یک request ID یکتا.مراحل پیشنهادی:Gateway امضای app key، timestamp و rate limit را بررسی می‌کند.Serving API placement را می‌شناسد، schema را validate و context ناامن را حذف می‌کند.Candidate retrieval با index بر اساس placement، کشور، فرمت و platform انجام می‌شود.فیلترها: campaign active، زمان، creative approved، targeting، consent، inventory rule، budget و frequency cap.Decision Engine کاندیداهای باقی‌مانده را rank می‌کند.برای برنده reservation کوتاه‌مدت گرفته می‌شود؛ اگر نشد، سراغ نفر بعدی می‌رود.پاسخ شامل creative CDN URL، expiry، tracking token، impression policy و اطلاعات rewarded در صورت نیاز است.no-fill یک خطا نیست؛ پاسخ عادی است و باید علت آن (مثلاً NO_ELIGIBLE_CAMPAIGN) به شکل کنترل‌شده برای عملیات ثبت شود، نه لزوماً برای SDK افشا شود.6. مدیریت Campaign و Indexingسیستم مدیریت، source of truth را در یک DB transactional نگه می‌دارد. تغییرات کمپین با الگوی outbox منتشر می‌شوند تا فاصلهٔ بین commit دیتابیس و publish event باعث گم‌شدن update نشود. یک indexer، نسخهٔ denormalized کمپین را می‌سازد و به cache/index هر region می‌رساند.تغییرات control plane ممکن است تا چند ثانیه eventual-consistent باشند. برای pause اضطراری، یک kill switch کوچک و strongly-consistent در Redis/edge config در نظر گرفته می‌شود تا کمپین بلافاصله از serving خارج شود.7. Targeting و DecisioningTargeting باید دو مرحله‌ای باشد:Pre-filter ارزان: placement، format، کشور، OS، app bundle و زمان؛ با bitmap/index معکوس.Predicate دقیق: نسخهٔ اپ، نوع اتصال، سگمنت، allow/deny list و قواعد پیچیده.امتیاز پیشنهادی می‌تواند ترکیبی از bid موثر، کیفیت پیش‌بینی‌شده، pacing و quality باشد:score = effective_bid × predicted_action_rate × pacing_multiplier × quality_multiplierاین فرمول یک نمونه است. اگر هدف CPM است، expected revenue می‌تواند نزدیک به bid باشد؛ اگر CPC/CPI است، باید احتمال کلیک یا نصب پیش‌بینی شود. مدل ML نباید تنها معیار باشد: policyهای budget، brand safety و fairness باید hard constraint بمانند.Pacing و Budgetبدون pacing، کمپین‌های پرتقاضا ابتدای روز بودجه را تمام می‌کنند. برای هر کمپین، بودجهٔ روزانه به سهم‌های زمانی تقسیم می‌شود و pacing_multiplier با توجه به spend واقعی نسبت به هدف تنظیم می‌گردد.در مسیر serving یک soft reservation با TTL کوتاه ثبت می‌شود.با impression معتبر، reservation به هزینهٔ واقعی تبدیل می‌شود.reservationهای منقضی‌نشده با job جبرانی آزاد یا reconcile می‌شوند.برای کمپین‌های کوچک، یک counter اتمیک Redis کافی است؛ برای دقت مالی، ledger نهایی مرجع است.Frequency cappingکلید پیشنهادی: cap:{campaign_id}:{user_or_device_id}:{window}. مقدار counter و expiry انتهای window را دارد. برای anonymous traffic، باید consent و سیاست platform تعیین کند که استفاده از device identifier مجاز است یا نه. در عدم دسترسی cap store، سیاست محافظه‌کارانه (عدم نمایش آن کمپین) از over-delivery بهتر است؛ اما می‌توان fallback سطح placement با TTL کوتاه داشت.8. تحویل Creativeفایل‌های حجیم هرگز از Serving API عبور نمی‌کنند. creative پس از malware scan، محدودیت اندازه، validation فرمت و approval، در object storage قرار می‌گیرد و از CDN تحویل می‌شود.URL باید نسخه‌دار باشد تا cache invalidation با نام جدید حل شود.پاسخ serving فقط metadata ضروری و URL امن/دارای expiry مناسب را دارد.برای video، manifest و bitrateهای سازگار آماده می‌شوند.SDK باید timeout، preload و fallback creative را کنترل کند.9. Event tracking، Analytics و Attributionرویدادهای impression، click، video milestones، reward completion و conversion جدا از Serving API ingest می‌شوند. SDK نباید منتظر processing سنگین بماند؛ server باید سریع 202 Accepted بدهد.هر event شامل event_id، request_id، impression_id (در صورت وجود)، occurred_at، campaign/creative/placement، app، نوع event، نسخهٔ SDK و provenance است. سرور identityهای قابل اعتماد را از tracking token استخراج می‌کند؛ هرگز campaign_id ارسال‌شدهٔ کلاینت را به‌تنهایی مبنای شارژ قرار نمی‌دهد.Attribution نصب/خرید معمولاً delayed و چندمنبعی است. ابتدا یک attribution provisional ثبت می‌کنیم و پس از اعمال window، dedupe و ruleهای شریک اندازه‌گیری، نتیجه را final می‌کنیم. گزارش real-time ممکن است با ledger final تفاوت کوچک و موقت داشته باشد.10. Billing و تسویهٔ Publisherبرای پول، aggregate قابل ویرایش کافی نیست. یک دفترکل immutable، دوطرفه و قابل audit نگه می‌داریم:با event قابل پرداخت، حساب advertiser debit و حساب platform/escrow credit می‌شود.سهم ناشر با قرارداد، revenue share، مالیات و adjustment محاسبه می‌شود.تسویه پس از دورهٔ اعتراض، اعتبارسنجی تقلب و آستانهٔ پرداخت ایجاد می‌شود.spend_total در dashboard یک projection است؛ حقیقت مالی، سطرهای ledger و وضعیت reconciliation است. chargeback، invalid traffic و اصلاح attribution با entry جبرانی ثبت می‌شوند، نه با update مخفی روی تاریخچه.11. Mediationوقتی direct demand مناسب نیست، mediation می‌تواند چند شبکهٔ خارجی را در waterfall یا bidding فراخوانی کند. adapterها باید isolation داشته باشند: timeout کوتاه، circuit breaker، محدودیت concurrency و cache تنظیمات شریک.ترتیب پیشنهادی:کمپین‌های direct با قرارداد/اولویت ویژهbidding یا شبکه‌های mediation با floor pricehouse ads یا no-fillلازم است هر پاسخ mediation به request و eventهای بعدی قابل ردیابی باشد، ولی دادهٔ شخصی غیرضروری ارسال نشود.12. Fraud و کیفیت ترافیک‫نمونه‌های ریسک: click farm، emulator، spoofed SDK، replay event، fake install و bot trafficامضای request و token کوتاه‌عمر برای اتصال event به ad responserate limit و replay protection با event_idruleهای stream: CTR غیرعادی، burst، ترتیب ناممکن رویدادها، IP/ASN مشکوکreputation برای app/publisher/device و مدل risk scorequarantine برای event مشکوک و امکان manual reviewwithholding یا adjustment مالی فقط با مسیر حسابرسی‌شدهFalse positive مستقیماً به درآمد ناشر آسیب می‌زند؛ پس ruleها باید قابل توضیح، نسخه‌دار و قابل اعتراض باشند.13. مدل داده — Data model14. Sample APIsPOST /v1/ad-requests&#xA;X-App-Key: ...&#xA;Idempotency-Key: 5e7...&#xA;&#xA;{&#xA;  &#34;request_id&#34;: &#34;req_...&#34;,&#xA;  &#34;placement_id&#34;: &#34;plc_rewarded_01&#34;,&#xA;  &#34;format&#34;: &#34;rewarded_video&#34;,&#xA;  &#34;session_id&#34;: &#34;...&#34;,&#xA;  &#34;consent&#34;: {&#34;personalized_ads&#34;: true},&#xA;  &#34;device&#34;: {&#34;os&#34;: &#34;android&#34;, &#34;app_version&#34;: &#34;4.2.0&#34;}&#xA;}POST /v1/events&#xA;{&#xA;  &#34;event_id&#34;: &#34;evt_...&#34;,&#xA;  &#34;type&#34;: &#34;impression&#34;,&#xA;  &#34;tracking_token&#34;: &#34;signed-short-lived-token&#34;,&#xA;  &#34;occurred_at&#34;: &#34;2026-08-04T10:15:30Z&#34;&#xA;}مدیریت کمپین نیز APIهای CRUD دارد، اما update آن باید با version یا ETag از lost update جلوگیری کند. endpointهای مالی فقط از back-office و با authorization قوی قابل دسترسی‌اند.15. Kafka topics پیشنهادیOrdering فقط درون یک partition معنا دارد. برای eventهای یک impression، کلید impression_id مناسب‌تر از campaign_id است؛ برای aggregation کمپین، repartition در stream processor انجام می‌شود.16. Caching و انتخاب StorageCache invalidation با versioning حل می‌شود: response/index یک campaign_version دارد و consumer فقط update جدیدتر را اعمال می‌کند. TTL تنها ابزار صحت نیست.17. Consistency، Idempotency و مدیریت failureServing سریع و غالباً eventual-consistent است؛ پول و settlement باید قوی تر باشند. این مرز را آگاهانه تعریف کنید.18. Security و PrivacymTLS یا امضای HMAC بین SDK/gateway در کنار app attestation در صورت پشتیبانی platformOAuth/OIDC و RBAC برای پنل؛ MFA برای عملیات مالیرمزنگاری در transit و at rest؛ secret manager و rotation کلیدهاvalidation سخت input، WAF، rate limit و جداسازی tenantهاconsent propagation تا decisioning و شریک mediationdata minimization، retention schedule، deletion/export workflow و access logعدم ثبت identifier حساس یا query parameterهای کامل در logهای عمومیالزامات قانونی به کشور، بازار و نقش سازمان بستگی دارد؛ طراحی باید با تیم حقوقی/حریم خصوصی نهایی شود، نه با یک فرض عمومی.19. Observability و عملیاتMetricهای ضروری:RPS، p50/p95/p99 latency، fill rate، no-fill reason، candidate count، cache hit، timeout mediation، budget reservation failure، lag Kafka، event dedupe rate، invalid traffic rate، ledger reconciliation delta و payout backlog.هر request_id باید بتواند در trace نمونه‌برداری‌شده مسیر gateway → decision → reservation → response → event را نشان دهد. logها ساخت‌یافته، دارای correlation ID و scrubbed از PII هستند.20. Deployment و ResilienceServing به‌صورت stateless در چند AZ، با autoscaling بر اساس RPS/latencyKafka، Redis و DB با replicationcreative در object storage چندمنطقه‌ای و CDNrouting نزدیک‌ترین region؛ دادهٔ مالی و control plane با replication کنترل‌شدهdeploy تدریجی: canary، feature flag، shadow traffic و rollback سریعrunbook برای توقف کمپین، lag stream، failure شریک mediation و reconciliationبرای جلوگیری از blast radius، poolهای ناشر/فرمت/region را تا حد معقول isolate کنید. یک ناشر یا شریک کند نباید کل decision engine را معطل کند.21. گلوگاه‌ها و Trade-offsدقت budget در برابر latency: رزرو synchronous دقیق‌تر است اما dependency اضافه می‌آورد؛ allocation دوره‌ای سریع‌تر است اما over-delivery بیشتری دارد.targeting پیچیده در برابر cacheability: ruleهای آزاد و SQL-like انعطاف می‌دهند اما hot path را کند می‌کنند؛ DSL محدود و indexable معمولاً انتخاب بهتری است.real-time dashboard در برابر حقیقت مالی: نمایش سریع از stream aggregate، و settlement از ledger نهایی.frequency cap دقیق در برابر availability: cross-device cap دقیق به identity قوی نیاز دارد؛ در نبود consent باید expectations محصول تغییر کند.mediation breadth در برابر latency: شریک بیشتر ممکن است fill را بالا ببرد، اما timeout و complexity را هم افزایش می‌دهد.22. مسیر تکامل — Evolution planابتدا correctness مسیر impression تا ledger را ثابت کنید؛ سپس ranking و ML را اضافه کنید. سیستم بسیار هوشمند با eventهای غیرقابل اعتماد، تنها خطای خود را سریع‌تر بزرگ می‌کند.جمع‌بندی — Conclusionیک پلتفرم AdTech موفق فقط «انتخاب تبلیغ» نیست؛ یک سیستم distributed است که تصمیم کم‌تاخیر، کنترل هزینه، اندازه‌گیری قابل اعتماد و تسویهٔ قابل حسابرسی را کنار هم قرار می‌دهد. جداسازی control plane از serving، استفاده از index و cache، event log پایدار، idempotency سرتاسری و ledger immutable ستون‌های اصلی این طراحی هستند.اگر این اصول درست پیاده شوند، قابلیت‌های پیشرفته‌تری مانند bidding، ML ranking، fraud detection و multi-region serving به تکامل طبیعی سیستم تبدیل می‌شوند، نه لایه‌هایی شکننده روی یک هستهٔ نامطمئن</description><category>افشین یاری نیا</category><author>افشین یاری نیا</author><pubDate>Tue, 04 Aug 2026 21:09:03 +0330</pubDate></item><item><title>طراحی سیستم یک پلتفرم Q-commerce در ابعاد گسترده</title><link>https://virgool.ir/@m_58197875/طراحی-سیستم-یک-پلتفرم-q-commerce-در-ابعاد-گسترده-t0v2lf8c3yjt</link><description>در نگاه اول، تجارت سریع یا Q-commerce شبیه همان E-commerce معمولی است، با این تفاوت که زمان تحویل بسیار کوتاه‌تر شده است. مشتری کالاهای سوپرمارکتی را انتخاب می‌کند، هزینه را می‌پردازد و سفارش را ظرف حدود سی دقیقه تحویل می‌گیرد.اما وقتی این سرویس در چندین شهر و منطقه فعالیت می‌کند، دیگر صرفاً با یک فروشگاه اینترنتی روبه‌رو نیستیم. اینجا با یک سیستم هماهنگی بلادرنگ سروکار داریم.پلتفرم باید تقاضای مشتری، موجودی فروشگاه، قیمت محصولات، تخفیف‌ها، ظرفیت نیروهای جمع‌آوری سفارش، تعداد پیک‌های در دسترس، وضعیت ترافیک، درگاه‌های پرداخت و عملیات پشتیبانی را هم‌زمان هماهنگ کند. هرکدام از این بخش‌ها مستقل از دیگری تغییر می‌کنند. با این حال، مشتری تنها یک انتظار ساده دارد: سفارش باید در زمان وعده‌داده‌شده، با کالاهای درست و قیمت صحیح به دست او برسد.پرسش اصلی طراحی این است:چگونه موجودی را با دقت نمایش دهیم، کالاها را بدون فروش بیش از موجودی رزرو کنیم، پرداخت را قطعی کنیم، سبد را در فروشگاه آماده کنیم و سفارش را ظرف چند دقیقه تحویل دهیم، در حالی که تمام اجزای فیزیکی و دیجیتال سیستم دائماً در حال تغییر هستند؟یک مثال ثابت را در طول مقاله در نظر می‌گیریم. مشتری‌ای در تهران از یک فروشگاه نزدیک، شیر، نان، میوه و مواد غذایی منجمد سفارش می‌دهد. اپلیکیشن همه اقلام را موجود نمایش می‌دهد. پرداخت موفق است. سپس فروشگاه ناگهان با موجی از سفارش‌ها مواجه می‌شود. نیروی جمع‌آوری سفارش نمی‌تواند شیر انتخاب‌شده را پیدا کند. هم‌زمان، بارندگی شدید باعث کاهش تعداد پیک‌های در دسترس می‌شود.یک طراحی واقعی باید این سفارش را مدیریت کند، بدون اینکه موجودی بیش از حد فروخته شود، وضعیت پرداخت گم شود، خطا از دید عملیات پنهان بماند، یا وعده تحویل بدون اطلاع مشتری نقض شود.این مقاله معماری فرضی یک پلتفرم مشابه اسنپ‌مارکت را طراحی می‌کند. رفتار عمومی محصول فقط برای ایجاد زمینه استفاده شده است. معماری، اعداد مقیاس و انتخاب‌های فنی این مقاله جنبه آموزشی دارند و زیرساخت خصوصی اسنپ‌مارکت را توصیف نمی‌کنند.۱. تعریف مسئلههدف، طراحی یک پلتفرم Q-commerce برای سرویس‌دهی گسترده در شهرهای مختلف و فروش مواد غذایی و کالاهای مصرف روزمره است.مشتری یک آدرس را انتخاب می‌کند، فروشگاه‌های قابل‌سرویس را می‌بیند، در کاتالوگ مخصوص همان فروشگاه جست‌وجو می‌کند، سبد خرید می‌سازد، قیمت و زمان تحویل دریافت می‌کند، پرداخت را انجام می‌دهد، مراحل جمع‌آوری و ارسال را دنبال می‌کند و سفارش را در یک بازه زمانی کوتاه تحویل می‌گیرد.تجربه عمومی خرید سوپرمارکتی اسنپ شامل فروشگاه‌های نزدیک، تحویل فوری، تحویل زمان‌بندی‌شده از هایپرمارکت، تخفیف، پرداخت آنلاین، پرداخت اعتباری، مقایسه قیمت و رهگیری سفارش است.پلتفرم باید پنج مسئله اصلی را حل کند:فروشگاه، کاتالوگ، قیمت و موجودی درست را برای موقعیت مکانی مشتری نمایش دهد.هنگام تسویه‌حساب‌های هم‌زمان، از فروش بیش از موجودی جلوگیری کند.پرداخت، ایجاد سفارش، رزرو موجودی و ظرفیت فروشگاه را بدون تراکنش توزیع‌شده هماهنگ کند.خطاهای عملیات فیزیکی مانند نبود کالا، وزن متفاوت و جایگزینی محصول را مدیریت کند.پیک مناسب را تخصیص دهد و با وجود تغییر شرایط عملیاتی، زمان تحویل واقع‌بینانه‌ای ارائه کند.در Q-commerce زمان جبران خطا بسیار کمتر از E-commerce سنتی است. ناسازگاری‌ای که ده دقیقه ادامه پیدا کند، شاید برای بسته‌ای که سه روز بعد تحویل می‌شود قابل‌تحمل باشد. همان ناسازگاری ممکن است کل بودجه زمانی یک سفارش فوری سوپرمارکتی را مصرف کند.۲. محدوده طراحی (scope)این طراحی بخش‌های زیر را پوشش می‌دهد:احراز هویت با شماره موبایل و مدیریت نشست‌ها(sessions)آدرس‌های مشتری و موقعیت جغرافیاییتشخیص قابلیت سرویس‌دهی و یافتن فروشگاه‌های نزدیککاتالوگ مرجع محصولاتتنوع کالا، قیمت، تخفیف و موجودی مخصوص هر فروشگاهجست‌وجو و مرور دسته‌بندی‌هاسبد خرید و پیشنهاد قیمت نهاییرزرو موجودیاعتبارسنجی تخفیف‌ها و کمپین‌هاپرداخت آنلاین، کیف پول و پرداخت اعتباریچرخه عمر سفارشپذیرش سفارش توسط فروشگاهجمع‌آوری، بسته‌بندی، کالای ناموجود و جایگزینیتخصیص پیک و رهگیری موقعیتتخمین زمان تحویللغو، بازپرداخت جزئی و بازپرداخت کاملیکپارچه‌سازی با شرکای تجاریابزارهای پشتیبانی، مالی و عملیاترویدادها، تحلیل داده، لاگ ممیزی و تطبیق اطلاعاتموارد زیر در محدوده نسخه اول (MVP) قرار ندارند:سبد فوری شامل کالا از چند فروشگاهفعالیت بین‌المللیبرنامه‌ریزی کامل زنجیره تأمین خرده‌فروشیربات‌های انبارداریجایگزینی کامل تمام سیستم‌های POS یا ERP فروشگاه‌هاتضمین دقیق زمان تحویل در تمام شرایط ترافیکی و آب‌وهواییپیاده‌سازی عمیق مدل پیشنهاد محصولمحدودیت تک‌فروشگاهی بودن سبد، یک تصمیم مهم محصولی است. این تصمیم مالکیت موجودی، آماده‌سازی سفارش، ارسال، بازپرداخت و ارتباط با مشتری را در یک واحد عملیاتی نگه می‌دارد.۳. Functional Requirementsتجربه مشتریورود با شماره موبایل و رمز یک‌بارمصرفایجاد و مدیریت آدرس‌هاانتخاب موقعیت روی نقشهمشاهده فروشگاه‌هایی که آدرس انتخاب‌شده را پوشش می‌دهندمرور دسته‌بندی‌ها و جست‌وجوی محصولاتمشاهده قیمت، تخفیف و موجودی مخصوص هر فروشگاهافزودن، حذف و تغییر تعداد اقلام سبدتعیین ترجیح مشتری برای جایگزینی کالااعمال کد تخفیف یا کمپیندریافت قیمت نهایی، هزینه‌ها و زمان تخمینی تحویلپرداخت با روش‌های پشتیبانی‌شدهرهگیری وضعیت سفارش تقریباً به‌صورت بلادرنگلغو سفارش در صورت اجازه سیاست‌هابررسی کالاهای جایگزین و اقلام ناموجوددریافت بازپرداخت جزئی یا کاملارتباط با پشتیبانی و امتیازدهی به سفارشعملیات فروشگاه و نیروی جمع‌آوری سفارشتنظیم ساعات کاری و محدوده سرویس‌دهی فروشگاههمگام‌سازی نگاشت محصولات، قیمت و موجودیتنظیم موجودی اطمینان و محدودیت تعداد خریدتنظیم ظرفیت جمع‌آوری سفارشپذیرش یا رد سفارشتخصیص نیروی جمع‌آوری سفارشاسکن و تأیید هر کالاگزارش کالای ناموجود یا آسیب‌دیدهپیشنهاد کالای جایگزینثبت وزن واقعی کالاهای وزنیبسته‌بندی و اعلام آمادگی برای تحویل به پیکتوقف موقت فروشگاه هنگام فشار عملیاتیعملیات پیکآنلاین و آفلاین شدنارسال موقعیت و وضعیت دسترس‌پذیریدریافت پیشنهاد مأموریت ارسالپذیرش یا رد مأموریتمسیریابی تا فروشگاهتأیید دریافت سفارشمسیریابی تا مشتریتأیید تحویل با روش مورد تأیید سیستمگزارش ناموفق بودن تحویل یا در دسترس نبودن مشتریعملیات پلتفرمتنظیم مناطق، محدوده‌ها، فروشگاه‌ها، سیاست‌ها و کمپین‌هابررسی سفارش و وضعیت پرداختاجرای مجدد مراحل امن فرایندانجام بازپرداخت و جبران خسارت در محدوده سیاست‌های مجازتطبیق پرداخت‌ها، سفارش‌ها، موجودی و تسویه‌هاپایش ظرفیت فروشگاه و پیکمدیریت تقلب و سوءاستفادهثبت و ممیزی اقدامات حساس کارکنان۴. Non-Functional Requirementsزمان درگاه پرداخت خارجی در هدف ایجاد داخلی سفارش محاسبه نمی‌شود.دسترس‌پذیری(availability)هدف دسترس‌پذیری مرور و جست‌وجو ۹۹٫۹۵ درصد است.هدف دسترس‌پذیری سبد و تسویه‌حساب ۹۹٫۹۹ درصد است.هدف دسترس‌پذیری رهگیری سفارش و عملیات فروشگاه ۹۹٫۹۵ درصد است.تحلیل داده، پیشنهاد محصول و اعلان‌های غیرحیاتی نباید مسیر تسویه‌حساب یا آماده‌سازی سفارش را مسدود کنند.consistencystrong consistency برای موارد زیر لازم است:رزرو موجودیمحدودیت استفاده از کمپین‌هارزرو ظرفیت فروشگاهانتقال وضعیت سفارشوضعیت پرداخت و بازپرداختجمع‌های مالیeventual consistency برای موارد زیر قابل‌قبول است:اسناد جست‌وجوپیشنهاد محصولاتتحلیل دادهمحبوبیت محصولاتنمای تاریخچه سفارش‌هاامتیاز فروشگاه‌هاdata persistenceسفارش تأییدشده، پرداخت موفق، بازپرداخت، رزرو موجودی یا رکورد تسویه باید پس از خرابی پردازش یا راه‌اندازی مجدد زیرساخت نیز باقی بماند.امنیت و حریم خصوصیترافیک شبکه و داده‌های حساس ذخیره‌شده رمزنگاری شوند.دسترسی به شماره موبایل و آدرس تحویل به حداقل برسد.آدرس کامل مشتری تا پیش از پذیرش مأموریت برای پیک پنهان بماند.تمام اقدامات حساس کارکنان ثبت شوند.روی مسیرهای OTP، سبد، تسویه‌حساب، کد تخفیف و پشتیبانی محدودیت نرخ اعمال شود.اطلاعات خام کارت بانکی ذخیره نشود.۵. فرضیات مقیاسیک میلیون سفارش روزانه، میانگین کمی ایجاد می‌کند:1,000,000 / 86,400 ≈ 11.6 سفارش در ثانیهاین میانگین بار واقعی را پنهان می‌کند. تقاضای خرید سوپرمارکتی در عصرها، آخر هفته‌ها، تعطیلات، کمپین‌ها، شرایط بد آب‌وهوایی و رویدادهای محلی افزایش می‌یابد.با ضریب اوج ۲۵ :11.6 × 25 ≈ 290 درخواست تسویه‌حساب در ثانیهحجم سفارش، پرترافیک‌ترین مسیر سیستم نیست. جست‌وجو، مرور دسته‌بندی، یافتن فروشگاه، نمایش موجودی، فعالیت سبد خرید و به‌روزرسانی موقعیت پیک، بار بسیار بیشتری ایجاد می‌کنند.پلتفرم همچنین نقاط داغ جغرافیایی دارد. الگوی ترافیک و تراکم پیک در تهران با یک شهر کوچک متفاوت است. منطقه، شهر، فروشگاه و بازه زمانی، همگی روی برنامه‌ریزی ظرفیت اثر می‌گذارند.۶. مدل دامنه (domain model) و چرخه عمر سفارشموجودیت‌های اصلی عبارت‌اند از:CustomerAddressRegionServiceZoneStoreProductSKUStoreOfferInventoryBalanceInventoryReservationCartPriceQuotePromotionRedemptionOrderOrderItemPaymentIntentRefundFulfillmentTaskSubstitutionRequestDeliveryTaskCourierSupportCaseSettlementEntryمهم‌ترین مفهوم سیستم، یک محصول عمومی نیست.StoreOffer = Store + SKU + Price + Promotion + Availability + Fulfillment Rulesیک SKU مشخص از شیر، در هر شعبه قیمت، موجودی، محدودیت خرید، تخفیف و امتیاز اطمینان متفاوتی دارد.سفارش یک state machine است، نه یک row قابل‌تغییر با یک رشته وضعیت مبهم.Invariants حیاتیInvariant، قانونی است که هنگام Retry، درخواست هم‌زمان و خرابی بخشی از سیستم نیز باید همواره برقرار بماند.هر سفارش فوری فقط به یک فروشگاه تعلق دارد.هر سفارش تأییدشده، دقیقاً یک پیشنهاد قیمت معتبر و یک مجموعه رزرو موجودی قطعی دارد.مقدار رزروشده هیچ‌گاه از موجودی قابل‌فروش بیشتر نمی‌شود.هر کلید Idempotency حداکثر یک نتیجه تسویه‌حساب ایجاد می‌کند.هر تراکنش تأییدشده درگاه، حداکثر یک PaymentIntent را بستانکار می‌کند.مجموع بازپرداخت‌های موفق از مبلغ پرداخت‌شده بیشتر نمی‌شود.فقط Order Service وضعیت سفارش را تغییر می‌دهد.فقط Inventory Service موجودی و رزروهای مرجع را تغییر می‌دهد.پیک فقط پس از تخصیص مأموریت، آدرس کامل تحویل را می‌بیند.هر اصلاح مالی یا عملیاتی، یک رکورد ممیزی ایجاد می‌کند.این قوانین، محدودیت‌های دیتابیس، تراکنش‌ها، APIها و فرایندهای بازیابی را شکل می‌دهند.۷. معماری سطح بالا ( high level design)پلتفرمی که در شهرها و مناطق مختلف فعالیت می‌کند، به مالکیت دامنه روشن و مسیرهای مقیاس‌پذیری مستقل نیاز دارد. این موضوع به معنای ساخت ده‌ها سرویس مستقل از روز اول نیست.یک نقطه شروع عملی شامل این بخش‌هاست:هسته تجارت (business core) برای ماژول‌های مشتری، آدرس، فروشگاه، کاتالوگ، سبد، قیمت‌گذاری و مدیریت سفارشمرزهای مستقل برای موجودی و پرداختIndependent search, fulfillment, dispatch, and notification workloadsRegional transactional databasesستون فقرات رویدادمحور برای Projectionها و اثرات جانبی غیرهم‌زمانمرز سرویس‌هاAPI Gateway وظیفه مسیریابی، انتقال اطلاعات احراز هویت، محدودیت نرخ و متادیتای درخواست را بر عهده دارد. منطق کسب‌وکار نباید در Gateway قرار گیرد.۸. پارتیشن‌بندی منطقه‌ای و قابلیت سرویس‌دهیسفارش‌های Q-commerce ماهیتی محلی دارند. سفارش مشتری در تهران نباید به تراکنش موجودی در شیراز وابسته باشد.کشور به مناطق عملیاتی تقسیم می‌شود. هر منطقه مالک موارد زیر است:Projectionهای عرضه فروشگاهی و موجودیرزروهای موجودیسفارش‌هاوظایف آماده‌سازی سفارشوضعیت تخصیص پیکCacheهای منطقه‌ایسرویس‌های سراسری مالک موارد زیر هستند:هویت مشتریکاتالوگ مرجعحساب شرکای تجاریتعریف کمپین‌های سراسریرسانه محصولاتExperiment configurationNational analyticsمسیریابی درخواستآدرس انتخاب‌شده، منطقه عملیاتی را مشخص می‌کند.مختصات&#xA;    -&gt; سلول H3&#xA;    -&gt; محدوده سرویس‌دهی&#xA;    -&gt; منطقه&#xA;    -&gt; API منطقه‌ای&#xA;    -&gt; شارد داده منطقه‌ایپس از انتخاب آدرس، یک Region Token با عمر کوتاه‌ به کلاینت داده می‌شود. درخواست‌های بعدی از منطقه حل‌شده استفاده می‌کنند و دیگر لازم نیست محاسبه کامل جغرافیایی تکرار شود.یافتن فروشگاهسیستم نباید تمام فروشگاه‌هایی را برگرداند که polygon  سرویس‌دهی آن‌ها شامل آدرس است. باید فروشگاه‌هایی را برگرداند که به‌احتمال زیاد سفارش را در بازه وعده‌داده‌شده آماده و تحویل می‌دهند.تولید کاندیدها عوامل زیر را در نظر می‌گیرد:قرار داشتن آدرس داخل محدوده سرویس‌دهیساعات کاری فروشگاهوضعیت توقف موقتحداکثر شعاع ارسالصف جمع‌آوری سفارشتعداد نیروهای جمع‌آوری در دسترسعرضه تخمینی پیکزمان سفرسازگاری فروشگاه با اقلام سبدکیفیت فروشگاه و نرخ لغوحتی فروشگاهی با موجودی کافی، وقتی ظرفیت جمع‌آوری یا ظرفیت پیک تمام شود، باید غیرفعال نمایش داده شود.Cache قابلیت سرویس‌دهیسلول‌های H3 کلیدهای پایداری برای Cache فروشگاه‌های نزدیک ایجاد می‌کنند.نمونه:serviceability:{region}:{h3_cell}:{time_bucket}فهرست کاندیدها TTL کوتاهی دارد. پیش از تسویه‌حساب، وضعیت فروشگاه، ظرفیت و ETA جاری دوباره به‌صورت بلادرنگ بررسی می‌شوند.۹. کاتالوگ، عرضه فروشگاهی و جست‌وجوکاتالوگ مرجع محصولمحصول مرجع، هویت مشترک یک کالا را تعریف می‌کند:برندنام محصولدسته‌بندیاندازه بستهواحدبارکدتصاویراطلاعات تغذیه‌ای و ویژگی‌هاعرضه فروشگاهی، وضعیت تجاری مخصوص همان شعبه را تعریف می‌کند:شناسه فروشگاهشناسه SKUکد داخلی محصول در فروشگاهقیمتتخفیفوضعیت موجودیمحدودیت خریدوضعیت دسترس‌پذیریقوانین مالیات و هزینهزمان آخرین همگام‌سازیاین تفکیک مانع ترکیب هویت محصول با قیمت و موجودی مخصوص شعبه می‌شود.mapping کاتالوگ شرکافروشگاه‌های مختلف از کدها و توضیحات داخلی متفاوتی برای محصولات استفاده می‌کنند. Pipeline نگاشت، کالاهای شریک را با این ترتیب به SKU مرجع متصل می‌کند:تطبیق دقیق بارکدmapping تأییدشده شریک تجاریتطبیق برند، عنوان، اندازه و واحد پس از نرمال‌سازیپیشنهادهای مبتنی بر مدلبررسی دستی برای موارد با اطمینان پایینmapping اشتباه باعث نمایش قیمت و موجودی نادرست و رفتار اشتباه در جایگزینی محصول می‌شود. کیفیت نگاشت به داشبورد عملیاتی و فرایند بررسی مستقل نیاز دارد.معماری جست‌وجوOpenSearch جست‌وجوی مخصوص هر فروشگاه و مرور دسته‌بندی‌ها را ارائه می‌دهد.هر سند جست‌وجو باید یک عرضه قابل‌جست‌وجوی فروشگاه را نمایش دهد، نه یک محصول سراسری را.{&#xA;  &#34;store_offer_id&#34;: 82190012,&#xA;  &#34;store_id&#34;: 4102,&#xA;  &#34;sku_id&#34;: 99118,&#xA;  &#34;title&#34;: &#34;شیر کم‌چرب یک لیتری&#34;,&#xA;  &#34;category_id&#34;: 310,&#xA;  &#34;brand_id&#34;: 52,&#xA;  &#34;price&#34;: 760000,&#xA;  &#34;discounted_price&#34;: 690000,&#xA;  &#34;availability&#34;: &#34;IN_STOCK&#34;,&#xA;  &#34;stock_confidence&#34;: 0.91,&#xA;  &#34;popularity&#34;: 0.74,&#xA;  &#34;updated_at&#34;: &#34;2026-07-23T10:15:00Z&#34;&#xA;}فیلترهای جست‌وجو شامل موارد زیر هستند:فروشگاهدسته‌بندیبرندبازه قیمتتخفیفموجودیویژگی‌های رژیمیاندازه بستهرتبه‌بندی ترکیبی از عوامل زیر است:ارتباط متنیاطمینان از موجودیتاریخچه خریدمحبوبیت محصول در همان فروشگاهقیمت و تخفیفکیفیت محصولشخصی‌سازیجایگاه تبلیغاتیجایگاه تبلیغاتی نباید هیچ‌گاه قواعد موجودی یا ایمنی را دور بزند.تازگی اطلاعات جست‌وجورویدادهای کاتالوگ و قیمت، ایندکس را به‌صورت غیرهم‌زمان به‌روزرسانی می‌کنند. جست‌وجو یک مدل مشتق‌شده است. در زمان تسویه‌حساب، تمام تصمیم‌های قیمت، تخفیف و موجودی باید دوباره از سرویس‌های مرجع اعتبارسنجی شوند.ممکن است نتیجه قدیمی جست‌وجو برای چند ثانیه قیمت قبلی را نمایش دهد. پیش از پرداخت، پیشنهاد قیمت نهایی باید قیمت جدید را شفاف به مشتری نشان دهد.بازسازی ایندکساز ایندکس‌های نسخه‌دار و Alias استفاده می‌شود.store_offers_v17&#xA;store_offers_v18&#xA;store_offers_current -&gt; store_offers_v18ایندکس جدید ساخته می‌شود، تغییرات مجدداً پخش می‌شوند، نمونه‌ای از Queryها و تعداد اسناد بررسی می‌شود و سپس Alias به‌صورت اتمیک تغییر می‌کند.۱۰. سازگاری موجودیموجودی دشوارترین بخش پلتفرم است، زیرا قفسه فیزیکی خارج از کنترل مستقیم نرم‌افزار تغییر می‌کند.موجودی به دلایل زیر تغییر می‌کند:مشتریان حضوری کالا می‌خرند.کارکنان کالاهای آسیب‌دیده یا تاریخ‌گذشته را از قفسه خارج می‌کنند.محموله‌های تأمین‌کننده وارد فروشگاه می‌شوند.همگام‌سازی POS با تأخیر انجام می‌شود.نگاشت بارکد اشتباه است.نیروی جمع‌آوری با اختلاف تعداد مواجه می‌شود.وزن واقعی کالاهای وزنی با تخمین اولیه تفاوت دارد.یک عدد ساده با نام stock نمی‌تواند این عدم قطعیت را توضیح دهد.مدل موجودیبرای هر فروشگاه و SKU، اطلاعات زیر نگهداری می‌شود:on_hand&#xA;reserved&#xA;safety_stock&#xA;sellable&#xA;version&#xA;last_counted_at&#xA;last_pos_sync_at&#xA;confidence_scoreموجودی قابل‌فروش از رابطه زیر محاسبه می‌شود:sellable = max(0, on_hand - reserved - safety_stock)موجودی اطمینان، اختلاف مورد انتظار میان موجودی دیجیتال و موجودی فیزیکی را جذب می‌کند. مقدار آن باید بر اساس فروشگاه، SKU، دسته‌بندی، زمان و نرخ خطای تاریخی متفاوت باشد.Inventory ledgerهر تغییر مرجع، یک رکورد در ledger  ایجاد می‌کند.RECEIPT&#xA;SALE&#xA;RESERVE&#xA;RELEASE&#xA;COMMIT&#xA;ADJUSTMENT&#xA;DAMAGE&#xA;EXPIRATION&#xA;PICKER_CORRECTIONموجودی فعلی برای خواندن سریع استفاده می‌شود. دفترکل برای ممیزی، تطبیق، عیب‌یابی و بازسازی وضعیت کاربرد دارد.تراکنش رزرودر زمان تسویه‌حساب، تعداد موردنیاز تمام اقلام در یک تراکنش دیتابیس منطقه‌ای رزرو می‌شود.UPDATE inventory_balances&#xA;SET reserved = reserved + :quantity,&#xA;    version = version + 1,&#xA;    updated_at = NOW()&#xA;WHERE store_id = :store_id&#xA;  AND sku_id = :sku_id&#xA;  AND on_hand - reserved - safety_stock &gt;= :quantity&#xA;  AND version = :expected_version;ردیف‌ها با ترتیب پایدار SKU قفل می‌شوند. Stable ordering ، احتمال Deadlock را کاهش می‌دهد.برای سفارشی با چند کالا:شناسه SKUهای درخواستی مرتب می‌شوند.ردیف‌ها با همان ترتیب قفل یا به‌روزرسانی می‌شوند.اگر یکی از اقلام ضروری موجود نباشد، کل رزرو شکست می‌خورد.رکوردهای رزرو با زمان انقضا ایجاد می‌شوند.رویداد Outbox در همان تراکنش نوشته می‌شود.انقضای رزرورزرو موجودی هنگام پرداخت، TTL کوتاهی دارد. Worker پاک‌سازی، رزروهای منقضی‌شده را آزاد می‌کند.عملیات آزادسازی باید Idempotent باشد. ممکن است Callback پرداخت و Worker انقضا هم‌زمان اجرا شوند. وضعیت رزرو و بررسی Version تضمین می‌کند فقط یکی از انتقال‌ها موفق شود.اطمینان از موجودیدسترس‌پذیری یک مقدار کاملاً صفر و یک نیست. امتیاز اطمینان موجودی بر اساس عوامل زیر محاسبه می‌شود:تأخیر همگام‌سازی POSتعداد کالاهای پیدا‌نشده اخیر توسط جمع‌آورندگانشمارش‌های دستی اخیرسرعت فروشکسری موجودی تاریخیزمان گذشته از آخرین اسکن تأییدشدهنوع محصولبرای عرضه‌هایی با اطمینان پایین، موجودی اطمینان بزرگ‌تر، رتبه پایین‌تر یا غیرفعال‌سازی موقت اعمال می‌شود.تطبیق موجودیفرایند تطبیق موارد زیر را مقایسه می‌کند:موجودی فعلیدفترکل موجودیرزروهای فعالآمار POSاصلاحات نیروی جمع‌آوریدر صورت اختلاف، یک وظیفه اصلاح عملیاتی ایجاد و امتیاز اطمینان به‌روزرسانی می‌شود.۱۱. سبد خرید و قیمت‌گذاریسبد خرید دائماً تغییر می‌کند، مختص هر کاربر است و عمر محدودی دارد. Redis برای  active-cart انتخاب مناسبی است.سبد شامل اطلاعات زیر است:شناسه مشتریشناسه آدرسشناسه منطقهشناسه فروشگاهاقلام و تعداد انتخاب‌شدهترجیحات جایگزینیکد تخفیف اعمال‌شدهنسخه سبدزمان آخرین فعالیتسبد مالک قیمت نهایی یا موجودی قطعی نیست.قوانین سبد خریدهر سبد فوری فعال فقط به یک فروشگاه تعلق دارد.افزودن کالا از فروشگاه دیگر باید با فرایندی شفاف برای جایگزینی فروشگاه یا ساخت سبد جداگانه انجام شود.هر قلم سبد به store_offer_id اشاره می‌کند، نه فقط sku_id.به‌روزرسانی سبد از Optimistic Versioning استفاده می‌کند.سرور محدودیت تعداد و سقف خرید را اعمال می‌کند.TTL سبد باید بازگشت معمول مشتری را پوشش دهد، بدون اینکه سبدهای رهاشده برای همیشه باقی بمانند.پیشنهاد قیمتسرویس قیمت‌گذاری پیش از پرداخت، یک پیشنهاد قیمت تغییرناپذیر ایجاد می‌کند.این پیشنهاد شامل موارد زیر است:قیمت اقلامتخفیف هر قلمتخصیص کمپین‌هاهزینه ارسالهزینه خدماتمالیاتسهم کیف پول یا اعتبارمبلغ نهایی قابل‌پرداختزمان انقضانسخه مجموعه قوانین قیمت‌گذاریسرور هیچ‌گاه به مجموع قیمت ارسال‌شده از کلاینت اعتماد نمی‌کند.سازگاری کمپین و تخفیفبرای کد تخفیفی با ظرفیت محدود، صرفاً اعتبارسنجی کافی نیست. ممکن است تعداد زیادی مشتری هم‌زمان مرحله اعتبارسنجی را با موفقیت عبور دهند.پیشنهاد قیمت باید یکی از این دو روش را اجرا کند:مزیت کمپین را برای مدت کوتاهی رزرو کند.در زمان تسویه‌حساب، کمپین را دوباره بررسی و مصرف آن را قطعی کند.Unique Constraint بر مشتری، کمپین و سیاست مصرف، استفاده تکراری را متوقف می‌کند.کالاهای وزنیمیوه، گوشت و محصولات مشابه معمولاً در زمان تسویه‌حساب با وزن تخمینی و هنگام جمع‌آوری با وزن واقعی محاسبه می‌شوند.برای هر قلم سفارش، اطلاعات زیر ذخیره می‌شود:وزن تخمینیحداکثر اختلاف مجازوزن واقعیمبلغ نهایی قلمپلتفرم یا ابتدا سقف مبلغ را Authorize می‌کند، یا پس از جمع‌آوری از اصلاح کیف پول و بازپرداخت استفاده می‌کند. طراحی پرداخت باید با قابلیت‌های درگاه‌های محلی هماهنگ باشد.۱۲. تسویه‌حساب و پرداختفرایند تسویه‌حساب از چند مرز مالکیت عبور می‌کند:قیمت‌گذاریموجودیظرفیت فروشگاهسفارشپرداختآماده‌سازی سفارشتراکنش توزیع‌شده دیتابیس میان این سرویس‌ها کند و شکننده است. راه مناسب، یک Saga ماندگار با قواعد جبران صریح است.وضعیت Workflow تسویه‌حسابWorkflow هر مرحله تکمیل‌شده را ذخیره می‌کند:STARTED&#xA;QUOTE_VALIDATED&#xA;INVENTORY_RESERVED&#xA;CAPACITY_RESERVED&#xA;ORDER_CREATED&#xA;PAYMENT_INITIALIZED&#xA;PAYMENT_VERIFIED&#xA;ORDER_CONFIRMED&#xA;FULFILLMENT_CREATED&#xA;COMPLETED&#xA;COMPENSATING&#xA;FAILEDپس از Restart شدن Worker، فرایند از وضعیت ذخیره‌شده ادامه پیدا می‌کند. کل تسویه‌حساب نباید بدون بررسی از ابتدا تکرار شود.قواعد جبرانIdempotencyEndpoint تسویه‌حساب به Idempotency Key نیاز دارد.اطلاعات زیر ذخیره می‌شود:customer_id&#xA;idempotency_key&#xA;request_hash&#xA;workflow_id&#xA;response_status&#xA;response_body&#xA;expires_atقواعد:کلید یکسان با درخواست یکسان، همان نتیجه قبلی را برمی‌گرداند.کلید یکسان با درخواست متفاوت، خطای Conflict ایجاد می‌کند.Retry هیچ‌گاه سفارش یا PaymentIntent دوم ایجاد نمی‌کند.وضعیت پرداختپرداخت ماشین حالت مستقل خود را دارد:CREATED&#xA;INITIALIZED&#xA;REDIRECTED&#xA;PENDING_VERIFICATION&#xA;SUCCEEDED&#xA;FAILED&#xA;EXPIRED&#xA;REFUND_PENDING&#xA;PARTIALLY_REFUNDED&#xA;REFUNDED&#xA;REFUND_FAILEDقواعد مهم:Redirect مرورگر، مدرک موفقیت پرداخت نیست.سرور تراکنش را مستقیماً از درگاه تأیید می‌کند.ترکیب Provider و Provider Transaction ID دارای Unique Constraint است.Callbackهای تکراری همان نتیجه قبلی را برمی‌گردانند.Callback خام برای ممیزی، به‌شکل امن ذخیره می‌شود.فرایند تطبیق، تراکنش‌های حل‌نشده یا ناسازگار را بررسی می‌کند.پرداخت موفق همراه با وضعیت نامشخص سفارش، یک حالت معتبر و قابل‌بازیابی است. این وضعیت باید در داشبورد مالی و عملیات دیده شود.دفترکل مالیحقیقت مالی نباید فقط از مجموع‌های قابل‌تغییر سفارش استخراج شود.برای موارد زیر رکوردهای Append-only ثبت می‌شود:دریافت پرداختبرداشت کیف پولمصرف اعتباراصلاح مبلغ قلمبازپرداخت جزئیبازپرداخت کاملمبلغ قابل‌پرداخت به فروشگاهمبلغ قابل‌پرداخت به پیککمیسیون پلتفرمیارانه کمپیناین دفترکل, پایه تطبیق و تسویه مالی است.۱۳. آماده‌سازی سفارش در فروشگاه و جایگزینی کالاسفارش تأییدشده وارد یک فرایند فیزیکی می‌شود.پذیرش فروشگاهاپلیکیشن فروشگاه سفارش را با مهلت مشخص برای تأیید دریافت می‌کند.فروشگاه ممکن است به دلایل زیر سفارش را رد کند:شعبه در حال بسته شدن است.صف جمع‌آوری بیش از ظرفیت است.اختلال محلی مانع آماده‌سازی می‌شود.فروشگاه اختلاف زیادی در موجودی تشخیص می‌دهد.تکرار رد سفارش باید رتبه و ظرفیت فروشگاه را کاهش دهد تا تیم عملیات موضوع را بررسی کند.فرایند جمع‌آورییک نیروی جمع‌آوری تخصیص داده می‌شود.فهرست اقلام بر اساس چیدمان فروشگاه مرتب می‌شود.بارکد هر کالا اسکن می‌شود.تعداد یا وزن واقعی ثبت می‌شود.اقلام ناموجود، آسیب‌دیده یا ناسازگار گزارش می‌شوند.جایگزینی‌ها تعیین تکلیف می‌شوند.مبلغ نهایی دوباره محاسبه می‌شود.سفارش بسته‌بندی و آماده تحویل اعلام می‌شود.سیاست جایگزینیبرای هر قلم سفارش یکی از ترجیحات زیر ذخیره می‌شود:بدون جایگزینیجایگزینی طبق قوانین مشخصدریافت تأیید پیش از جایگزینیحذف کالا در صورت نبود موجودییک کالای جایگزین باید شرایط زیر را داشته باشد:سازگاری دسته‌بندیرعایت محدودیت‌های غذایی مشتریرعایت سیاست اختلاف قیمترعایت محدودیت تعداد و اندازهموجودی فعلیقوانین فروشگاهوضعیت جایگزینیمهلت پاسخ مشتری باید کوتاه باشد. سفارش نمی‌تواند در حالی که نیروی فروشگاه و پیک منتظر هستند، بدون محدودیت زمانی متوقف بماند.اصلاح مبلغ نهاییپس از جمع‌آوری، مبلغ نهایی سفارش ممکن است به دلایل زیر تغییر کند:حذف کالاپذیرش کالای جایگزینوزن واقعیاصلاح قیمت فروشگاه طبق سیاست مجازسرویس پرداخت اختلاف مبلغ را از مسیر مالی پشتیبانی‌شده اعمال می‌کند. هر اصلاح باید به قلم سفارش و دلیل آن ارجاع داشته باشد.۱۴. تخصیص پیک و تخمین زمان تحویلDispatch یک مسئله Matching بلادرنگ است.هدف، تخصیص نزدیک‌ترین پیک نیست. هدف، کاهش مجموع تأخیر تحویل و احتمال شکست تخصیص است، در حالی که عدالت میان پیک‌ها و سلامت بازار نیز حفظ شود.Pipeline موقعیت پیکاپلیکیشن پیک، به‌روزرسانی موقعیت را از طریق light polling یا پروتکل Streaming ارسال می‌کند.مراحل Pipeline:احراز هویت پیک و دستگاهاعتبارسنجی Timestamp و منطقی بودن حرکتتبدیل مختصات به سلول H3ذخیره آخرین موقعیت در Redisانتشارsampled history در Kafkaذخیره داده تاریخی در analytical storageتمام به‌روزرسانی‌های موقعیت نباید وارد دیتابیس تراکنشی شوند.تولید کاندیدسرویس Dispatch پیک‌ها را بر اساس عوامل زیر انتخاب می‌کند:سلول‌های H3 نزدیکوضعیت آنلاین و آزادنوع وسیله نقلیهتعداد مأموریت‌های فعلیمحدودیت دسترسی به فروشگاهزمان تخمینی رسیدن به فروشگاهزمان تخمینی آماده شدن سفارشقابلیت اطمینان پیکالگوی رد مأموریت‌های اخیرراهبرد تخصیصاز یک بازه پیشنهاد کوتاه همراه با تخصیص Compare-and-set استفاده می‌شود.delivery_task.status = SEARCHING&#xA;    -&gt; پیشنهاد به کاندیدهای رتبه‌بندی‌شده&#xA;    -&gt; اولین پذیرش معتبر برنده می‌شود&#xA;    -&gt; پیشنهاد دیگران لغو می‌شودبه‌روزرسانی دیتابیس شامل وضعیت مورد انتظار و Version است. پذیرش تکراری، نتیجه تخصیص موجود را برمی‌گرداند.مدل ETAزمان تحویل برای مشتری باید به‌صورت بازه نمایش داده شود، نه یک دقیقه دقیق.ETA =&#xA;    تأخیر صف&#xA;  + زمان جمع‌آوری&#xA;  + بافر جایگزینی&#xA;  + زمان بسته‌بندی&#xA;  + زمان تخصیص پیک&#xA;  + مسیر پیک تا فروشگاه&#xA;  + زمان تحویل گرفتن از فروشگاه&#xA;  + مسیر تا مشتری&#xA;  + بافر عدم قطعیتورودی‌های مدل شامل موارد زیر هستند:تعداد اقلام و ترکیب دسته‌بندی سبدچیدمان فروشگاه و عملکرد جمع‌آورندهصف فعلی جمع‌آوریتعداد پیک در دسترستأخیر تاریخی فروشگاهزمان سفر جاده‌ایآب‌وهوا و رخدادهای محلیالگوی تحویل در ساختمان و محلهپلتفرم باید یک بازه همراه با سطح اطمینان نمایش دهد. دقت ظاهری و غیرواقعی، اعتماد مشتری را کاهش می‌دهد.کاهش کنترل‌شده سرویس Dispatchوقتی عرضه پیک کم می‌شود:ETA نمایش‌داده‌شده افزایش می‌یابد.شعاع سرویس‌دهی کاهش پیدا می‌کند.فروشگاه‌های با اطمینان پایین موقتاً متوقف می‌شوند.هزینه ارسال طبق سیاست مصوب افزایش می‌یابد.ترافیک کمپین محدود می‌شود.سفارش فقط زمانی وارد صف می‌شود که مشتری پیش از پرداخت وعده طولانی‌تر را دیده باشد.سیستم نباید پس از از بین رفتن ظرفیت، همچنان وعده تحویل سی‌دقیقه‌ای نمایش دهد.۱۵. مدل دادهPostgreSQL مرجع تراکنشی اصلی سیستم است.جدول‌های اصلینمونه جدول سفارشCREATE TABLE orders (&#xA;    id                 BIGINT PRIMARY KEY,&#xA;    customer_id        BIGINT NOT NULL,&#xA;    address_id         BIGINT NOT NULL,&#xA;    region_id          INTEGER NOT NULL,&#xA;    store_id           BIGINT NOT NULL,&#xA;    quote_id           BIGINT NOT NULL,&#xA;    status             VARCHAR(40) NOT NULL,&#xA;    subtotal_amount    BIGINT NOT NULL,&#xA;    discount_amount    BIGINT NOT NULL,&#xA;    delivery_fee       BIGINT NOT NULL,&#xA;    service_fee        BIGINT NOT NULL,&#xA;    payable_amount     BIGINT NOT NULL,&#xA;    final_amount       BIGINT,&#xA;    version            INTEGER NOT NULL DEFAULT 1,&#xA;    promised_from      TIMESTAMPTZ NOT NULL,&#xA;    promised_until     TIMESTAMPTZ NOT NULL,&#xA;    created_at         TIMESTAMPTZ NOT NULL,&#xA;    updated_at         TIMESTAMPTZ NOT NULL&#xA;);ایندکس‌های مهمCREATE INDEX idx_orders_customer_created&#xA;    ON orders (customer_id, created_at DESC, id DESC);&#xA;&#xA;CREATE INDEX idx_orders_store_status_created&#xA;    ON orders (store_id, status, created_at);&#xA;&#xA;CREATE INDEX idx_reservations_expiry&#xA;    ON inventory_reservations (expires_at)&#xA;    WHERE status = &#39;ACTIVE&#39;;&#xA;&#xA;CREATE UNIQUE INDEX uq_payment_provider_transaction&#xA;    ON payment_intents (provider, provider_transaction_id)&#xA;    WHERE provider_transaction_id IS NOT NULL;پارتیشن‌بندیپس از اندازه‌گیری واقعی، جدول‌های بزرگ منطقه‌ای بر اساس زمان پارتیشن‌بندی می‌شوند:order_events به تفکیک ماهinventory_ledger به تفکیک ماه و منطقهaudit_logs به تفکیک ماهCallbackهای پرداخت به تفکیک ماهدر مرحله اول، سفارش‌ها باید بر اساس منطقه عملیاتی Shard شوند. پارتیشن‌بندی زمانی در هر منطقه به نگهداری، آرشیو و Retention کمک می‌کند.۱۶. طراحی APIبرای Commandهای مشتری و عملیات از REST استفاده می‌شود. انتشار وضعیت و ساخت Projectionها از طریق رویدادهای غیرهم‌زمان انجام می‌شود.یافتن فروشگاهGET /v1/stores?address_id=addr_123&#xA;Authorization: Bearer &lt;token&gt;{&#xA;  &#34;data&#34;: [&#xA;    {&#xA;      &#34;store_id&#34;: &#34;store_4102&#34;,&#xA;      &#34;name&#34;: &#34;سوپرمارکت نمونه&#34;,&#xA;      &#34;eta&#34;: {&#xA;        &#34;from_minutes&#34;: 24,&#xA;        &#34;to_minutes&#34;: 34&#xA;      },&#xA;      &#34;delivery_fee&#34;: 450000,&#xA;      &#34;is_open&#34;: true&#xA;    }&#xA;  ]&#xA;}جست‌وجوی محصولGET /v1/stores/store_4102/products?q=milk&amp;cursor=...از Cursor Pagination استفاده می‌شود. Cursor شامل مقادیر مرتب‌سازی قطعی جست‌وجو و یک نشانگر نسخه امضاشده است.تغییر سبدPUT /v1/carts/current/items/offer_82190012&#xA;If-Match: &#34;cart-version-18&#34;&#xA;Content-Type: application/json&#xA;&#xA;{&#xA;  &#34;quantity&#34;: 2,&#xA;  &#34;substitution_policy&#34;: &#34;ASK&#34;&#xA;}تسویه‌حسابPOST /v1/checkouts&#xA;Idempotency-Key: 8d233f0a-1c3c-4adc-b54a-3d488f5db732&#xA;Content-Type: application/json&#xA;&#xA;{&#xA;  &#34;cart_id&#34;: &#34;cart_9012&#34;,&#xA;  &#34;cart_version&#34;: 18,&#xA;  &#34;address_id&#34;: &#34;addr_123&#34;,&#xA;  &#34;payment_method&#34;: &#34;ONLINE&#34;&#xA;}ساختار خطا{&#xA;  &#34;error&#34;: {&#xA;    &#34;code&#34;: &#34;INVENTORY_UNAVAILABLE&#34;,&#xA;    &#34;message&#34;: &#34;برخی کالاها دیگر موجود نیستند.&#34;,&#xA;    &#34;retryable&#34;: false,&#xA;    &#34;request_id&#34;: &#34;req_01J2...&#34;,&#xA;    &#34;details&#34;: {&#xA;      &#34;items&#34;: [&#xA;        {&#xA;          &#34;store_offer_id&#34;: &#34;offer_82190012&#34;,&#xA;          &#34;available_quantity&#34;: 0&#xA;        }&#xA;      ]&#xA;    }&#xA;  }&#xA;}قواعد APIهر Command دارای Request ID است.Commandهای حساس به Retry به Idempotency Key نیاز دارند.منابع نسخه‌دار از Optimistic Concurrency استفاده می‌کنند.کلاینت کد خطای دامنه دریافت می‌کند، نه نام Exception داخلی.APIهای داخلی Deadline کوتاه و Retry محدود دارند.Authorization مالکیت منبع و سیاست نقش را بررسی می‌کند.۱۷. Workflowهای رویدادمحورKafka رویدادهای دامنه را به جست‌وجو، اعلان، تحلیل داده، Projectionهای پشتیبانی و یکپارچه‌سازی شرکا منتقل می‌کند.نمونه رویدادها:StoreOfferUpdated&#xA;InventoryAdjusted&#xA;InventoryReserved&#xA;ReservationExpired&#xA;CheckoutStarted&#xA;OrderCreated&#xA;PaymentVerified&#xA;OrderConfirmed&#xA;StoreAccepted&#xA;PickingStarted&#xA;SubstitutionRequested&#xA;OrderPacked&#xA;CourierAssigned&#xA;OrderPickedUp&#xA;OrderDelivered&#xA;RefundCompletedTransactional Outboxفرض کنید Order Service وضعیت سفارش را روی CONFIRMED ثبت می‌کند و پیش از انتشار OrderConfirmed از کار می‌افتد. در این حالت Fulfillment هیچ‌گاه سفارش را دریافت نمی‌کند.الگوی Outbox، انتقال وضعیت سفارش و رویداد را در یک تراکنش دیتابیس ذخیره می‌کند.orders update&#xA;+ order_events insert&#xA;+ outbox_events insert&#xA;= one transactionیک Relay ردیف‌های منتشرنشده را به Kafka ارسال می‌کند. Consumerها رویدادها را با تضمین At-least-once پردازش می‌کنند.Idempotency در ConsumerConsumer باید رویدادهای تکراری را تحمل کند.روش‌های متداول:Upsert بر اساس Aggregate ID و VersionUnique Constraint روی Event IDجدول Inbox داخل تراکنش Consumerانتقال وضعیت Compare-and-setترتیب رویدادهارویدادهای سفارش بر اساس order_id پارتیشن‌بندی می‌شوند. رویدادهای موجودی در مواردی که ترتیب اهمیت دارد، بر اساس ترکیب store_id و sku_id پارتیشن‌بندی می‌شوند.ترتیب سراسری لازم نیست و هزینه زیادی ایجاد می‌کند.Backpressureبزرگ شدن صف باید سیاست صریح داشته باشد:Consumerها بر اساس Lag و سن قدیمی‌ترین پیام Scale شوند.Topicهای سفارش، پرداخت و ایمنی اولویت بالاتر داشته باشند.تحلیل داده پیش از مسیرهای حیاتی اجازه تأخیر پیدا کند.پیش از رسیدن سیستم تراکنشی به اشباع ناامن، پذیرش ترافیک جدید محدود شود.رویدادهای خراب پس از Retry محدود به Dead-letter Topic منتقل شوند.تطبیقرویدادها انتشار وضعیت را بهتر می‌کنند. فرایند تطبیق، وضعیت‌های ازدست‌رفته یا ناسازگار را اصلاح می‌کند.Jobهای زمان‌بندی‌شده باید موارد زیر را پیدا کنند:سفارش تأییدشده بدون وظیفه Fulfillmentپرداخت موفق بدون سفارش تأییدشدهرزرو فعال پس از زمان انقضاسفارش تحویل‌شده با Capture یا Settlement معلقدرخواست بازپرداخت بدون وضعیت نهایی درگاهعرضه فعال موجود در دیتابیس ولی غایب از Searchسیستم قابل‌اعتماد از Retry و Reconciliation در کنار یکدیگر استفاده می‌کند.۱۸. Cache و مالکیت دادهRedis و OpenSearch زمان پاسخ را بهبود می‌دهند. هیچ‌کدام مالک حقیقت تراکنشی نیستند.اهداف مناسب برای Cache:کاندیدهای قابلیت سرویس‌دهیخلاصه فروشگاهدرخت دسته‌بندیجزئیات محصولنتایج جست‌وجوی محبوبتعریف قوانین قیمتوضعیت نشستشمارنده‌های Rate Limitآخرین موقعیت پیکاهداف نامناسب برای ذخیره صرفاً در Cache:وضعیت نهایی پرداختوضعیت سفارشمجموع بازپرداخترزرو موجودیدفترکل مالیجلوگیری از Cache Stampedeفروشگاه‌های پرترافیک و صفحات کمپین به روش‌های زیر نیاز دارند:TTL JitterRequest CoalescingStale-while-revalidateNegative Caching کوتاهکلیدهای نسخه‌دارPrewarm پیش از کمپین برنامه‌ریزی‌شدهرفتار سیستم هنگام خرابی Cacheخرابی Redis نباید به خرابی دیتابیس تبدیل شود.پلتفرم باید:دیتابیس را با محدودیت سخت Connection و Request محافظت کند.قابلیت‌های غیرحیاتی و پرهزینه را غیرفعال کند.در موارد مجاز، داده قدیمی اما امن ارائه دهد.پیش از آنکه ترافیک Fallback روی PostgreSQL فشار بیش از حد ایجاد کند، Circuit Breaker فعال کند.وقتی وابستگی‌های لازم برای صحت در دسترس نیستند، تسویه‌حساب جدید را رد کند.۱۹. سناریوهای خرابی و بازیابیایندکس جست‌وجو قدیمی استعامل:تأخیر Consumer کاتالوگ یا قیمتاثر:مشتری قیمت یا وضعیت موجودی قدیمی می‌بیند.محافظت:تسویه‌حساب، قیمت، موجودی و کمپین را دوباره اعتبارسنجی می‌کند.بازیابی:افزایش ظرفیت Consumerها، Replay رویدادها و اجرای تطبیق ایندکسنتیجه مهندسی:جست‌وجو مدل کشف محصول است، نه مرجع تراکنش.موجودی رزرو شده، اما پرداخت شکست می‌خوردمحافظت:TTL رزرو و عملیات جبران هنگام شکست پرداختبازیابی:رزرو موجودی و توکن ظرفیت فروشگاه به‌شکل Idempotent آزاد شوند.نتیجه مهندسی:هر رزرو به مالک، وضعیت، زمان انقضا و مسیر آزادسازی نیاز دارد.پرداخت موفق است، اما تأیید سفارش Timeout می‌شودمحافظت:Workflow ماندگار تسویه‌حساب و Unique بودن شناسه تراکنش درگاهبازیابی:پرداخت تأیید شود، تأیید سفارش Retry شود و سپس پرونده اصلاح دستی یا بازپرداخت ایجاد شود.نتیجه مهندسی:Callback پرداخت نباید به یک زنجیره هم‌زمان و شکننده وابسته باشد.فروشگاه سفارش را پذیرفته، اما جمع‌آورنده کالا را پیدا نمی‌کندمحافظت:موجودی اطمینان، امتیاز اطمینان، اسکن بارکد و سیاست جایگزینیبازیابی:جایگزین تعیین شود، کالا حذف شود، مبلغ اصلاح و امتیاز اطمینان موجودی به‌روزرسانی شود.نتیجه مهندسی:خطاهای دنیای فیزیکی باید بخشی از جریان عادی سیستم باشند.فروشگاه بیش از ظرفیت شلوغ می‌شودمحافظت:رزرو ظرفیت و آستانه‌های صفبازیابی:فروشگاه از نتایج کشف موقتاً حذف شود، ETA افزایش یابد، تقاضای جدید هدایت شود و سفارش‌های پذیرفته‌شده تکمیل شوند.نتیجه مهندسی:ظرفیت فروشگاه بخشی از دسترس‌پذیری قابل‌فروش است.عرضه پیک ناگهان کاهش پیدا می‌کندمحافظت:متریک‌های بلادرنگ عرضه و بازه اطمینان ETAبازیابی:شعاع سرویس‌دهی کاهش یابد، ETA افزایش پیدا کند، کمپین‌ها غیرفعال و فروشگاه‌های کم‌اطمینان متوقف شوند.نتیجه مهندسی:وعده تحویل باید پیش از تسویه‌حساب با ظرفیت واقعی هماهنگ شود، نه پس از شروع تأخیر.Consumer کافکا عقب می‌افتدمحافظت:Alert برای Lag، مقیاس‌پذیری خودکار Consumer، اندازه محدود Payload و برنامه‌ریزی پارتیشن‌هابازیابی:گروه‌های حیاتی اولویت‌بندی و رویدادها از Offsetهای نگهداری‌شده Replay شوند.نتیجه مهندسی:عمق صف به‌تنهایی معیار ضعیفی است. سن قدیمی‌ترین پیام، اثر روی مشتری را بهتر نشان می‌دهد.دیتابیس منطقه‌ای از دسترس خارج می‌شودمحافظت:دیتابیس Multi-AZ، Failover آزمایش‌شده، جداسازی منطقه‌ای و Backup ماندگاربازیابی:Failover درون همان منطقه انجام شود. وقتی سازگاری قوی در دسترس نیست، تسویه‌حساب جدید رد شود. مسیرهای مرور امن در صورت امکان فعال بمانند.نتیجه مهندسی:کاهش کنترل‌شده سرویس به سیاست از پیش تعیین‌شده برای قابلیت‌های قابل‌حفظ نیاز دارد.۲۰. امنیت و جلوگیری از سوءاستفادهاحراز هویتورود با OTP و محدودیت تعداد تلاشرهگیری دستگاه و نشستAccess Token کوتاه‌عمرچرخش Refresh Tokenلغو نشستبررسی ریسک برای رفتار ورود غیرعادیمجوزدهیاز Role-based Access Control همراه با سیاست‌های سطح منبع استفاده می‌شود.نمونه‌ها:مدیر فروشگاه فقط شعب تخصیص‌یافته را تغییر می‌دهد.نیروی جمع‌آوری فقط وظایف Fulfillment تخصیص‌یافته را به‌روزرسانی می‌کند.پیک فقط مأموریت تحویل فعال خود را می‌بیند.کارشناس پشتیبانی فقط تا سقف تأییدشده بازپرداخت انجام می‌دهد.کارشناس مالی وضعیت پرداخت را بدون مشاهده داده شخصی نامرتبط می‌بیند.داده حساسشماره موبایل و آدرس در صورت امکان رمزنگاری شوند.فیلدهای حساس در لاگ Mask شوند.دسترسی به جزئیات تحویل محدود و زمان‌دار باشد.دلیل و هویت فرد برای دسترسی مدیریتی ثبت شود.Retention داده بر اساس سیاست مالی و حریم خصوصی انجام شود.سناریوهای سوءاستفادهپلتفرم باید موارد زیر را تشخیص دهد:سوءاستفاده از OTPساخت حساب‌های متعدد برای استفاده از کد تخفیفبررسی مکرر و مشکوک درگاه پرداختاحتکار سبد و رزرو موجودیادعای جعلی عدم تحویلجعل موقعیت پیکدستکاری وضعیت موجودی توسط فروشگاهتصاحب حسابرفتار غیرعادی در درخواست بازپرداختScraping خودکار قیمت و کاتالوگکنترل‌هاRate Limit بر اساس حساب، دستگاه، IP، فروشگاه و کمپینمحدودیت رزرو و TTL کوتاهامتیازدهی ریسک بر اساس دستگاه و رفتاربه‌روزرسانی امضاشده موقعیت پیکسیاست اثبات تحویلصف بررسی دستیرکوردهای ممیزی تغییرناپذیرCredential سرویس با کمترین سطح دسترسی۲۱. مشاهده‌پذیری و سطح سرویسپلتفرم به مشاهده‌پذیری فنی و کسب‌وکاری نیاز دارد.لاگ‌هالاگ ساخت‌یافته شامل موارد زیر است:Request IDCorrelation IDTrace IDHash شناسه مشتریRegion IDStore IDOrder IDWorkflow IDنام و نسخه سرویسکد خطاPayload کامل پرداخت، شماره موبایل و آدرس نباید در لاگ ثبت شوند.متریک‌هامتریک‌های مسیر مشتری:نرخ موفقیت یافتن فروشگاهLatency جست‌وجو و نرخ نتیجه صفرخطاهای تغییر سبدنرخ موفقیت تسویه‌حسابنرخ موفقیت پرداختتأخیر تأیید سفارشدرصد تحویل در بازه وعده‌داده‌شدهمتریک‌های موجودی:نرخ شکست رزرواختلاف موجودی میان جست‌وجو و تسویه‌حسابنرخ کالای پیدا‌نشده توسط جمع‌آورندهتأخیر همگام‌سازی POSاطمینان موجودی به تفکیک فروشگاه و SKUمتریک‌های آماده‌سازی سفارش:زمان پذیرش فروشگاهصف جمع‌آوریمدت جمع‌آورینرخ جایگزینیفاصله زمانی بسته‌بندی تا دریافت پیکمتریک‌های Dispatch:تعداد پیک آزاد به تفکیک سلول H3زمان تخصیصنرخ رد پیشنهادتأخیر دریافت سفارش از فروشگاهخطای ETAتعداد سفارش بدون پیکمتریک‌های مالی:نرخ موفقیت درگاهتعداد پرداخت در انتظار تأییدصف بازپرداختتعداد اختلاف‌های تطبیقعدم توازن تسویهTracingمسیر کامل تسویه‌حساب و آماده‌سازی باید در بخش‌های زیر Trace شود:پیشنهاد قیمترزرو موجودیرزرو ظرفیتایجاد سفارشتأیید پرداختایجاد وظیفه Fulfillmentتخصیص پیکSpanهای غیرهم‌زمان باید از طریق متادیتای رویداد به یکدیگر متصل شوند.هشدارهاAlert باید بر اثر مشتری و علائم نزدیک شدن به اشباع متمرکز باشد:مصرف Error Budget تسویه‌حسابصف تأیید پرداختLatency رزرو موجودیتأخیر اعلان سفارش به فروشگاهسن قدیمی‌ترین پیام حیاتی Kafkaسن سفارش بدون پیکصف بازپرداختLag تکثیر دیتابیساشباع ظرفیت منطقه‌اینمونه SLO۲۲. استقرار و انتخاب فناوریفناوری به‌تنهایی صحت سیستم را ایجاد نمی‌کند.Kafka مشکل Dual Write ناامن را حل نمی‌کند. از Transactional Outbox استفاده کنید.Redis مالکیت موجودی را حل نمی‌کند. رزرو تراکنشی و دفترکل لازم است.Temporal قوانین کسب‌وکار را حذف نمی‌کند. وضعیت‌های Workflow، Timeoutها، Retryها و جبران‌ها باید تعریف شوند.Kubernetes دیتابیس اشباع‌شده را اصلاح نمی‌کند. محدودیت ظرفیت، پارتیشن‌بندی و Backpressure لازم است.دسترس‌پذیری دیتابیسهر منطقه عملیاتی باید شامل موارد زیر باشد:PostgreSQL به‌شکل Multi-AZReplication هم‌زمان در دامنه خرابی اصلی، در صورت مناسب بودن LatencyRead Replica برای بارهای خواندنی مناسبPoint-in-time Recoveryبازیابی آزمایش‌شده BackupFailover خودکار همراه با Fencing روشن۲۳. trade off های مهم مهندسیModular Monolith در برابر MicroservicesModular Monolith تعداد تماس‌های شبکه، هزینه استقرار و تراکنش‌های توزیع‌شده را کاهش می‌دهد. سرویس‌های مستقل، مالکیت تیمی و مقیاس‌پذیری جداگانه ایجاد می‌کنند.کار را با ماژول‌های قوی آغاز کنید. موجودی، پرداخت، جست‌وجو، Fulfillment و Dispatch را زمانی جدا کنید که مقیاس یا مالکیت تیمی به ایزوله‌سازی نیاز داشته باشد.رزرو با PostgreSQL در برابر رزرو با RedisRedis Throughput بالا و Latency پایین ارائه می‌دهد. PostgreSQL تراکنش ماندگار، Constraint و تطبیق مستقیم با دفترکل موجودی فراهم می‌کند.در مقیاس فرض‌شده، PostgreSQL باید مالک رزرو باشد. Redis برای Hint موجودی و خواندن‌های پرتکرار استفاده می‌شود. فقط زمانی به سرویس تخصصی رزرو مهاجرت کنید که اندازه‌گیری واقعی، Contention را ثابت کند.موجودی دقیق در برابر موجودی مبتنی بر اطمیناندانستن دقیق موجودی فیزیکی میان دو اسکن و دو همگام‌سازی POS اغلب ممکن نیست. نمایش موجودی به‌عنوان حقیقت مطلق، دسترس‌پذیری کاذب ایجاد می‌کند.از موجودی اطمینان، امتیاز اطمینان، بازخورد جمع‌آورنده و تطبیق استفاده کنید. این طراحی، عدم قطعیت را قابل‌مشاهده و قابل‌مدیریت می‌کند.نزدیک‌ترین پیک در برابر بهترین پیکنزدیک‌ترین پیک همیشه بهترین انتخاب نیست. آمادگی فروشگاه، مسیر رسیدن، تعداد مأموریت‌ها، قابلیت اطمینان و عدالت بلندمدت اهمیت دارند.از امتیاز تخصیص رتبه‌بندی‌شده همراه با Guardrailهای عملیاتی استفاده کنید.۲۴. تکامل معماریمرحله اول: محصول اولیهاپلیکیشن تجارت به‌شکل ModularPostgreSQLRedisOpenSearchصف و Workerیک منطقه عملیاتیقوانین ساده ظرفیت فروشگاه و پیکتطبیق پرداخت از اولین نسخهمرحله دوم: رشد چندشهریمسیریابی منطقه‌ایاستخراج سرویس‌های موجودی و پرداختTransactional Outbox و Kafkaبارهای کاری مستقل Fulfillment و Dispatchقابلیت سرویس‌دهی با PostGIS و H3تحلیل داده با ClickHouseSLO رسمی و مالکیت On-callمرحله سوم: گسترش سراسریچند منطقه عملیاتیشاردهای منطقه‌ای PostgreSQLظرفیت مستقل Search و Kafka در هر منطقهموتور Workflow ماندگارمدل اطمینان فروشگاه و SKUمدل‌های پیشرفته Dispatch و ETAپلتفرم یکپارچه‌سازی شرکاتطبیق خودکار و ابزارهای اصلاح عملیاتیمنطقه Disaster Recovery و Failover آزمایش‌شدهمعماری مناسب، هزینه مرحله سوم را از روز اول تحمیل نمی‌کند. در عوض، مسیری روشن برای رسیدن به مرحله سوم ایجاد می‌کند و از بن‌بست معماری جلوگیری می‌کند.جمع‌بندیتقلیل Q-commerce به «کاتالوگ، سبد، پرداخت و پیک» مسئله اصلی مهندسی را نادیده می‌گیرد.یک طراحی در سطح Senior باید نکات زیر را صریح کند:واحد تجاری اصلی، عرضه مخصوص فروشگاه است، نه محصول سراسری.موجودی، یک وضعیت فیزیکی همراه با عدم قطعیت است، نه یک عدد کاملاً قابل‌اعتماد.Search و Cache مدل‌های مشتق‌شده هستند. تسویه‌حساب حقیقت را دوباره اعتبارسنجی می‌کند.موجودی، پرداخت، وضعیت سفارش و ظرفیت، مالک مشخص دارند.تسویه‌حساب یک Saga ماندگار با جبران صریح است.کالای ناموجود و جایگزینی، وضعیت‌های عادی جریان سفارش هستند.تطبیق پرداخت از اولین نسخه Production وجود دارد.ظرفیت فروشگاه و عرضه پیک پیش از پرداخت روی دسترس‌پذیری اثر می‌گذارند.هر عملیات حساس به Retry باید Idempotent باشد.Event وضعیت را منتشر می‌کند. Reconciliation وضعیت را اصلاح می‌کند.مالکیت منطقه‌ای، Latency و دامنه خرابی را کاهش می‌دهد.مشاهده‌پذیری باید وعده کامل به مشتری را اندازه‌گیری کند، نه فقط CPU و حافظه سرویس‌ها را.معماری بر یک اصل استوار است:هر وعده‌ای که به مشتری نمایش داده می‌شود، به یک مالک، یک منبع حقیقت، یک قانون انقضا و یک مسیر بازیابی نیاز دارد.با این مرزبندی‌ها، پلتفرم هنگام قدیمی بودن موجودی، تکرار Callback پرداخت، فشار بیش از ظرفیت فروشگاه، ناپدید شدن پیک‌ها، رشد صف‌ها و Restart سرویس‌ها، رفتاری قابل‌پیش‌بینی خواهد داشت.سخت‌ترین مسئله، پذیرش یک میلیون سفارش در روز نیست. سخت‌ترین مسئله، هماهنگ نگه داشتن وعده دیجیتال با دنیای فیزیکی، دقیقه‌به‌دقیقه است.تمام جزئیات معماری و فرضیات مقیاس در این مقاله پیشنهادی هستند. این متن زیرساخت خصوصی، ترافیک Production، کد منبع، الگوریتم‌های اختصاصی یا ساختار داخلی اسنپ‌مارکت را توصیف نمی‌کند.</description><category>افشین یاری نیا</category><author>افشین یاری نیا</author><pubDate>Thu, 23 Jul 2026 19:00:53 +0330</pubDate></item><item><title>طراحی یک پلتفرم آگهی در مقیاس بالا، از ثبت آگهی تا جست‌وجو، چت و مقابله با تقلب</title><link>https://virgool.ir/@m_58197875/طراحی-یک-پلتفرم-آگهی-در-مقیاس-بالا-از-ثبت-آگهی-تا-جست-وجو-چت-و-مقابله-با-تقلب-cl636iwm5lgu</link><description>وقتی درباره طراحی سیستمی شبیه دیوار صحبت می‌کنیم، مسئله فقط ذخیره‌کردن چند آگهی در دیتابیس و نمایش آن‌ها در یک صفحه نیست.در چنین پلتفرمی، هر روز تعداد زیادی آگهی ثبت، ویرایش، تمدید، حذف و گزارش می‌شود. کاربران انتظار دارند نتایج جست‌وجو سریع و مرتبط باشند، تصاویر بدون تأخیر باز شوند، پیام‌ها به مقصد برسند و آگهی‌های مشکوک نیز پیش از آسیب‌زدن به کاربران شناسایی شوند.سؤال اصلی این است:چگونه سیستمی طراحی کنیم که ثبت آگهی در آن ساده باشد، جست‌وجو را با تأخیر کم انجام دهد، هزینه نگهداری تصاویر را کنترل کند، جلوی تقلب و مزاحمت را بگیرد و با رشد ترافیک همچنان قابل اعتماد بماند؟برای ادامه مقاله، یک آگهی را به‌عنوان مثال در نظر می‌گیریم. کاربری تازه‌وارد، یک آیفون را با قیمتی غیرعادی و تصویری کپی‌شده از فروشنده‌ای دیگر ثبت می‌کند. آگهی اعتبارسنجی اولیه را پشت سر می‌گذارد، وارد فرایند بررسی می‌شود، به نتایج جست‌وجو می‌رسد، پیام دریافت می‌کند و مدتی بعد کاربران آن را گزارش می‌کنند. هر بخش از معماری باید قسمتی از این مسیر را بدون به خطر انداختن صحت سیستم یا اعتماد کاربران مدیریت کند.این مقاله معماری داخلی واقعی دیوار را توصیف نمی‌کند. قابلیت‌های عمومی محصول از وب‌سایت و مرکز پشتیبانی دیوار برداشت شده‌اند، اما معماری، اعداد و انتخاب‌های فنی، یک طراحی پیشنهادی برای پلتفرمی در این کلاس هستند.۱. صورت مسئلهمی‌خواهیم یک پلتفرم نیازمندی‌ها طراحی کنیم که فروشنده در آن آگهی ثبت کند و خریدار بر اساس متن، دسته‌بندی، شهر، محله، قیمت و ویژگی‌های اختصاصی هر دسته به نتیجه مناسب برسد.دامنه محصول فقط خرید و فروش کالای دست‌دوم نیست. صفحه عمومی دیوار دسته‌هایی مانند املاک، وسایل نقلیه، کالای دیجیتال، خدمات و استخدام را نمایش می‌دهد. هر دسته داده، فیلتر، سیاست انتشار و الگوی تقلب متفاوتی دارد. برای نمونه، «کارکرد خودرو» در آگهی موبایل معنا ندارد و «مبلغ ودیعه» نیز برای فروش خودرو استفاده نمی‌شود.بنابراین هسته سیستم باید سه مسئله را هم‌زمان حل کند:مدیریت چرخه عمر آگهیکشف سریع و مرتبط آگهی‌هاایجاد اعتماد میان افرادی که یکدیگر را نمی‌شناسند۲. محدوده (Scope) طراحیقابلیت‌های داخل محدودهورود با شماره موبایل و OTPثبت، ویرایش، حذف، تمدید و مدیریت آگهیدسته‌بندی‌های چندسطحی و ویژگی‌های پویابارگذاری و پردازش تصویربررسی و تأیید آگهیجست‌وجو، فیلتر، مرتب‌سازی و پیشنهاد آگهی مشابهصفحه جزئیات آگهیعلاقه‌مندی‌هاچت خریدار و فروشندهنمایش کنترل‌شده اطلاعات تماسثبت گزارش تخلفنردبان، فوری و سایر خدمات ارتقای آگهیپرداخت و تطبیق وضعیت تراکنشپنل کارشناسان بررسی محتواثبت رویدادهای تحلیلیخارج از محدوده (scope) نسخه اول (MVP) طراحیموتور پیشنهاددهی پیشرفته مبتنی بر یادگیری عمیقتماس تلفنی Proxy در سطح اپراتورتسویه کامل معامله میان خریدار و فروشندهمعماری چندکشوری و چندارزیطراحی جزئی مدل‌های یادگیری ماشینپرداخت امن در محصول عمومی دیوار دیده می‌شود، اما در این طراحی، تمرکز بخش مالی روی خرید خدمات ارتقای آگهی است. ۳. Functional Requirementsحساب کاربریکاربر با شماره موبایل وارد شود.ارسال OTP محدودیت نرخ داشته باشد.نشست‌ها و دستگاه‌های فعال قابل مدیریت باشند.رفتار مشکوک در ورود، ثبت آگهی و تماس ثبت شود.آگهیکاربر پیش‌نویس ایجاد کند.دسته‌بندی، شهر، محله، عنوان، توضیحات، قیمت و تصاویر را وارد کند.فرم ثبت بر اساس دسته‌بندی تغییر کند.آگهی پیش از انتشار از قوانین بررسی عبور کند.ویرایش‌های حساس، آگهی را دوباره وارد فرایند بررسی کنند. مرکز پشتیبانی دیوار نیز اشاره می‌کند که آگهی ویرایش‌شده دوباره بررسی می‌شود.آگهی منقضی، حذف یا تعلیق شود.جست‌وجوجست‌وجو با متن فارسی انجام شود.فیلتر بر اساس شهر، محله، دسته، قیمت، زمان انتشار و عکس‌داربودن وجود داشته باشد.ویژگی‌های خاص هر دسته قابل فیلتر باشند.نتایج صفحه‌بندی شوند(pagination).آگهی حذف یا تعلیق‌شده سریع از دسترس خارج شود.ارتباطکاربر گفت‌وگو را از صفحه آگهی آغاز کند.ارسال پیام تکراری و لینک‌های مشکوک محدود شود.نمایش شماره تماس Rate Limit داشته باشد.خریدار یا فروشنده بتواند پیام، کاربر یا آگهی را گزارش کند.درآمدکاربر خدماتی مانند نردبان و فوری را بخرد.نتیجه پرداخت مستقل از Redirect مرورگر بررسی شود.تکرار Callback باعث فعال‌شدن چندباره خدمت نشود.در صورت پرداخت موفق و فعال‌نشدن خدمت، Reconciliation مشکل را اصلاح کند.۴. none functional requirementsکاراییتأخیر P95 جست‌وجو کمتر از ۳۰۰ میلی‌ثانیهتأخیر P95 دریافت جزئیات آگهی کمتر از ۲۰۰ میلی‌ثانیهتأیید دریافت پیام چت کمتر از ۲۰۰ میلی‌ثانیه در شرایط عادیبارگذاری تصاویر از CDN و نزدیک‌ترین نقطه شبکهاین اعداد هدف طراحی هستند، نه آمار واقعی دیوار.دسترس‌پذیریAPIهای اصلی: هدف 99.95 درصدجست‌وجو و مشاهده آگهی: هدف 99.9 درصد یا بیشتربخش‌های غیرضروری مانند شمارنده بازدید نباید مسیر اصلی را متوقف کنند.سازگاری دادهمالکیت، وضعیت آگهی، پرداخت و تصمیم بررسی محتوا به Strong Consistency نیاز دارند.ایندکس جست‌وجو، تعداد بازدید، پیشنهادها و تحلیل‌ها Eventual Consistency را می‌پذیرند.دوامآگهی تأییدشده و پرداخت موفق نباید با خرابی یک Worker از بین بروند.نسخه پشتیبان دیتابیس و تمرین بازیابی دوره‌ای لازم است.تصاویر اصلی باید روی Object Storage با سیاست Replication نگهداری شوند.امنیت و حریم خصوصیداده‌های حساسی مانند شماره موبایل تا حد ممکن کمتر ذخیره و به‌درستی محافظت شوند.دسترسی کارشناسان به داده‌های کاربران ثبت و ممیزی شود.EXIF تصاویر حذف شود.محدودیت نرخ (rate limit) برای OTP، تماس، پیام و ثبت آگهی اعمال شود.هزینهترافیک تصویر از سرورهای اپلیکیشن عبور نکند.رویدادهای تحلیلی وارد دیتابیس تراکنشی نشوند.پردازش‌های گران به صف منتقل شوند.۵. برآورد مقیاسبرای انتخاب معماری به چند فرض نیاز داریم:فقط تصاویر جدید روزانه، پیش از ساخت اندازه‌های مختلف، حدود ۷۵۰ گیگابایت داده تولید می‌کنند:500,000 ads × 5 images × 300 KB ≈ 750 GB/day&#xA;میانگین روزانه برای Capacity Planning کافی نیست. باید پیک ترافیک را هم تخمین بزنیم:100M daily reads ÷ 86,400 ≈ 1,157 requests/second average&#xA;Estimated peak at 8× average ≈ 9,250 requests/second&#xA;&#xA;500K daily submissions ÷ 86,400 ≈ 5.8 submissions/second average&#xA;Estimated peak at 10× average ≈ 58 submissions/second&#xA;&#xA;30M daily messages ÷ 86,400 ≈ 347 messages/second average&#xA;Estimated peak at 10× average ≈ 3,470 messages/second&#xA;این محاسبه‌ها ساده‌اند، اما شکل بار سیستم را مشخص می‌کنند، مسیرهای اصلی را نشان می‌دهند و نقطه شروع مناسبی برای Load Test می‌سازند.در مسیر خواندن نیز نسبت Read به Write بالاست. پس فشار اصلی روی Search، CDN، Cache و APIهای خواندنی قرار می‌گیرد، نه صرفاً روی ثبت آگهی.ترافیک هم یکنواخت نیست. تغییرات بازار خودرو، مسکن یا انتشار یک خبر اقتصادی می‌تواند جست‌وجوی یک دسته و شهر را به Hot Partition تبدیل کند.۶. مدل دامنه و چرخه عمر آگهیموجودیت‌های اصلی:UserAdAdRevisionCategoryCategoryAttributeLocationMediaAssetChatThreadMessageFavoriteReportModerationCasePromotionPaymentچرخه عمر آگهی، مهم‌تر از جدول ads است:Invariantهای مهمInvariant قانون همیشگی دامنه(domain) است. اگر چنین قانونی شکسته شود، سیستم از نظر کسب‌وکار نادرست عمل کرده است.فقط آگهی ACTIVE در نتایج عمومی نمایش داده شود.فقط مالک آگهی یا نقش مجاز اجازه ویرایش و حذف داشته باشد.هر تصویر متصل به آگهی باید متعلق به همان کاربر یا همان Upload Session باشد.یک Callback پرداخت نباید بیش از یک Promotion ایجاد کند.تعلیق آگهی باید دسترسی مستقیم را نیز مسدود کند، حتی اگر ایندکس جست‌وجو هنوز Stale باشد.اطلاعات تماس به کاربران محدودشده یا رفتارهای Botگونه نمایش داده نشود.تغییر دسته‌بندی یا داده‌های حساس نیازمند اعتبارسنجی و بررسی دوباره باشد.هر تصمیم Moderation فقط روی همان نسخه‌ای اعمال شود که بررسی شده است.جلوگیری از تأیید نسخه قدیمی آگهیمثال آیفون یک Race Condition مهم را نشان می‌دهد:فروشنده نسخه اول آگهی را ارسال می‌کند.سیستم بررسی نسخه اول را آغاز می‌کند.فروشنده قیمت، توضیحات یا محتوای حساس را تغییر می‌دهد و نسخه دوم ساخته می‌شود.کارشناس یا Risk Engine پس از ایجاد نسخه دوم، نسخه اول را تأیید می‌کند.اگر تأیید فقط مقدار ads.status را به ACTIVE تغییر دهد، ممکن است نسخه دوم بدون بررسی منتشر شود.برای جلوگیری از این خطا، هر ویرایش یک AdRevision تغییرناپذیر می‌سازد. ModerationCase نیز revision_id، نسخه مورد انتظار آگهی، نسخه Ruleset و نسخه مدل را نگه می‌دارد. تراکنش تأیید فقط زمانی یک Revision را فعال می‌کند که شناسه آن همچنان با pending_revision_id آگهی برابر باشد. تصمیم قدیمی بسته می‌شود و نسخه جدید در صف بررسی باقی می‌ماند.۷. معماری سطح بالادر شروع، تبدیل تمام دامنه‌ها به Microservice تصمیم خوبی نیست. مرزهای اشتباه، تراکنش‌های توزیع‌شده و عملیات پیچیده تولید می‌کنند.پیشنهاد:Modular Monolith برای User، Ad، Category، Location، Favorite و Reportسرویس یا Deployment مستقل برای Search، Media، Chat و PaymentWorkerهای مستقل برای Moderation، Indexing، Notification و AnalyticsEvent Backbone برای اتصال غیرهم‌زمان اجزاچرا Modular Monolith؟ثبت آگهی، دسته‌بندی، مالکیت، وضعیت و گزارش تخلف در شروع مرزهای نزدیک دارند. نگهداری آن‌ها در یک برنامه ماژولار، تراکنش‌ها و توسعه را ساده‌تر می‌کند.Search و Media الگوی مقیاس متفاوتی دارند. Search به Cluster و Replica نیاز دارد. Media به CPU، Object Storage و CDN وابسته است. Chat نیز اتصال طولانی، ترتیب پیام و Fan-out دارد. جداکردن این بخش‌ها از ابتدا توجیه فنی روشن دارد.۸. مدل دادهPostgreSQL منبع اصلی حقیقت است.نمونه ساده جدول آگهی:CREATE TABLE ads (&#xA;    id              BIGINT PRIMARY KEY,&#xA;    owner_id        BIGINT NOT NULL,&#xA;    category_id     BIGINT NOT NULL,&#xA;    city_id         BIGINT NOT NULL,&#xA;    neighborhood_id BIGINT,&#xA;    status          VARCHAR(32) NOT NULL,&#xA;    current_revision_id BIGINT,&#xA;    pending_revision_id BIGINT,&#xA;    version         INTEGER NOT NULL DEFAULT 1,&#xA;    published_at    TIMESTAMPTZ,&#xA;    expires_at      TIMESTAMPTZ,&#xA;    created_at      TIMESTAMPTZ NOT NULL,&#xA;    updated_at      TIMESTAMPTZ NOT NULL&#xA;);&#xA;&#xA;CREATE TABLE ad_revisions (&#xA;    id              BIGINT PRIMARY KEY,&#xA;    ad_id           BIGINT NOT NULL REFERENCES ads(id),&#xA;    revision_number INTEGER NOT NULL,&#xA;    schema_version  INTEGER NOT NULL,&#xA;    title           VARCHAR(120) NOT NULL,&#xA;    description     TEXT NOT NULL,&#xA;    price           BIGINT,&#xA;    attributes      JSONB NOT NULL DEFAULT &#39;{}&#39;,&#xA;    created_at      TIMESTAMPTZ NOT NULL,&#xA;    UNIQUE (ad_id, revision_number)&#xA;);&#xA;ویژگی‌های پویابرای خودرو به برند، مدل، سال و کارکرد نیاز داریم. برای ملک، متراژ، تعداد اتاق، ودیعه و اجاره مهم است.سه گزینه رایج وجود دارد:یک جدول بسیار عریض با صدها ستونمدل EAVستون‌های ثابت برای داده‌های مشترک و JSONB برای ویژگی‌های دسته‌ایبرای این طراحی، گزینه سوم تعادل بهتری دارد. داده‌های مشترک مانند price، city_id و status ستون مستقل دارند. ویژگی‌های دسته‌ای در JSONB ذخیره می‌شوند. Schema هر دسته نوع، اجباری‌بودن و محدودیت هر ویژگی را تعریف می‌کند.فیلترهای پرتکرار از JSONB مستقیماً روی دیتابیس تراکنشی اجرا نمی‌شوند. هنگام انتشار آگهی، این ویژگی‌ها به سند OpenSearch منتقل می‌شوند.نباید اجازه دهیم کلیدهای دلخواه JSON به Fieldهای دلخواه در OpenSearch تبدیل شوند. این کار Mapping Explosion، بزرگ‌شدن Cluster State و ناپایداری Indexing را به‌دنبال دارد. یک Field Registry باید هر ویژگی مجاز را به نوع و فیلد جست‌وجوی مشخصی نگاشت کند. Index Templateها Mapping کنترل‌شده دارند و برای سندهای اصلی از قواعد سخت‌گیرانه Dynamic Field استفاده می‌شود. دسته‌هایی با ساختار ناسازگار نیز در گروه‌های Index جدا قرار می‌گیرند.ایندکس‌های دیتابیسCREATE INDEX idx_ads_owner_created&#xA;    ON ads (owner_id, created_at DESC);&#xA;&#xA;CREATE INDEX idx_ads_active_city_category&#xA;    ON ads (city_id, category_id, published_at DESC)&#xA;    WHERE status = &#39;ACTIVE&#39;;&#xA;&#xA;CREATE INDEX idx_ads_expiration&#xA;    ON ads (expires_at)&#xA;    WHERE status = &#39;ACTIVE&#39;;&#xA;ایندکس باید از Query واقعی استخراج شود. افزودن ایندکس‌های زیاد، هزینه Write و Vacuum را بالا می‌برد.۹. جریان ثبت و انتشار آگهیجزئیات جریان:Client یک Idempotency Key می‌فرستد.API مالکیت تصاویر و Schema دسته را بررسی می‌کند.Revision تغییرناپذیر، وضعیت آگهی و Outbox Event در یک تراکنش ذخیره می‌شوند.پاسخ 202 Accepted به کاربر برمی‌گردد.Moderation همان Revision ارسال‌شده را بر اساس ریسک بررسی می‌کند.تراکنش تأیید، معتبر بودن pending_revision_id را بررسی و رویداد AdActivated را ثبت می‌کند.Search Indexer سند جست‌وجو را ایجاد می‌کند.چرا Transactional Outbox؟فرض کنید آگهی در PostgreSQL ذخیره شود، اما انتشار پیام به Kafka شکست بخورد. دیتابیس آگهی فعال دارد و Search از وجود آن خبر ندارد.با Outbox، تغییر دامنه و رکورد رویداد در یک تراکنش ثبت می‌شوند. Publisher بعداً رکورد Outbox را به Event Bus می‌فرستد. در نتیجه، Dual Write بدون هماهنگی حذف می‌شود.مصرف‌کننده‌ها باید Idempotent باشند، زیرا تحویل پیام معمولاً At-least-once است. مصرف‌کننده‌هایی که Side Effect پرهزینه دارند، شناسه رویداد پردازش‌شده را در یک Inbox Table و در همان تراکنش تغییر محلی ثبت می‌کنند.۱۰. معماری جست‌وجوجست‌وجو مهم‌ترین مسیر خواندن است. اجرای همه جست‌وجوها روی PostgreSQL، Full-text، فیلترهای پویا، رتبه‌بندی و حجم خواندن را به دیتابیس اصلی تحمیل می‌کند.OpenSearch یا Elasticsearch یک Read Model تخصصی برای Discovery فراهم می‌کند.نمونه سند:{&#xA;  &#34;ad_id&#34;: 920183,&#xA;  &#34;title&#34;: &#34;پژو ۲۰۶ سفید مدل ۱۳۹۸&#34;,&#xA;  &#34;category_id&#34;: 1402,&#xA;  &#34;city_id&#34;: 1,&#xA;  &#34;neighborhood_id&#34;: 217,&#xA;  &#34;location&#34;: { &#34;lat&#34;: 35.7219, &#34;lon&#34;: 51.3347 },&#xA;  &#34;price&#34;: 650000000,&#xA;  &#34;published_at&#34;: &#34;2026-07-11T08:30:00Z&#34;,&#xA;  &#34;has_image&#34;: true,&#xA;  &#34;seller_trust_score&#34;: 0.82,&#xA;  &#34;risk_score&#34;: 0.08,&#xA;  &#34;attributes&#34;: {&#xA;    &#34;brand&#34;: &#34;peugeot&#34;,&#xA;    &#34;model&#34;: &#34;206&#34;,&#xA;    &#34;year&#34;: 1398,&#xA;    &#34;mileage&#34;: 85000&#xA;  }&#xA;}&#xA;پردازش متن فارسیتبدیل ي به یتبدیل ك به کیکسان‌سازی اعداد فارسی و انگلیسیمدیریت نیم‌فاصلهحذف فاصله‌های اضافهتعریف Synonym برای واژه‌هایی مانند خودرو، ماشین و اتومبیلAnalyzer و Ranking اختصاصی برای هر دستهجست‌وجوی مکانیcity_id و neighborhood_id برای فیلتر دقیق و Aggregation پایدار مناسب‌اند. یک فیلد geo_point نیز جست‌وجوی نزدیک‌ترین آگهی‌ها، مرور روی نقشه، مرتب‌سازی بر اساس فاصله و Bounding Box Query را فراهم می‌کند. Geohash یا H3 برای Aggregation منطقه‌ای و ساخت Cache Keyهای تقریبی مناسب است.موقعیت دقیق و خصوصی فروشنده نباید به‌صورت پیش‌فرض نمایش داده شود. بر اساس سیاست هر دسته، مختصات عمومی گرد می‌شوند یا با مرکز محله جایگزین می‌شوند. سطح دقت مکانی برای املاک یا خدمات با کالای شخصی یکسان نیست.رتبه‌بندیامتیاز نهایی فقط BM25 نیست. سیگنال‌های زیر ترکیب می‌شوند:ارتباط متنیتطابق دستهفاصله مکانیتازگی آگهیکیفیت و تعداد تصاویراعتبار فروشندهاحتمال تخلفخدمات ارتقارفتار جمعی کاربرانخدمات پولی باید Boost کنترل‌شده ایجاد کنند. اگر Promotion تمام سیگنال‌های کیفیت و ریسک را کنار بزند، اعتماد کاربر از بین می‌رود.Paginationاز OFFSET 100000 پرهیز می‌کنیم. برای صفحات عمیق، search_after همراه یک Sort Key پایدار مانند امتیاز، زمان انتشار و ad_id مناسب‌تر است.حذف سریع آگهی ناامنIndex ممکن است چند ثانیه عقب باشد. پس دو لایه دفاع داریم:AdSuspended با اولویت بالا سند را از Search حذف می‌کند.API جزئیات، وضعیت فعلی آگهی را از Cache معتبر یا دیتابیس بررسی می‌کند.در نتیجه، نتیجه Stale شاید برای مدت کوتاه دیده شود، اما صفحه آگهی ناامن باز نمی‌شود.Reindex بدون قطعیMapping، Analyzer و سیگنال‌های Ranking در طول زمان تغییر می‌کنند. تغییر مستقیم یک Index بزرگ و فعال ریسک زیادی دارد.Indexer روی Indexهای نسخه‌دار مانند ads_v12 می‌نویسد. Backfill داده‌های Revision تأییدشده را از PostgreSQL می‌خواند یا Domain Eventها را برای ساخت ads_v13 دوباره پخش می‌کند. هنگام مهاجرت، تغییرات جدید هم‌زمان به هر دو نسخه می‌رسند. سپس تعداد اسناد، Queryهای نمونه و فیلدهای حیاتی مقایسه می‌شوند. در پایان، Alias با نام ads_current به‌صورت Atomic به Index جدید منتقل می‌شود. نسخه قبلی تا پایان دوره اطمینان برای Rollback باقی می‌ماند.۱۱. خط لوله (pipeline) تصاویرعبور فایل تصویر از API Server، پهنای باند و حافظه برنامه را مصرف می‌کند. مسیر بهتر:Media Worker این کارها را انجام می‌دهد:بررسی Magic Byte و MIME واقعیمحدودیت حجم و ابعادحذف EXIF و داده مکانیاسکن بدافزارتشخیص محتوای ممنوعساخت Thumbnail و اندازه‌های مختلفتولید WebP یا AVIF در کنار نسخه سازگارمحاسبه Perceptual Hash برای کشف تصاویر تکراریUpload Session عمر محدود دارد. Job دوره‌ای تصاویر یتیمی را که به آگهی متصل نشده‌اند حذف می‌کند.CDN باید نسخه مناسب تصویر را بر اساس اندازه صفحه و قابلیت Client تحویل دهد. این تصمیم روی هزینه زیرساخت بیش از بسیاری از بهینه‌سازی‌های Backend اثر می‌گذارد.در مثال آیفون مشکوک، Perceptual Hash تصویر بارگذاری‌شده را حتی پس از Resize یا Crop جزئی به تصویر فروشنده قبلی مرتبط می‌کند. این تطابق یک سیگنال Moderation است، نه مدرک قطعی تخلف.۱۲. بررسی محتوا، تقلب و سوءاستفادهدر یک Classified Marketplace، Trust and Safety بخشی از هسته محصول است، نه یک پنل جانبی.سیگنال‌های ریسک:حساب تازه‌ساخته‌شده با تعداد زیاد آگهیقیمت بسیار پایین‌تر یا بالاتر از الگوی دستهتصویر تکراری در چند حسابشماره تلفن مشترک میان حساب‌هالینک خارجی یا عبارت‌های فیشینگDevice Fingerprint مشکوکIP و الگوی جغرافیایی غیرعادیتعداد زیاد نمایش شماره تماسپیام یکسان برای تعداد زیادی فروشندهگزارش‌های معتبر کاربرانچرا Risk Tier؟بررسی دستی همه آگهی‌ها هزینه و تأخیر زیادی دارد. تأیید خودکار همه آگهی‌ها نیز محیط را برای Spam و کلاهبرداری باز می‌گذارد.پس سیستم بر اساس ریسک عمل می‌کند:کاربر معتبر با الگوی عادی: تأیید خودکارآگهی مبهم: صف بررسی انسانیالگوی شناخته‌شده تخلف: رد یا تعلیق خودکارFalse Positive باید اندازه‌گیری شود. مدلی که جلوی تقلب را می‌گیرد اما آگهی‌های سالم زیادی را رد می‌کند، موفق نیست.در مثال اصلی مقاله، حساب تازه، قیمت غیرعادی، تصویر تکراری و رفتار تماس مشکوک Risk Score را بالا می‌برند. ترکیب این شواهد ممکن است آگهی را پیش از انتشار به Human Review بفرستد یا پس از انتشار تعلیق کند.پنل بررسی باید دلیل تصمیم، سیگنال‌ها، سابقه کاربر، Revision بررسی‌شده و Audit Trail را نمایش دهد. اقدام کارشناس نیز باید قابل ممیزی باشد. نتیجه اعتراض کاربران نیز برای تحلیل False Positive و ارزیابی Rulesetهای بعدی ثبت می‌شود.۱۳. طراحی چتChat الگوی متفاوتی نسبت به آگهی دارد:اتصال WebSocket طولانیترتیب پیام در هر Threadتحویل دوباره پس از قطع اتصالوضعیت Delivered و ReadPush Notification برای کاربر Offlineاسکن Abuse بدون افزایش شدید تأخیرجریان پیشنهادی:client_message_id پیام را Idempotent می‌کند. اگر Client پس از Timeout دوباره پیام را بفرستد، Unique Constraint از ذخیره پیام تکراری جلوگیری می‌کند.ترتیب سراسری پیام‌ها لازم نیست. ترتیب در محدوده هر Thread کافی است. Presence و وضعیت Typing داده‌های موقت هستند و Redis برای آن‌ها مناسب است. خود پیام نباید فقط در Redis ذخیره شود.در شروع، PostgreSQL پارتیشن‌بندی‌شده برای Message Store کافی است. مهاجرت به Cassandra یا ScyllaDB فقط پس از اثبات گلوگاه و با توجه به Query Pattern انجام می‌شود.۱۴. خدمات ارتقا و پرداختقابلیت‌های عمومی دیوار شامل خدماتی مانند نردبان و فوری هستند. جریان مالی باید مستقل از بازگشت کاربر از درگاه باشد.قواعد مهم:مبلغ، نوع خدمت و ad_id در سمت Server تعیین شوند.Callback با امضای درگاه و درخواست Verify بررسی شود.gateway_transaction_id Unique باشد.مصرف دوباره Callback نتیجه قبلی را برگرداند.فعال‌سازی Promotion نیز Idempotent باشد.Job تطبیق، پرداخت‌های موفق بدون Promotion را پیدا کند.پرداخت موفق و ارتقای ناموفق یک وضعیت واقعی و قابل پیگیری است، نه یک Exception گم‌شده در Log.۱۵. Cache StrategyRedis و CDN سرعت را بالا می‌برند، اما نباید منبع حقیقت شوند.موارد مناسب برای Cache:درخت دسته‌بندیفهرست شهر و محلهخلاصه آگهی‌های پرترافیکتنظیمات Schema دستهRate LimitهاOTP ChallengeهاPresence چتصفحات عمومی پرطرفدار با TTL کوتاهموارد نامناسب:وضعیت نهایی پرداختتصمیم اصلی بررسی محتوامالکیت آگهیپیام‌های چت به‌عنوان تنها نسخهجلوگیری از Cache Stampedeبرای Hot Keyها از این الگوها استفاده می‌کنیم:TTL همراه JitterRequest CoalescingStale While RevalidateNegative Caching کوتاه برای شناسه نامعتبرVersioned Keys برای داده‌های ساختاریInvalidation باید از رویداد دامنه پیروی کند. برای نمونه، AdUpdated کش خلاصه آگهی را حذف می‌کند و AdSuspended کش جزئیات را فوراً بی‌اعتبار می‌کند.۱۶. سازگاری و مرز حقیقتشفاف‌کردن این جدول جلوی بسیاری از خطاهای معماری را می‌گیرد. اگر تیم نداند کدام سیستم صاحب داده است، در زمان اختلاف میان PostgreSQL، Redis و OpenSearch پاسخ مشخصی نخواهد داشت.۱۷. شکست‌های مهم و روش بازیابیتصویر آپلود شده، اما آگهی ثبت نشدهUpload Session با TTL ذخیره می‌شود.Cleanup Worker فایل یتیم را حذف می‌کند.آگهی فعال شده، اما Indexing شکست خوردهConsumer با Backoff تلاش دوباره انجام می‌دهد.پیام ناسالم به Dead-letter Queue می‌رود.Reconciliation، آگهی‌های ACTIVE بدون سند Search را پیدا می‌کند.آگهی تعلیق شده، اما هنوز در Search دیده می‌شودرویداد حذف با اولویت بالا منتشر می‌شود.صفحه جزئیات وضعیت فعلی را بررسی می‌کند.Monitor به اختلاف DB و Index هشدار می‌دهد.Moderation از دسترس خارج شدهدسته‌های کم‌ریسک بر اساس Ruleهای محلی ادامه پیدا می‌کنند.دسته‌های پرریسک در PENDING_REVIEW می‌مانند.درخواست‌ها در صف Durable حفظ می‌شوند.OpenSearch از دسترس خارج شدهصفحات Cacheشده دسته و شهر نمایش داده می‌شوند.ثبت و مدیریت آگهی ادامه پیدا می‌کند.جست‌وجوی متنی با پیام مناسب Degrade می‌شود.Query سنگین جایگزین روی Primary DB اجرا نمی‌شود.پرداخت موفق، Promotion نامشخصوضعیت SUCCEEDED حفظ می‌شود.Workflow یا Reconciliation فعال‌سازی را تکرار می‌کند.در صورت ناتوانی، پرونده Repair یا Refund ساخته می‌شود.Queue BacklogQueue Depth و Oldest Message Age پایش می‌شوند.Workerها Horizontal Scale می‌شوند.رویدادهای Safety و Suspension اولویت بالاتر می‌گیرند.Analytics در زمان فشار عقب می‌افتد، نه Moderation.Graceful Degradation یعنی سیستم بداند در بحران کدام قابلیت را حفظ کند و کدام قابلیت را موقتاً کاهش دهد.۱۸. مقیاس‌پذیریPostgreSQLConnection Pooling با PgBouncerRead Replica برای Queryهای مناسبPartitioning جدول‌های حجیم مانند Audit و MessageArchival آگهی‌های قدیمیIndexهای Partial برای داده فعالحذف Queryهای جست‌وجویی از دیتابیس اصلیSharding را از روز اول شروع نمی‌کنیم. پس از رسیدن به محدودیت واقعی، گزینه‌هایی مانند Shard بر اساس ad_id یا Region بررسی می‌شوند. Shard بر اساس شهر ساده به نظر می‌رسد، اما جست‌وجوی چندشهری، جابه‌جایی کاربران و عدم توازن تهران را پیچیده می‌کند.OpenSearchسندهای کم‌حجمMapping کنترل‌شدهReplica برای افزایش Read ThroughputRollover و Index Lifecycle برای داده قدیمیRouting پس از اندازه‌گیری الگوی ترافیکجلوگیری از Wildcard و Aggregationهای پرهزینهEvent BusPartition Key بر اساس ad_id برای حفظ ترتیب تغییرات یک آگهیPartition Key بر اساس thread_id برای ترتیب پیام‌های یک گفت‌وگوConsumer Group مستقل برای Search، Moderation، Analytics و NotificationSchema Registry و سازگاری نسخه رویدادهاچند منطقه خرابیسرویس‌های Stateless در چند Availability ZonePostgreSQL با Replica و Failover تست‌شدهKafka با Replication مناسبObject Storage دارای ReplicationBackup خارج از Cluster اصلیتعریف RPO و RTO برای هر دامنهMulti-region Active-Active برای همه Writeها هزینه و پیچیدگی بالایی دارد. برای نسخه اولیه در مقیاس ملی، Multi-AZ همراه Disaster Recovery Region تصمیم واقع‌بینانه‌تری است.تاب‌آوری در برابر اختلال سرویس‌دهندگان داخل ایرانبخشی از قابلیت اطمینان سیستم به Providerهای بیرونی وابسته است. ارسال OTP به یک سرویس‌دهنده اصلی و یک سرویس‌دهنده جایگزین نیاز دارد. برای هر Provider باید Health Metric، Circuit Breaker و Failover کنترل‌شده داشته باشیم. درگاه پرداخت نیز به Verify مستقل و Reconciliation نیاز دارد، چون Redirect و Callback همیشه قابل اعتماد نیستند. CDN، نقشه و Object Storage هم باید رفتار جایگزین مشخصی داشته باشند تا خرابی یک Provider کل محصول را متوقف نکند.ترافیک بالای Android و کیفیت متفاوت اینترنت موبایل نیز روی طراحی اثر می‌گذارند. Client باید Payload فشرده، تصویر Responsive، Pagination، Resumable Upload و Retry کنترل‌شده داشته باشد. داده‌های حیاتی و Backupها نیز باید زیرساختی داشته باشند که تیم در زمان اختلال سرویس‌های خارجی قادر به اداره آن باشد.۱۹. امنیت و حریم خصوصیتهدیدهای مهم:تصاحب حساب با OTP AbuseIDOR در ویرایش یا حذف آگهیScraping و برداشت شماره تماسانتشار لینک فیشینگثبت انبوه آگهی با Botفایل مخرب یا تصویر جعلیCallback پرداخت جعلیسوءاستفاده کارشناس داخلیکنترل‌ها:Authorization در هر عملیات، نه فقط پنهان‌کردن دکمهRate Limit ترکیبی بر اساس User، Device، IP و PhoneOTP کوتاه‌عمر، محدودیت تلاش و Cooldownرمزگذاری داده حساس در انتقال و StorageTokenهای کوتاه‌عمر و Rotation نشستAudit Log تغییرناپذیر برای عملیات حساسContent Security Policy در وبSecret Manager و Rotation کلیدهاSigned Upload URL محدود به نوع و اندازه فایلVerify مستقل تراکنش با درگاهحذف EXIF و اطلاعات مکانی تصویرData Retention و حذف داده بر اساس سیاست مشخصمحدودکردن نمایش شماره تماس فقط یک تصمیم امنیتی نیست. این بخش باید تجربه کاربر سالم را حفظ کند و هم‌زمان Bot و Phone Harvester را محدود سازد.۲۰. Observability و SLOسه ستون اصلی:Metrics برای فهم وضعیت کلیLogs برای جزئیات رخدادTraces برای دنبال‌کردن درخواست میان سرویس‌هاشاخص‌های مهم:مسیر آگهینرخ موفقیت ثبت آگهیزمان از Submit تا Publishتعداد آگهی در PENDING_REVIEWنرخ رد و False PositiveIndexing Lagجست‌وجوP50، P95 و P99 LatencyError RateZero-result RateClick-through RateQuery TimeoutCache Hit Ratioرسانهزمان پردازش تصویرنرخ شکست Thumbnailتعداد فایل یتیمCDN Hit Ratioحجم Egressچت و اعتمادMessage Acceptance RateDelivery Delayاتصال WebSocket فعالگزارش به ازای هزار گفت‌وگوContact Reveal RateSpike پیام تکراریپرداختPayment Success RateCallback Verification Failureپرداخت موفق بدون Promotionزمان Reconciliationهشدار باید Actionable باشد. Alert با عنوان «CPU بالا» ارزش کمتری از «Indexing Lag بیشتر از ۵ دقیقه و رشد Queue ادامه دارد» دارد.SLO نیز باید به تجربه کاربر متصل شود. برای نمونه:در 99.9 درصد پنجره ماهانه، کاربر باید نتیجه جست‌وجو را در کمتر از ۵۰۰ میلی‌ثانیه دریافت کند.Error Budget مشخص می‌کند تیم چه زمانی Feature جدید منتشر کند و چه زمانی روی Reliability تمرکز کند.۲۱. APIهای اصلیPOST   /v1/auth/otp/request&#xA;POST   /v1/auth/otp/verify&#xA;&#xA;POST   /v1/uploads&#xA;POST   /v1/ads&#xA;PATCH  /v1/ads/{ad_id}&#xA;DELETE /v1/ads/{ad_id}&#xA;POST   /v1/ads/{ad_id}/renew&#xA;&#xA;GET    /v1/search&#xA;GET    /v1/ads/{ad_id}&#xA;POST   /v1/ads/{ad_id}/favorite&#xA;POST   /v1/ads/{ad_id}/contact-reveal&#xA;POST   /v1/ads/{ad_id}/reports&#xA;&#xA;POST   /v1/chat/threads&#xA;POST   /v1/chat/threads/{thread_id}/messages&#xA;&#xA;POST   /v1/ads/{ad_id}/promotions&#xA;POST   /v1/payments/{payment_id}/verify&#xA;POST   /v1/webhooks/payment-gateway&#xA;برای عملیات‌های حساس مانند ثبت آگهی، ارسال پیام و آغاز پرداخت، Idempotency Key یا Client-generated ID در نظر می‌گیریم.Response باید وضعیت دامنه را شفاف نشان دهد. برای نمونه، ثبت موفق در دیتابیس به معنای انتشار عمومی نیست:{&#xA;  &#34;ad_id&#34;: 920183,&#xA;  &#34;status&#34;: &#34;PENDING_REVIEW&#34;,&#xA;  &#34;submitted_at&#34;: &#34;2026-07-11T08:30:00Z&#34;&#xA;}&#xA;۲۲. انتخاب فناوریاین معماری به یک زبان خاص وابسته نیست. انتخاب فناوری باید با توان تیم، هزینه عملیات و الگوی بار هماهنگ باشد.یک ترکیب مناسب در مقیاس بالا:وجود Kubernetes، Kafka و Temporal به‌تنهایی سیستم را مقیاس‌پذیر نمی‌کند. ۲۳. Trade-offهای کلیدیSQL در برابر NoSQLPostgreSQL برای مالکیت، وضعیت، پرداخت و ارتباط‌های تراکنشی مناسب است. Search Engine و Message Store تخصصی زمانی وارد می‌شوند که Query Pattern متفاوتی دارند. استفاده از یک دیتابیس برای همه مسائل ساده به نظر می‌رسد، اما فشار و پیچیدگی را روی یک نقطه جمع می‌کند.Sync در برابر Asyncاعتبارسنجی اولیه و ذخیره آگهی Sync هستند. پردازش تصویر، بررسی تکمیلی، Indexing، Notification و Analytics به‌صورت Async انجام می‌شوند. این انتخاب Latency مسیر کاربر را کم می‌کند، اما نیازمند نمایش وضعیت، Retry و Reconciliation است.Strong در برابر Eventual Consistencyهمه داده‌ها Strong Consistency نمی‌خواهند. اعمال چنین مدلی روی Search و Analytics هزینه و Latency را بالا می‌برد. در مقابل، وضعیت آگهی ناامن یا پرداخت را نباید به امید همگام‌سازی آینده رها کرد.Microservice در برابر Modular MonolithMonolith ماژولار سرعت توسعه و تراکنش محلی را حفظ می‌کند. سرویس مستقل، مقیاس و Ownership جدا می‌دهد. استخراج باید بر اساس فشار واقعی یا مرز تیم انجام شود، نه علاقه به دیاگرام شلوغ.Availability در برابر Freshnessهنگام اختلال Indexing، نمایش نتایج چند ثانیه قدیمی بهتر از قطع کامل Browse است. هنگام تعلیق آگهی خطرناک، Safety بر Availability اولویت دارد. یک پاسخ واحد برای همه داده‌ها وجود ندارد.۲۴. مسیر تکامل معماریمرحله اول: محصول در حال رشدModular MonolithPostgreSQLRedisObject Storageیک Search ClusterQueue و WorkerCDNمرحله دوم: رشد ترافیک و تیماستخراج Search، Media و ChatTransactional Outbox و KafkaRead Replica و PartitioningClickHouse برای Analyticsپنل Moderation پیشرفتهSLO و On-call رسمیمرحله سوم: مقیاس سنگینConsumer Groupهای مستقلShard یا Partition داده پس از اندازه‌گیریMulti-AZ کامل و DR RegionRanking Pipeline پیشرفتهFeature Store و Risk PlatformCapacity Planning و Cost Attribution برای هر دامنهمعماری خوب از روز اول برای مرحله سوم هزینه نمی‌دهد. مسیر رسیدن به مرحله سوم را بدون بن‌بست حفظ می‌کند.جمع‌بندیاگر طراحی را به «Client، API، Database و Elasticsearch» خلاصه کنیم، مهم‌ترین بخش مسئله را از دست داده‌ایم.یک طراحی ارشد برای پلتفرمی شبیه دیوار باید این واقعیت‌ها را روشن کند:آگهی فقط یک رکورد نیست، یک State Machine نسخه‌دار است.PostgreSQL منبع حقیقت است و Search و Cache مدل‌های مشتق‌شده‌اند.Media، Chat، Moderation و Payment به مسیر بازیابی مستقل نیاز دارند.Eventual Consistency برای Discovery پذیرفتنی است، اما Safety به اعمال سریع تصمیم نیاز دارد.Retry به Idempotency و پردازش Async به Reconciliation نیاز دارد.مقیاس‌پذیری بدون Observability، Graceful Degradation و بازیابی تست‌شده کامل نیست.هسته این معماری یک اصل ساده دارد:هر بخش باید صاحب مشخص، منبع حقیقت مشخص، مدل سازگاری مشخص و رفتار مشخصی در زمان خرابی داشته باشد.با چنین مرزبندی‌ای، سیستم فقط در روزهای عادی سریع نیست. در زمان تأخیر Search، رشد Queue، شکست درگاه پرداخت، قطع Worker یا حمله Spam نیز رفتار قابل پیش‌بینی دارد. تفاوت یک معماری نمایشی با یک معماری آماده تولید دقیقاً همین‌جاست.منابع عمومی محصولصفحه اصلی دیوار و دسته‌بندی‌های عمومینمونه صفحه جست‌وجو و فیلترهای شهر تهرانمدیریت و بررسی دوباره آگهی ویرایش‌شدهدلایل منتشرنشدن آگهیدسته‌بندی‌های اصلی آگهیشرایط تصاویر آگهیخدمات ارتقای آگهیثبت گزارش تخلفمخزن‌های عمومی تیم دیوار در GitHubتمام جزئیات معماری و فرض‌های مقیاس در این مقاله پیشنهادی هستند و ادعایی درباره زیرساخت خصوصی دیوار ندارند.</description><category>افشین یاری نیا</category><author>افشین یاری نیا</author><pubDate>Wed, 15 Jul 2026 17:46:56 +0330</pubDate></item><item><title>رزرو پرواز: طراحی قولی که بین Search، Payment و Ticketing می‌دهیم</title><link>https://virgool.ir/@m_58197875/رزرو-پرواز-طراحی-قولی-که-بین-search-payment-و-ticketing-می-دهیم-j7qsf6upjoi3</link><description>Alibaba.ir مثال‌ خوبی هست، چون محصول از بیرون خیلی آشنا و ساده به نظر می‌رسد:جستجوی پرواز انتخاب گزینه مناسب وارد کردن اطلاعات مسافر پرداخت دریافت بلیطاز نگاه کاربر، این شبیه یک checkout معمولی است.اما از نگاه backend، ماجرا checkout ساده نیست.مسئله اصلی این نیست که یک ردیف در جدول order ذخیره کنیم یا یک payment gateway را صدا بزنیم. سؤال سخت‌تر این است:چطور چیزی را می‌فروشیم که هم زمان قیمت، موجودی، وضعیت payment، پاسخ supplier و وضعیت نهایی ticketing آن می‌تواند مستقل از هم تغییر کند؟مشکل واقعی دقیقاً پشت همان دکمه ساده «پرداخت» پنهان شده است.در یک ecommerce یا marketplace معمولی هم پیچیدگی کم نیست، اما پلتفرم معمولاً کنترل بیشتری روی حقیقت سیستم دارد: catalog، inventory فروشنده، lifecycle سفارش، انبار، ارسال، refund و غیره.در رزرو پرواز، پلتفرم از همان ابتدا با سیستم‌هایی کار می‌کند که داخل database خودش نیستند و با transaction خودش commit نمی‌شوند.پلتفرم می‌تواند وضعیت booking خودش را کنترل کند. اما مالک airline inventory، سیستم GDS، charter supplier، NDC provider یا payment gateway نیست.این تفاوت خیلی مهم است.خیلی از مشکل‌های سطح inventory را خود پلتفرم سفر حل نمی‌کند. اگر دو کانال مختلف هم‌زمان بخواهند آخرین seat را بفروشند، مرجع نهایی معمولاً airline، GDS یا supplier reservation system است. پلتفرم نمی‌تواند از database خودش کل دنیای airline را lock کند.مسئولیت سخت‌تر پلتفرم چیز دیگری است:به کاربر قول اشتباه ندهد. مبلغ اشتباه دریافت نکند. booking پرداخت‌شده را گم نکند. بعد از retry دو بار ticket صادر نکند. timeout را با failure اشتباه نگیرد. وقتی payment truth و supplier truth با هم نمی‌خوانند، تیم عملیات را کور نگذارد.پس این مقاله درباره اختراع دوباره airline inventory control نیست. درباره طراحی workflow کنار سیستم‌های خارجی است: اینکه پلتفرم چطور quote می‌دهد، revalidate می‌کند، hold می‌گیرد، payment را verify می‌کند، ticket صادر می‌کند، reconcile می‌کند، refund می‌دهد و وضعیت را برای کاربر و تیم support قابل فهم نگه می‌دارد.مرز مالکیت حقیقت تقریباً این شکلی است:با این نگاه، طراحی نباید از اینجا شروع شود:orders table + payments table + tickets tableباید از این سؤال‌ها شروع شود:invariantهای سیستم چیست؟ مالک هر حقیقت واقعاً چه سیستمی است؟ کدام عملیات را می‌توان safe retry کرد؟ کدام stateها ممکن است موقتاً unknown باشند؟ وقتی واقعیت بیرون با state داخلی ما نمی‌خواند، چطور آن را repair می‌کنیم؟در این مقاله  تمرکز اصلی روی patternهای system design پشت یک پلتفرم رزرو پرواز است.فرض‌های اولیه در مقیاسعدد دقیق خیلی مهم نیست. چیزی که مهم است، شکل workload است.برای یک پلتفرم رزرو پرواز، فرض‌های منطقی می‌تواند این‌ها باشد:ترافیک search بسیار بیشتر از booking است. هدف این است که latency در search تا حد ممکن در حد چند صد میلی‌ثانیه باقی بماند.latency در سمت supplier می‌تواند متغیر باشد و گاهی به چند ثانیه برسد. reliability نیز بین supplierها یکسان نیست.freshness نتایج search محدود است و مطلق نیست. به همین دلیل، freshness در مرحله booking باید در همان لحظه revalidate شود.payment state ماهیت asynchronous دارد و گاهی ممکن است در وضعیت unknown باقی بماند.ticketing state به وضعیت و پاسخ supplier وابسته است.ترکیب passengerها شامل adult، child، infant، nationality و documents است.شکل itinerary می‌تواند one-way، round-trip، multi-city یا connecting flights باشد.seat constraints شامل adjacent seats، locked seats و paid seat selection است.bundle complexity می‌تواند از flight-only تا flight-plus-hotel یا tour package متفاوت باشد.support risk بیشتر روی حالت paid-but-not-ticketed متمرکز می‌شود.این فرض‌ها مسیر طراحی را مشخص می‌کنند:search و booking باید مسیرهای جداگانه داشته باشند. search result فقط یک candidate است، نه تعهد نهایی.قبل از payment باید reprice انجام شود. اگر supplier پشتیبانی کند، قبل از charge باید reservation hold گرفته شود.سیستم به state machine نیاز دارد، نه چند flag پراکنده.همه external writeها باید idempotent باشند.external eventها باید قبل از پردازش ذخیره شوند.reconciliation بخشی از core system است، نه یک job تزئینی.stateهای ambiguous باید برای تیم operations قابل مشاهده باشند.itinerary و passenger constraints باید به‌صورت explicit مدل شوند.چالش طراحی happy path نیست. چالش این است که وقتی happy path از وسط راه خراب می‌شود، damage را کنترل کنیم.1. چرا رزرو پرواز از checkout معمولی سخت‌تر است؟در یک checkout ساده ecommerce، پلتفرم معمولاً می‌تواند این کارها را انجام دهد:reserve item&#xA;charge customer&#xA;create order&#xA;ship itemحتی اگر هر مرحله پیچیده باشد، بخش بزرگی از system boundary دست خود شرکت است.رزرو پرواز فرق دارد. نتیجه‌ای که در search به کاربر نشان داده می‌شود، معمولاً snapshot از availability بیرونی است. این snapshot ممکن است قبل از اینکه کاربر اطلاعات مسافر را کامل کند، stale شود.بین search و ticket issuing ممکن است این اتفاق‌ها بیفتد:fare ممکن است دوباره قیمت‌گذاری شود.fare class ممکن است بسته شود.آخرین seat ممکن است از کانال دیگری فروخته شود.supplier hold ممکن است expire شود.payment gateway ممکن است با تأخیر پاسخ دهد.callback ممکن است دو بار دریافت شود.supplier ممکن است ticket را issue کرده باشد، اما response در timeout گم شود.platform ممکن است بعد از payment success و قبل از ticketing crash کند.این دیگر CRUD نیست. این یک distributed workflow است که در آن inventory خارجی و user promise کنار هم قرار گرفته‌اند.تغییر ذهنیت مهم این است:booking row منبع حقیقت کل دنیا نیست. booking row فقط تفسیر پایدار پلتفرم از چند حقیقت بیرونی است.این تفسیر باید آن‌قدر درست و قابل repair باشد.2. مسئله اصلی: قیمت، seat availability و payment مستقل از هم تغییر می‌کنندیک رزرو پرواز حداقل چند حوزه حقیقت دارد:Commercial truth یعنی: آیا این گزینه در همین لحظه با این قیمت قابل فروش است؟Inventory truth یعنی: آیا این seat یا fare class هنوز موجود است؟Payment truth یعنی: آیا پول واقعاً جابه‌جا شده است یا نه؟Ticketing truth یعنی: آیا supplier order یا ticket نهایی واقعاً صادر شده است یا نه؟Constraint truth یعنی: آیا این itinerary هنوز برای این passenger mix، seat request و bundle معتبر است یا نه؟این حوزه‌ها هم‌زمان update نمی‌شوند.کاربر یک flow ساده می‌بیند. backend یک saga می‌بیند:Search -&gt; Reprice -&gt; Hold -&gt; Pay -&gt; Verify -&gt; Issue -&gt; Deliver -&gt; Reconcileطراحی خوب این saga را explicit می‌کند.چند invariant مهم:Invariant 1: در flow معمول prepaid consumer، قبل از verified payment نباید ticket صادر شود.Invariant 2: نباید قیمتی را charge کنیم که کاربر آن را قبول نکرده است.Invariant 3: برای یک active booking attempt نباید دو payment موفق وجود داشته باشد.Invariant 4: نباید برای یک passenger و یک supplier booking reference، دو ticket عمدی صادر شود.Invariant 5 :موضوع paid-but-not-ticketed یک final failure نیست. یک recovery state مهم است.Invariant 6:موضوع timeout یعنی unknown، نه failure.Invariant 7:در offer قیمت‌گذاری‌شده باید دقیقاً با itinerary، passenger mix، seat constraints و bundle مورد قبول کاربر match باشد.این invariantها از schema مهم‌ترند. schema فقط ابزاری است برای enforce و observe کردن آن‌ها.3. موجودیت‌های اصلیطراحی ساده ممکن است فقط یک جدول orders داشته باشد.طراحی بهتر مفاهیمی را جدا می‌کند که مستقل از هم تغییر می‌کنند.Search ResultSearchResult چیزی است که کاربر در لیست نتایج می‌بیند. این یک candidate است، نه ticket تضمین‌شده.قانون مهم: client نباید search result را تبدیل به trusted price کند. server باید با supplier reference یا server-side quote دوباره reprice کند.Fare / OfferFare یا Offer گزینه قیمت‌گذاری‌شده دقیق‌تری است.supplier offer reference&#xA;fare class and cabin&#xA;total price and currency&#xA;price validity window&#xA;rules hashrules_hash مهم است، چون دو fare ممکن است قیمت یکسان داشته باشند اما refund rule، baggage rule یا change rule متفاوتی داشته باشند.ItineraryItinerary شکل واقعی سفر را توضیح می‌دهد. خیلی از requirementهایی که در UI کوچک به نظر می‌رسند، اینجا تبدیل به constraintهای backend می‌شوند.trip type: one-way, round-trip, multi-city&#xA;segments: each flight leg&#xA;connection rules&#xA;round-trip pairing&#xA;multi-city groupingone-way، round-trip، multi-city و connecting flight فقط optionهای UI نیستند. این‌ها روی pricing، availability، cancellation rule و supplier booking semantics اثر می‌گذارند.SegmentSegment هر leg پرواز داخل itinerary است.origin_airport&#xA;destination_airport&#xA;departure_time&#xA;arrival_time&#xA;marketing_airline&#xA;operating_airline&#xA;flight_number&#xA;fare_classدر connecting یا multi-city، partial availability یک ریسک واقعی است. ممکن است یک segment available باشد، اما segment بعدی دیگر available نباشد.Passengerاطلاعات passenger بخشی از supplier order و ticket می‌شود.first_name&#xA;last_name&#xA;national_id_or_passport&#xA;birth_date&#xA;passenger_type&#xA;nationality&#xA;document_type&#xA;validation_statusPassenger validation بخشی از booking correctness است. یک typo ممکن است از یک payment failed هم گران‌تر تمام شود. adult، child و infant هم می‌توانند pricing یا rule متفاوت داشته باشند.Seat RequestSeat selection فقط یک UI decoration نیست. می‌تواند inventory و adjacency constraint ایجاد کند.requested_seat&#xA;seat_type&#xA;adjacency_group_idاگر یک خانواده بخواهد کنار هم بنشیند، سیستم دیگر فقط ticket inventory را چک نمی‌کند. باید seat-map inventory و adjacency constraints را هم در نظر بگیرد.Ancillary / Bundleهمه bookingها flight-only نیستند. ممکن است baggage، seat، insurance، hotel، airport transfer یا tour package هم داشته باشند.bundle type&#xA;required components&#xA;optional components&#xA;component-level state&#xA;bundle-level price and rulesflight-plus-hotel consistency متفاوتی دارد. پلتفرم باید availability و cancellation rule چند product domain را با هم هماهنگ کند.Booking Attemptاین core aggregate سیستم است.accepted_price&#xA;current_price&#xA;version&#xA;idempotency_key&#xA;expires_atversion مهم است. state transitionها باید با optimistic locking یا مکانیزم مشابه محافظت شوند.Reservation HoldReservationHold وقتی وجود دارد که supplier از hold موقت پشتیبانی کند.supplier_reference&#xA;hold_expires_at&#xA;request_id&#xA;fencing_tokenfencing_token جلوی workerهای stale را می‌گیرد. اگر یک worker قدیمی بعداً بیدار شود، نباید بتواند با assumption قدیمی ticketing را ادامه دهد.PaymentPayment lifecycle خودش را دارد و نباید در booking.is_paid خلاصه شود.amount&#xA;currency&#xA;merchant_reference&#xA;gateway_reference&#xA;status&#xA;idempotency_key&#xA;verified_atTicket / Supplier OrderTicket از payment جداست.ticket record باید به یک سؤال ساده جواب دهد: آیا supplier واقعاً travel document یا final order قابل اتکا صادر کرده است؟به همین دلیل ticketing از payment جدا می‌شود. یک booking پرداخت‌شده هنوز می‌تواند در TICKETING_UNKNOWN، MANUAL_REVIEW یا REFUND_REQUIRED باشد. معماری نباید این stateها را به یک paid = true ساده کاهش دهد.Supplierرفتار supplierها یکسان نیست. پس capability modeling مهم است.supports_reprice&#xA;supports_hold&#xA;supports_hold_cancel&#xA;supports_order_retrieve&#xA;supports_idempotency_key&#xA;supports_void&#xA;max_safe_retry_count&#xA;timeout_policy&#xA;reconciliation_policysupplier model باید روی workflow اثر بگذارد. supplierای که hold-and-ticket دارد نباید دقیقاً همان path supplierای را طی کند که فقط immediate issue دارد.4. معماری سطح بالامعماری باید سه concern را جدا کند:Serving: مدیریت search سریع و readهای کاربرOrchestration: مدیریت booking saga، state transition، idempotency و retryRecovery: مدیریت reconciliation، repair، manual review و refundاجزای مهم:Search Aggregator  : برای supplier fanout، latency، cache freshness و ranking بهینه می‌شود. Supplier Gateway  :اینجا APIهای مختلف supplierها را normalize می‌کند و جزئیات هر supplier را از core workflow جدا نگه می‌دارد. Booking Orchestrator:اینجا allowed state transitionها و business invariantها را کنترل می‌کند. Transactional Outbox  : مطمئن می‌شود side effect خارجی فقط بعد از commit شدن state داخلی schedule شود. Webhook / Supplier Inbox: اینحا callbackهای خارجی را قبل از پردازش ذخیره می‌کند تا deduplication و replay ممکن باشد. Ticketing Worker: اینجا  supplier writeها را با idempotency و retrieve-before-retry انجام می‌دهد. Reconciliation Jobs:  اینجا platform state را با payment truth و supplier truth مقایسه می‌کنند. Component Coordinator  : وقتی محصول flight-only نیست استفاده می‌شود. مثلاً flight، hotel، baggage، transfer یا insurance را به عنوان reservationهای جدا زیر یک bundle state دنبال می‌کند.برای flight-only، Booking Orchestrator شاید فقط با repricing، hold، payment و ticketing سروکار داشته باشد. اما در package، unit of sale تغییر می‌کند:bundle = flight component + hotel component + optional servicesهر component ممکن است availability، hold expiry، cancellation rule و compensation path خودش را داشته باشد. bundle فقط وقتی باید confirmed شود که همه componentهای ضروری به state سازگار برسند. اگر یکی fail شود، سیستم باید بداند بقیه را release کند، retry کند، reprice کند، alternative بدهد یا booking را به manual review ببرد.outbox و inbox مهم‌اند، چون خیلی از سیستم‌ها دقیقاً در crashهای بین state و side effect event را گم می‌کنند:DB updated but message not published&#xA;message published but DB not updated&#xA;callback received but processing crashed&#xA;supplier issued but response timed outمعماری باید این حالت‌ها را recoverable کند.در سطح lifecycle، booking یک saga است، نه یک transaction:5. جریان Search: گرفتن نتیجه از Airline، GDS و Supplier APIهاSearch منبع حقیقت booking نیست. Search سطح discovery است.پلتفرم در search روی این‌ها optimize می‌کند:latency coverage supplier fallback deduplication ranking bounded freshnessflow معمولی:1. کاربر route، date، passenger count و cabin را وارد می‌کند.2. Search Aggregator cache کوتاه‌مدت را چک می‌کند. 3. برای resultهای missing یا stale، به supplierها fanout می‌کند. 4. Supplier Gateway responseها را normalize می‌کند. 5. Search service offerها را deduplicate و rank می‌کند. 6. response شامل expiry و server-side offer reference است.سخت‌ترین سؤال search، هزینه freshness است.اگر هر search به همه supplierها live call بزند، latency و cost بالا می‌رود. اگر cache خیلی aggressive باشد، کاربر fareهایی می‌بیند که دیگر قابل فروش نیستند.پس می‌توان freshness را adaptive طراحی کرد:Hot route:&#xA;  shorter TTL, background refresh, tighter supplier monitoring&#xA;&#xA;Long-tail route:&#xA;  slightly longer TTL, revalidate at booking boundary&#xA;&#xA;Unstable supplier:&#xA;  lower cache confidence, visible reprice expectation&#xA;&#xA;High conversion route:&#xA;  invest in prefetch and freshnessSearch result می‌تواند confidence داخلی داشته باشد:fresh&#xA;cached_recently&#xA;stale_but_usable_for_discovery&#xA;requires_live_repriceکاربر لازم نیست همه این label ها را ببیند، اما booking service باید آن‌ها را بداند.قانون مهم:Search price is display price.&#xA;Booking price is confirmed price.مرز این دو، repricing است.یک مرز دیگر هم هست که خیلی راحت فراموش می‌شود:Generic availability is not booking availability.ممکن است search result بگوید flight seat دارد، اما booking دقیق همچنان fail شود چون:fare rule برای infant مناسب نیست.child fare قیمت متفاوتی دارد.nationality مسافر به document validation متفاوتی نیاز دارد.در connecting itinerary، یکی از segmentها دیگر available نیست.round-trip fare فقط زمانی معتبر است که outbound و return با هم pair شوند.کاربر adjacent seats درخواست کرده، اما seat map نمی‌تواند آن را فراهم کند.hotel component در package available است، اما flight available نیست.به همین دلیل booking service باید full constraint set را validate کند، نه فقط selected flight ID را.6. جریان Booking: قبل از payment، temporary hold می‌گیریمBooking وقتی شروع می‌شود که کاربر اطلاعات passenger را برای یک fare انتخاب‌شده وارد می‌کند.در این نقطه، سیستم باید دیگر به search freshness اعتماد نکند و از supplier بپرسد:آیا این itinerary دقیقاً برای این passenger set و با این قیمت هنوز قابل فروش است؟در عمل، این سؤال می‌تواند این باشد:آیا این itinerary دقیق، با این passengerها، documentها، seat preferenceها، ancillaryها و package componentها هنوز با همین شرایط پذیرفته‌شده قابل فروش است؟یک booking flow امن‌تر:1. BookingAttempt با status CREATED ساخته می‌شود. 2. passenger data، document rules و route rules validate می‌شوند. 3. concurrency guard در سطح booking گرفته می‌شود. 4. selected offer با supplier reprice می‌شود. 5. شکل itinerary validate می‌شود: one-way، round-trip، multi-city، connecting. 6. seat و ancillary constraints در صورت نیاز validate می‌شوند. 7. supplier price با user-accepted price مقایسه می‌شود. 8. اگر قیمت تغییر کرده باشد، status به PRICE_CHANGED می‌رود و user acceptance جدید لازم است. 9. اگر available بود، در صورت پشتیبانی supplier، hold ساخته می‌شود. 10. hold reference، expiry و fencing token ذخیره می‌شود. 11. BookingAttempt به HOLD_CONFIRMED می‌رود. 12. payment برای مبلغ confirmed و accepted شروع می‌شود.booking flowSupplier hold را باید مثل lease دید، نه ownership.این lease چند چیز دارد:holder&#xA;expiry time&#xA;supplier reference&#xA;allowed next action&#xA;release policy&#xA;fencing tokenاگر payment بعد از hold expiry کامل شود، پلتفرم نباید کورکورانه ticket issue کند. باید supplier state را دوباره چک کند یا بسته به capability supplier، hold جدید بگیرد.اینجا جایی است که designهای ساده خراب می‌شوند. آن‌ها فرض می‌کنند:hold confirmed once = safe foreverدر واقعیت:hold confirmed = safe until expiry, subject to supplier contract and reconciliation7. جریان Payment: success، failure، timeout، duplicate callback و idempotencyPayment باید state machine مستقل خودش را داشته باشد.frontend redirect حقیقت payment نیست. فقط navigation event کاربر است.backend باید payment را server-to-server با gateway verify کند و هر gateway event را قبل از processing ذخیره کند.قانون‌های اصلی:Payment initiation باید idempotent باشد. Payment callback processing باید idempotent باشد. Payment verification باید amount، currency، merchant reference و booking ID را چک کند. Duplicate success callback باید harmless باشد. Timeout باید UNKNOWN بسازد، نه FAILED. در flow معمول prepaid consumer، ticketing باید منتظر verified payment بماند.Payment event model بهتر:payment_attempt&#xA;payment_gateway_event&#xA;payment_verification&#xA;payment_ledger_entryچرا جدا؟چون callback با verified money یکی نیست.Gateway event: &#34;یک callback payload دریافت شد.&#34; Verification: &#34;از gateway پرسیدیم و status را تأیید کردیم.&#34; Ledger entry: &#34;پلتفرم این مبلغ را captured، refunded یا failed ثبت کرد.&#34;این جداسازی وقتی callbackها duplicate یا out of order می‌آیند کمک می‌کند.مثال:12:00 user starts payment&#xA;12:02 platform receives callback success&#xA;12:02 verification API times out&#xA;12:04 user refreshes booking page&#xA;12:05 callback success arrives again&#xA;12:07 reconciliation verifies payment succeededسیستم ضعیف بسته به اینکه کدام event زودتر برسد، stateهای تصادفی می‌زند.سیستم قوی می‌گوید:payment is UNKNOWN until verification succeeds&#xA;duplicate callbacks are stored and deduplicated&#xA;booking page reads current durable state&#xA;reconciliation completes the transitionidempotency باید در چند سطح وجود داشته باشد:client request idempotency: user اگر دو بار Pay را بزند، نباید دو active payment بسازد. gateway idempotency: retry کردن payment creation نباید gateway transaction تکراری بسازد. callback idempotency: duplicate gateway event نباید internal ledger entry تکراری بسازد. ticketing idempotency: paid booking نباید چند ticket صادر کند.یکی از مهم‌ترین درس‌های طراحی همین است: idempotency یک key در یک table نیست. idempotency ویژگی هر boundaryای است که در آن retry ممکن است اتفاق بیفتد.callback handler نباید فقط شبیه «mark paid» باشد. باید بیشتر شبیه «store، verify، deduplicate، بعد advance state» باشد:handlePaymentCallback(event):&#xA;    if inbox.alreadyProcessed(event.id):&#xA;        return OK&#xA;&#xA;    inbox.store(event)&#xA;&#xA;    payment = payments.findByMerchantReference(event.merchantReference)&#xA;    verified = gateway.verify(event.gatewayReference)&#xA;&#xA;    if verified.success:&#xA;        if payment.canMoveToCaptured(verified.amount, verified.currency):&#xA;            payment.markCaptured(verified.gatewayReference)&#xA;            booking.markPaymentVerified(payment.bookingId)&#xA;            outbox.enqueue(&#34;IssueTicket&#34;, payment.bookingId)&#xA;    else:&#xA;        payment.markFailedOrUnknown(verified.status)&#xA;&#xA;    inbox.markProcessed(event.id)&#xA;    return OKمهم syntax نیست. مهم ترتیب کار است:   external event را ذخیره کن، پول را server-to-server verify کن، internal state را فقط یک بار update کن و ticketing را از طریق outbox schedule کن.8. جریان Ticket Issuing: بعد از payment success چه می‌شود؟Payment success یعنی پلتفرم حالا یکی از این دو چیز را به کاربر بدهکار است:ticket یا refund / support resolutionpayment success به معنی complete شدن booking نیست.Ticket issuing معمولاً بهتر است async و از طریق worker انجام شود، چون supplier call می‌تواند timeout شود، ambiguous جواب دهد یا follow-up read لازم داشته باشد.یک ticketing flow امن‌تر:1. Payment به VERIFIED_SUCCESS می‌رود. 2. Booking transition به READY_TO_ISSUE commit می‌شود. 3. Outbox ticketing command را schedule می‌کند. 4. Ticketing worker آخرین booking version را load می‌کند. 5. Worker hold expiry و fencing token را چک می‌کند. 6. Worker supplier issue request را با idempotency reference می‌فرستد، اگر supplier پشتیبانی کند. 7. اگر supplier success داد، ticket/PNR ذخیره می‌شود و status به TICKET_ISSUED می‌رود. 8. اگر supplier timeout داد، status به TICKETING_UNKNOWN می‌رود. 9. قبل از retry، reconciliation supplier order را retrieve می‌کند. 10. اگر supplier permanent reject داد، booking به MANUAL_REVIEW یا refund flow می‌رود.قانون حیاتی:Never retry an external write blindly when the previous attempt may have succeeded.برای supplier ticketing یعنی:issue request timed out&#xA;-&gt; retrieve supplier order by reference&#xA;-&gt; if issued, store ticket&#xA;-&gt; if not issued and safe, retry&#xA;-&gt; if ambiguous, manual reviewاین pattern جلوی duplicate ticketing و گم شدن supplier order تأییدشده را می‌گیرد.مسیر recovery برای paid-but-not-ticketed ارزش visual شدن دارد:worker logic هم همین ایده را دنبال می‌کند:issueTicket(bookingId):&#xA;    booking = bookings.loadForTicketing(bookingId)&#xA;&#xA;    if booking.ticketAlreadyIssued():&#xA;        return&#xA;&#xA;    if booking.holdExpired():&#xA;        booking.markNeedsRevalidation()&#xA;        return&#xA;&#xA;    result = supplier.issue(booking.holdReference, booking.idempotencyKey)&#xA;&#xA;    if result.issued:&#xA;        booking.storeTicket(result.pnr, result.ticketNumber)&#xA;        return&#xA;&#xA;    if result.timeout:&#xA;        supplierState = supplier.retrieveOrder(booking.holdReference)&#xA;&#xA;        if supplierState.issued:&#xA;            booking.storeTicket(supplierState.pnr, supplierState.ticketNumber)&#xA;        elif supplierState.safeToRetry:&#xA;            outbox.enqueue(&#34;IssueTicket&#34;, booking.id)&#xA;        else:&#xA;            booking.markManualReview()فرق مهم این است: retry کردن job مشکلی ندارد. retry کردن کورکورانه ی یک supplier write است که duplicate یا ambiguous ticketing درست می‌کند.9. سناریوهای خطاقیمت بعد از Search تغییر می‌کندSearch این قیمت را نشان داده:18,500,000 IRRRepricing این را برمی‌گرداند:19,200,000 IRRرفتار درست:booking به PRICE_CHANGED می‌رود payment شروع نمی‌شود قیمت جدید به کاربر نشان داده می‌شود explicit user acceptance لازم است quote version جدید ساخته می‌شودaccepted price باید version داشته باشد تا support، refund و audit tools دقیقاً بدانند کاربر چه قیمتی را قبول کرده است.Seat قبل از Payment فروخته می‌شودSupplier نمی‌تواند hold بسازد، چون inventory از بین رفته است.رفتار درست:HOLD_REJECTED payment شروع نمی‌شود alternativeها نمایش داده می‌شوند supplier reason ذخیره می‌شود search freshness signals update می‌شوندنرخ بالای HOLD_REJECTED روی یک route یا supplier یعنی search freshness ضعیف است. این فقط booking failure نیست؛ سیگنال کیفیت search است.Payment موفق می‌شود ولی Supplier Ticketing شکست می‌خورداین یکی از مهم‌ترین business-risk stateهاست.پلتفرم پول را گرفته، اما ticket را تحویل نداده است.رفتار درست:در این سناریو، با وجود موفق بودن پرداخت، وضعیت booking به TICKETING_FAILED یا MANUAL_REVIEW تغییر می‌کند.در این مرحله، ارسال notification موفقیت به‌صورت خودکار block می‌شود تا کاربر پیام اشتباه دریافت نکند.قبل از هر retry، سیستم ابتدا وضعیت سفارش را در سمت تأمین‌کننده بررسی می‌کند. فقط در صورتی که retry از نظر عملیاتی safe باشد، درخواست صدور بلیت دوباره انجام می‌شود.اگر بلیت دیگر قابل issue نباشد، فرآیند refund آغاز می‌شود.در نهایت، action موردنیاز برای operator به‌صورت واضح نمایش داده می‌شود تا تیم پشتیبانی بداند باید چه تصمیمی بگیرد.وضعیت paid-but-not-ticketed باید بر اساس age سفارش و amount پرداختی alert شود.این حالت یک background failure معمولی نیست. کاربر پرداخت را انجام داده، اما هنوز بلیت صادر نشده است. بنابراین سیستم باید این وضعیت را به‌عنوان یک failure حساس و نیازمند پیگیری فوری تشخیص دهد.هرچه زمان بیشتری از پرداخت گذشته باشد، یا مبلغ سفارش بالاتر باشد، سطح alert باید جدی‌تر شود تا تیم عملیات سریع‌تر وارد عمل شود.Supplier تأیید می‌کند ولی Callback یا Response دیر می‌رسدممکن است تأمین‌کننده بلیت را صادر کرده باشد، اما response صدور بلیت به پلتفرم نرسیده باشد.رفتار درست:وضعیت سفارش به TICKETING_UNKNOWN تغییر می‌کند. سپس سفارش ثبت‌شده در سمت تأمین‌کننده با استفاده از correlation ID بازیابی می‌شود.اگر رویداد تأخیری از سمت تأمین‌کننده دریافت شود، در inbox ذخیره می‌شود. این رویداد با استفاده از supplier reference بررسی و deduplicate می‌شود تا از پردازش تکراری جلوگیری شود.بعد از آن، وضعیت سفارش در پلتفرم repair می‌شود و پس از دریافت تأیید نهایی، نتیجه به کاربر اطلاع داده می‌شود.late success در distributed system عادی است. نباید هر timeout را به refund یا retry تبدیل کرد.کاربر Refresh می‌کند یا دوباره Payment را می‌زندکاربر tab دیگری باز می‌کند، back می‌زند، refresh می‌کند یا دوبار payment را کلیک می‌کند.رفتار درست:در این حالت، active BookingAttempt بارگذاری می‌شود و current status همان attempt به کاربر برگردانده می‌شود.اگر امکان‌پذیر باشد، همان active PaymentAttempt دوباره استفاده می‌شود تا از ایجاد payment جدید و رخ دادن second successful payment جلوگیری شود.تمام callbackهای پرداخت باید به‌صورت idempotent پردازش شوند. به همین شکل، فرآیند ticket issuing هم باید idempotent بماند تا اجرای تکراری callback یا retry باعث صدور چندباره بلیت یا تغییر اشتباه state نشود.UI باید تا حد ممکن stateless طراحی شود تا در صورت refresh شدن صفحه، کاربر بتواند بدون از دست رفتن فرآیند به وضعیت درست برگردد.Passenger Mix قیمت را تغییر می‌دهدصفحه search شاید ظاهراً «سه seat» بفروشد، اما تأمین‌کننده ها مسافر ها را متفاوت قیمت‌گذاری می‌کنند.مثال:1 adult + 1 child + 1 infantهمیشه معادل این نیست:3 adultsرفتار درست:price by passenger type&#xA;validate infant and child rules&#xA;validate documents and nationality rules&#xA;bind accepted price to passenger mix&#xA;reprice if passenger data changesترکیب مسافر ها بخشی از هویت quote است. اگر کاربر اطلاعاتی مثل child، infant، nationality یا document data را تغییر دهد، quote قبلی ممکن است دیگر معتبر نباشد.Connecting یا Multi-City Itinerary به شکل Partial شکست می‌خوردیک پرواز مستقیم one-way ساده‌ترین حالت است. connecting یا multi-city سخت‌تر است، چون availability می‌تواند در سطح segment fail شود.مثال:Segment 1: Tehran -&gt; Istanbul available&#xA;Segment 2: Istanbul -&gt; Berlin no longer availableرفتار درست:itinerary نیمه‌معتبر ساخته نمی‌شود. segment شکست‌خورده باید به‌صورت واضح مشخص شود.اگر امکان‌پذیر باشد، سیستم باید alternative combination برگرداند.تا زمانی که کل itinerary به‌صورت کامل تأیید نشده باشد، charge انجام نمی‌شود.unit of sale خود itinerary است، نه individual segment، مگر اینکه product صراحتاً partial booking را پشتیبانی کند.Seat Adjacency قابل تأمین نیستممکن است پرواز به اندازه کافی seat داشته باشد، اما seat دقیق یا کنار هم مورد نظر کاربر موجود نباشد.مثال:3 seats available&#xA;but no 3 adjacent seats availableرفتار درست:موجودی بلیت باید از موجودی seat map جدا در نظر گرفته شود.قبل از تأیید نهایی، خطای انتخاب صندلی باید به کاربر نمایش داده شود. اگر policy اجازه دهد، کاربر می‌تواند بدون انتخاب صندلی ادامه دهد.انتخاب صندلی یک مسئله مربوط به ancillary inventory است. نباید core ticketing state را خراب کند.Flight-Plus-Hotel Package به شکل Partial unavailable می‌شودوقتی product تبدیل به package می‌شود، consistency problem گسترده‌تر می‌شود.مثال:flight hold succeeded&#xA;hotel room is no longer available&#xA;payment is still pendingرفتار درست:هر package component باید به‌صورت جداگانه مدل شود و success و failure rules در سطح package تعریف شوند.تا زمانی که همه required components تأیید یا hold نشده‌اند، charge انجام نمی‌شود.اگر bundle کامل نشود، componentهای تأییدشده release می‌شوند.در صورت رخ دادن paid partial failure، سفارش به manual review منتقل می‌شود.Package booking یک multi-resource reservation problem است. سیستم به component-level state و bundle-level state نیاز دارد.Platform بین Local Commit و External Call crash می‌کنداین یک bug کلاسیک distributed system است.مثال:booking marked READY_TO_ISSUE&#xA;process crashes before ticketing message is publishedرفتار درست:transactional outbox، command مربوط به ticketing را در همان DB transaction ذخیره می‌کند.سپس outbox dispatcher در مرحله بعد آن را publish می‌کند.ارسال تکراری command امن است، چون فرآیند ticketing به‌صورت idempotent طراحی شده است.بدون outbox یا مکانیزم مشابه، سیستم ممکن است هنگام crash کار را گم کند.External Write موفق می‌شود ولی Response گم می‌شودمثال:supplier issued ticket&#xA;network timeout before response reached platformرفتار درست:فرآیند issue به‌صورت کورکورانه retry نمی‌شود.ابتدا supplier order بازیابی می‌شود. سپس با استفاده از supplier reference، deduplication انجام می‌شود.در نهایت، ticket تأییدشده ذخیره می‌شود.10. مدل Consistency: strong consistency سخت است و همیشه هم هدف درست نیستپلتفرم می‌تواند داخل boundary خودش strong consistency داشته باشد.این موارد را می‌تواند تضمین کند:valid state transitions&#xA;one active payment attempt per booking policy&#xA;one captured ledger entry per gateway transaction&#xA;one final ticket record per supplier order&#xA;no ticketing command before verified payment&#xA;audited support actionsاما نمی‌تواند این‌ها را در یک ACID transaction بگذارد:platform DB&#xA;airline inventory&#xA;supplier hold&#xA;payment gateway settlement&#xA;ticket issuing system&#xA;email/SMS deliveryپس consistency model باید explicit باشد.Strong Internal Consistencyبرای این‌ها از database transaction، uniqueness constraint، optimistic locking و state-transition guard استفاده می‌شود:booking attempt version&#xA;payment ledger entries&#xA;ticket records&#xA;refund records&#xA;operator actions&#xA;outbox command creation&#xA;inbox event deduplicationBounded External Inconsistencyسیستم‌های خارجی ممکن است موقتاً با platform state اختلاف داشته باشند.این فقط وقتی قابل قبول است که:state نام داشته باشد کاربر گمراه نشود سیستم بداند چطور reconcile کند operatorها risk را ببینند money exposure محدود باشدمثال:PAYMENT_UNKNOWN&#xA;TICKETING_UNKNOWN&#xA;HOLD_RELEASE_UNKNOWN&#xA;REFUND_PENDING&#xA;SUPPLIER_CONFIRMATION_PENDINGEventual Consistencyeventual consistency برای این‌ها قابل قبول است:search result freshness&#xA;notification delivery&#xA;analytics&#xA;dashboards&#xA;supplier status after timeout&#xA;payment verification after gateway outageاما اینکه فقط بگوییم «eventual consistency» کافی نیست.طراحی باید مشخص کند:eventual by when?&#xA;reconciled by what job?&#xA;visible to whom?&#xA;what happens if it never resolves?جدول TradeoffCache search results: برای سریع‌تر شدن search و کاهش فشار روی supplier، قیمت ها را cache کن؛ اما قبل از booking، حتماً reprice انجام بده.Reprice before payment: درست بودن قیمت را حفظ می‌کندHold before payment : باعث کاهش حالت paid-without-seat می‌شود، اما پیچیدگی hold expiry را هم اضافه می‌کند. این حالت فقط زمانی مناسب است که supplier از hold پشتیبانی کند.Payment before ticketing : باعث می‌شود ticket بدون پرداخت صادر نشود، اما ریسک paid-but-not-ticketed را ایجاد می‌کند. این حالت رایج است، ولی به recovery قوی نیاز دارد.Async ticketing باعث می‌شود retry و observability بهتر مدیریت شوند، اما معماری را پیچیده‌تر می‌کند. برای production booking توصیه می‌شود.Ticketing inside callback پیاده‌سازی ساده‌تری دارد، اما callback را کند می‌کند، recovery ضعیف‌تری دارد و ریسک timeout را بالا می‌برد. برای production booking مناسب نیست.Outbox pattern احتمال lost command را کاهش می‌دهد، اما نیاز به infra اضافه دارد. این pattern برای مرز بین state change و side effect مناسب است.Inbox pattern برای deduplication callbackها و eventها استفاده می‌شود، اما به storage و replay logic نیاز دارد. این pattern برای gateway و supplier مناسب است.Manual review ابهام را مدیریت می‌کند، اما هزینه عملیاتی دارد. برای edge caseهای مربوط به money و inventory لازم است.Automatic refund باعث resolution سریع‌تر می‌شود، اما ممکن است booking قابل بازیابی را refund کند. فقط برای rejection نهایی یا policy واضح مناسب است.Reconciliation job یک کار maintenance ساده نیست. مکانیزمی است که بعد از partial failure، وضعیت واقعی سیستم را کامل، بررسی و اصلاح می‌کند.Loopهای مهم:Payment reconciliation یعنی بررسی و اصلاح اختلاف وضعیت بین platform و payment gateway.برای مثال، gateway وضعیت پرداخت را paid گزارش می‌کند، اما platform از این پرداخت خبر ندارد.یا platform وضعیت پرداخت را unknown می‌داند، اما gateway final state را ثبت کرده است.گاهی هم refund درخواست شده، اما gateway هنوز settlement را کامل نکرده است.Supplier reconciliation یعنی بررسی و اصلاح اختلاف وضعیت بین platform و supplier.برای مثال، platform وضعیت ticketing را unknown می‌داند، اما supplier بلیت را issued کرده است.یا platform وضعیت hold را confirmed می‌داند، اما supplier آن را expired گزارش می‌کند.گاهی هم supplier order وجود دارد، اما platform هنوز ticket را ذخیره نکرده است.Booking reconciliation یعنی بررسی bookingهایی که در state ناسالم یا ناقص باقی مانده‌اند.برای مثال، booking پرداخت‌شده بعد از SLA هنوز ticket ندارد.یا hold منقضی شده، اما payment همچنان pending است.یا booking بیش از حد مجاز در یک intermediate state مانده است.یک dashboard مفید فقط لیست failed rowها نیست.باید به این سؤال‌ها جواب دهد:چقدر پول در risk است؟ چند کاربر منتظر ticket هستند؟ کدام supplier بیشترین (ابهام )ambiguity را دارد؟ کدام bookingها نزدیک hold expiry هستند؟ کدام paid booking از ticketing SLA عبور کرده؟ کدام retryها safe هستند؟ کدام caseها human action لازم دارند؟queueهای عملیاتی مهم:Paid but not ticketed&#xA;Payment unknown over N minutes&#xA;Ticketing unknown over N minutes&#xA;Hold expires soon&#xA;Refund pending&#xA;Supplier degraded&#xA;High price-change rate&#xA;High hold-reject rate&#xA;Duplicate callback spikedashboard باید next safe action را هم نشان دهد:wait&#xA;verify payment&#xA;retrieve supplier order&#xA;retry issue&#xA;refund&#xA;manual call to supplier&#xA;contact customerفرق observability واقعی با support nightmare همین است.12. امنیت و سوءاستفادهرزرو پرواز ترکیبی از payment، identity و scarce inventory است. Abuse prevention باید بخشی از architecture باشد.کنترل‌های مهم:هرگز به price ارسال‌شده از سمت client اعتماد نکن.amount پرداخت باید به accepted quote سمت سرور bind شود.payment callback باید به‌صورت server-to-server verify شود.external eventها باید deduplicate شوند.search باید بر اساس user، IP، device و route rate-limit شود.ساخت hold نیز باید rate-limit شود.hold abuse و bot traffic باید detect شوند.unpaid holdها باید سریع expire شوند.sensitive passenger data باید encrypt شود.همه support state changeها باید audit شوند.دسترسی manual refund و ticket override باید محدود شود.supplier credentials و fare tokenها باید محافظت شوند.نمونه abuse:Bot با ساخت تعداد زیادی hold تلاش می‌کند inventory ارزان را block کند.User مقدار پرداخت را در سمت client دست‌کاری می‌کند.Attacker، payment callback را replay می‌کند.Scraper از search API برای جمع‌آوری fare data استفاده می‌کند.Support account بدون trace، وضعیت booking را تغییر می‌دهد.Compromised supplier credential باعث expose شدن booking operationها می‌شود.security و consistency در این بخش از هم جدا نیستند.13. چند اصل طراحی که از این flow بیرون می‌آیدتا اینجا pattern طراحی مشخص است: سیستم حول happy path سازماندهی نمی‌شود. سیستم حول مرز بین local certainty و external uncertainty طراحی می‌شود.پلتفرم می‌تواند روی ruleهای خودش strict باشد:one booking state machine&#xA;one accepted quote version&#xA;one captured payment record&#xA;one final ticket per supplier order&#xA;audited support actionsاما باید درباره سیستم‌هایی که مالکشان نیست واقع‌بین باشد:supplier inventory can change&#xA;payment status can be delayed&#xA;ticketing can timeout&#xA;callbacks can duplicate&#xA;seat maps can disagree with fare availability&#xA;package components can fail independentlyبه همین دلیل طراحی از transaction ساده به workflow control صریح‌تر می‌رود:Use internal transactions for internal invariants.&#xA;Use a saga for the external workflow.&#xA;Use repricing before payment.&#xA;Use holds as leases, not permanent guarantees.&#xA;Treat itinerary, passenger mix, seats, and bundles as quote constraints.&#xA;Use idempotency at every retry boundary.&#xA;Use outbox for commands and inbox for callbacks.&#xA;Treat timeout as unknown.&#xA;Reconcile payment and supplier truth continuously.&#xA;Expose high-risk states to operations.درس عمیق‌تر boundary awareness است.database transaction می‌تواند platform state را محافظت کند. اما نمی‌تواند airline، supplier، payment gateway و notification system را مجبور کند با booking table ما commit شوند. وقتی این روشن شود، معماری کمتر درباره «order را کجا ذخیره کنیم؟» است و بیشتر درباره این است: «وقتی واقعیت اطراف مدام تغییر می‌کند، چطور قولی که به کاربر داده‌ایم را safe نگه داریم؟»14. جمع‌بندیرزرو پرواز شبیه checkout به نظر می‌رسد، چون UI uncertainty را پنهان می‌کند.واقعیت backend این است:search فقط یک snapshot است.price موقتی است.inventory بیرونی است.itinerary shape می‌تواند unit of sale را تغییر دهد.passenger mix روی pricing و validation اثر می‌گذارد.seat selection ممکن است مستقل از ticket inventory شکست بخورد.packageها مسئله cross-domain reservation را اضافه می‌کنند.holdها lease هستند.پرداخت asynchronous است.ticketing ممکن است ambiguous شود.callbackها ممکن است duplicate شوند.timeout یعنی وضعیت unknown است.reconciliation لازم است.طراحی خوب وانمود نمی‌کند این مشکل‌ها وجود ندارند. آن‌ها را نام‌گذاری می‌کند و معماری را دورشان می‌سازد.این معماری به این‌ها نیاز دارد:separate search and booking paths&#xA;server-side repricing&#xA;supplier-aware reservation holds&#xA;explicit itinerary, passenger, seat, and bundle constraints&#xA;explicit booking state machine&#xA;payment ledger and verification&#xA;idempotent external writes&#xA;transactional outbox&#xA;callback inbox&#xA;retrieve-before-retry supplier logic&#xA;reconciliation jobs&#xA;manual review queues&#xA;operational dashboardsدرس اصلی:رزرو پرواز سخت نیست چون UI آن چند مرحله راحت دارد. سخت است چون پلتفرم در حال دادن یک قول به کاربر است، آن هم روی چند سیستمی که کامل در کنترل خودش نیستند.همین است که رزرو پرواز را به یک مسئله جدی system design تبدیل می‌کند.</description><category>افشین یاری نیا</category><author>افشین یاری نیا</author><pubDate>Thu, 09 Jul 2026 12:40:38 +0330</pubDate></item><item><title>پشت‌صحنه ترب : جمع‌آوری، تطبیق و به‌روزرسانی میلیون‌ها محصول</title><link>https://virgool.ir/@m_58197875/پشت-صحنه-ترب-جمع-آوری-تطبیق-و-به-روزرسانی-میلیون-ها-محصول-e8zvdmnnl3nq</link><description>ترب از نظر طراحی سیستم مثال جالبی است، چون مسئله اصلی آن فقط «اسکریپ کردن چند سایت» نیست.مسئله واقعی این است:چطور می‌توان داده‌های نامنظم، متغیر و گاهی غیرقابل‌اعتماد هزاران فروشنده را به یک تجربه قابل‌اعتماد برای جست‌وجو و مقایسه قیمت تبدیل کرد؟دیجی‌کالا و ترب هر دو در فضای ecommerce هستند، اما جنس مسئله‌شان فرق دارد. دیجی‌کالا بیشتر شبیه یک commerce platform است: بخش بزرگی از کاتالوگ، فرایند خرید، جریان سفارش، قوانین فروشنده، موجودی و تجربه checkout داخل یک سیستم کنترل‌شده مدیریت می‌شود. ترب بیشتر شبیه یک لایه کشف محصول و مقایسه قیمت روی تعداد زیادی فروشگاه مستقل است.همین تفاوت باعث می‌شود ترب با واقعیت بیرونی سروکار داشته باشد:فروشگاه‌های مختلف با پیاده‌سازی‌های مختلفCMSهای متفاوت مثل WooCommerce، فروشگاه‌سازهای اختصاصی یا سیستم‌های داخلیساختار URLهای متفاوتفرمت‌های متفاوت قیمت و موجودیعنوان‌های متفاوت برای یک محصول مشابهسایت‌هایی که کند می‌شوند یا از دسترس خارج می‌شوندقیمت‌های staleمحصولات تکراریmatching اشتباه بین محصولاتفروشندگانی که ممکن است برای گرفتن کلیک، قیمت یا اطلاعات گمراه‌کننده منتشر کننددر این مقاله از زاویه system design بررسی می‌کنیم ترب احتمالا چطور محصولات را وارد سیستم می‌کند، چطور داده‌ها را normalize و match می‌کند، چطور freshness قیمت و موجودی را مدیریت می‌کند، و چه edge caseهایی در مقیاس واقعی اهمیت دارند.چیزهایی که به صورت عمومی می‌دانیماز صفحات عمومی ترب چند نکته مشخص می‌شود:فروشگاه‌های آنلاین بعد از تایید و شارژ حساب، می‌توانند محصولاتشان را به صورت خودکار توسط ربات‌های ترب وارد سیستم کنند.ترب می‌گوید تغییرات قیمت و موجودی بعد از تغییر در سایت فروشنده، به صورت خودکار به‌روزرسانی می‌شود.در فرایند ترب، اطلاعات محصول خوانده می‌شود و بعد محصولات دسته‌بندی و ادغام می‌شوند.فروشگاه‌های فیزیکی بدون سایت می‌توانند محصول موجود در ترب را جست‌وجو کنند و فقط قیمت خودشان را برای آن ثبت کنند.ترب پلاگین رسمی WooCommerce دارد.مقاله پلاگین WooCommerce ترب می‌گوید این پلاگین یک API بر پایه WordPress REST API ایجاد می‌کند و فیلدهایی مثل عنوان، شناسه محصول، قیمت فعلی، قیمت قبلی، موجودی، دسته‌بندی، لینک تصویر، لینک محصول، توضیحات، ویژگی‌ها، تاریخ انتشار، گارانتی و اطلاعات رجیستری را استخراج می‌کند.changelog عمومی پلاگین WordPress به مواردی مثل product-change webhook، token validation، product preview و connection check اشاره می‌کند.از اینجا به بعد باید بین دو نوع گزاره فرق بگذاریم:Verified = چیزی که در اطلاعات عمومی ترب یا پلاگین آمده است&#xA;Inferred = معماری منطقی برای یک سیستم شبیه ترباین تفکیک مهم است، چون هدف مقاله حدس زدن معماری داخلی ترب نیست؛ هدف این است که مسئله را از زاویه طراحی سیستم درست تحلیل کنیم.فرض‌های مقیاسقبل از طراحی سیستم، باید چند فرض تقریبی درباره مقیاس داشته باشیم. لازم نیست دقیق باشند، ولی جهت معماری را مشخص می‌کنند.برای یک سیستم شبیه ترب می‌توان چنین فرض‌هایی داشت:تعداد فروشنده‌ها: هزاران تا ده‌ها هزارتعداد محصول/offer: میلیون‌هاالگوی ترافیک: read-heavyتغییرات: قیمت و موجودی دائما تغییر می‌کنندمحصولات داغ: نیاز به freshness در حد چند دقیقهمحصولات long-tail: تحمل refresh کندترlatency جست‌وجو: چند صد میلی‌ثانیهمدل consistency: eventually consistentاز همین فرض‌ها چند نتیجه مهم می‌گیریم:کاربران خیلی بیشتر از فروشنده‌ها سیستم را می‌خوانند.مسیر ingestion و مسیر serving باید از هم جدا باشند.crawl کردن همه فروشگاه‌ها در هر دقیقه واقع‌بینانه نیست.freshness قیمت باید adaptive باشد، نه یکسان برای همه محصولات.search باید از روی search index سرو شود، نه مستقیم از catalog database.چون product matching کامل و بی‌خطا نیست، سیستم به human review نیاز دارد.مدل اصلی: محصول canonical در برابر offer فروشندهمهم‌ترین مفهوم در طراحی چنین سیستمی تفاوت بین canonical product و seller offer است.canonical product یعنی محصول Normalize شده ایی که کاربر می‌خواهد مقایسه کند:Apple iPhone 15 Pro Max, 256GB, Natural Titaniumseller offer یعنی پیشنهاد یک فروشنده برای همان محصول:فروشنده A: قیمت ۷۱,۵۰۰,۰۰۰ تومان، موجودفروشنده B: قیمت ۷۲,۲۰۰,۰۰۰ تومان، موجودفروشنده C: قیمت ۶۹,۹۰۰,۰۰۰ تومان، ناموجوداین تفکیک پایه سیستم است.اگر ترب هر listing فروشنده را یک محصول مستقل در نظر بگیرد، کاربر با تعداد زیادی محصول تکراری روبه‌رو می‌شود. اگر هم محصولات را بیش از حد aggressive با هم merge کند، کاربر قیمت‌های چند محصول متفاوت را کنار هم می‌بیند. هر دو حالت اعتماد کاربر را از بین می‌برد.پس سیستم به دو لایه نیاز دارد:Canonical product = محصول نرمال‌شده و قابل مقایسهSeller offer = قیمت، موجودی، گارانتی، لینک، فروشنده و freshnessمدل دادهاین مدل به ترب اجازه می‌دهد برای یک محصول، چندین قیمت و فروشنده مختلف نشان دهد.معماری کلیدر سطح بالا، سیستم محصول در ترب دو مسیر اصلی دارد:مسیر ingestion: جمع‌آوری، پاک‌سازی، normalize، match و index کردن داده محصول.مسیر serving: پاسخ سریع به جست‌وجو و صفحه محصول برای کاربران.ایده اصلی این است:Ingestion می‌تواند کند، retryable و eventually consistent باشد.Serving باید سریع، قابل‌اتکا و user-facing باشد.به همین دلیل نمی‌خواهیم هر بار که کاربر جست‌وجو می‌کند، سیستم برود سایت فروشنده‌ها را live crawl کند. کاربر باید از view ایندکس‌شده ترب از دنیا جواب بگیرد.محصولات چطور وارد سیستم می‌شوند؟یک سیستم شبیه ترب معمولا فقط یک روش ingestion ندارد. هر روش مزایا و محدودیت‌های خودش را دارد.tradeoff روش‌های ingestionروش: HTML crawlingمزیت: با سایت‌های زیادی کار می‌کندضعف: با تغییر HTML شکننده استکاربرد مناسب: فروشگاه‌هایی که feed یا API ندارندروش: Sitemap/feedمزیت: discovery ساده‌ترضعف: ممکن است stale یا ناقص باشدکاربرد مناسب: پیدا کردن URL محصولاتروش: Plugin/APIمزیت: ساختاریافته و قابل‌اعتمادترضعف: نیاز به نصب یا تنظیم فروشنده داردکاربرد مناسب: WooCommerce یا پلتفرم‌های پشتیبانی‌شدهروش: Webhookمزیت: به‌روزرسانی سریعضعف: ممکن است گم، تکراری یا غیرفعال شودکاربرد مناسب: تغییرات قیمت و موجودیروش: Manual offerمزیت: ساده برای فروشگاه فیزیکیضعف: برای ساخت محصول کامل کافی نیستکاربرد مناسب: ثبت قیمت روی محصول موجودمعماری پخته معمولا انتخاب بین «crawler یا API» نیست. ترکیبی است:API/plugin وقتی ممکن استSitemap/feed برای discoveryCrawling به عنوان fallbackWebhook برای update سریعPolling به عنوان safety netHuman review برای موارد مبهمonboarding فروشگاه آنلاینبرای یک فروشگاه آنلاین، جریان کلی می‌تواند این‌طور باشد:نکته مهم این است که ingestion بعد از تایید فروشگاه شروع می‌شود. ترب نباید بدون شناخت فروشنده، هر سایت تصادفی را وارد index کند.جریان فروشگاه فیزیکیبرای فروشگاهی که سایت ندارد، جریان فرق می‌کند:اینجا دوباره اهمیت canonical product مشخص می‌شود. فروشنده لازم نیست کل محصول را از صفر بسازد؛ فقط قیمت و offer خودش را به یک محصول موجود وصل می‌کند.Crawling سایت فروشنده‌هاHTML crawling منعطف‌ترین روش ingestion است، اما در عین حال شکننده‌ترین روش هم هست.یک crawler subsystem معمولا چنین اجزایی دارد:crawler باید به چند سوال جواب دهد:الان کدام فروشنده باید crawl شود؟کدام URLها احتمالا product page هستند؟با چه سرعتی می‌توان سایت این فروشنده را crawl کرد بدون اینکه به سایت فشار بیاید؟از آخرین fetch موفق چه چیزی تغییر کرده؟اگر سایت فروشنده down شد، چه باید کرد؟اگر parser دیگر نتوانست قیمت را پیدا کند، چه اتفاقی باید بیفتد؟Discovery در برابر ExtractionDiscovery و extraction باید جدا باشند.Discovery جواب می‌دهد:چه URLهایی برای محصول وجود دارد؟Extraction جواب می‌دهد:روی این URL چه داده محصولی وجود دارد؟خروجی discovery می‌تواند شبیه این باشد:seller_id&#xA;product_url&#xA;discovered_at&#xA;source_type = sitemap | api | crawl | plugin | manualخروجی extraction می‌تواند شبیه این باشد:seller_id&#xA;product_url&#xA;seller_product_id&#xA;title&#xA;price&#xA;old_price&#xA;availability&#xA;image_url&#xA;attributes&#xA;category&#xA;fetched_atاین جداسازی در debug خیلی مهم است. اگر محصولی در ترب دیده نمی‌شود، تیم می‌تواند دقیق‌تر بپرسد: مشکل در discovery بوده یا URL پیدا شده ولی parse نشده است؟API، feed یا plugin ساختاریافتهوقتی فروشنده بتواند داده ساختاریافته بدهد، API یا plugin معمولا بهتر از HTML crawling است.پلاگین WooCommerce ترب یک نمونه عمومی از این مدل است. طبق توضیح ترب، این پلاگین داده محصول را از طریق یک API مبتنی بر WordPress REST API در اختیار ترب قرار می‌دهد. changelog عمومی پلاگین هم به connection check، token validation، product preview و webhook اشاره می‌کند.API بهتر است چون:نام فیلدها مشخص استpagination قابل کنترل‌تر استproduct IDها پایدارتر هستندvariantها بهتر قابل نمایش هستندقیمت و موجودی مستقیم از ecommerce platform می‌آیندparser کمتر به HTML و CSS سایت وابسته استاما API هم edge case دارد:نسخه پلاگین قدیمی استtoken منقضی یا اشتباه استپلاگین با پلاگین دیگری در WordPress تداخل داردفروشنده pricing rule اختصاصی داردAPI داده stale برمی‌گرداندpagination محصولی را جا می‌اندازد یا تکراری برمی‌گرداندcatalog بزرگ باعث timeout می‌شودfirewall فروشنده requestهای ترب را block می‌کندپس هر seller integration باید مثل یک data contract مانیتور شود.Webhook و PollingWebhook برای freshness خیلی مهم است. به جای اینکه ترب مدام بپرسد «این محصول تغییر کرده؟»، سیستم فروشنده می‌تواند وقتی قیمت یا موجودی تغییر کرد به ترب خبر بدهد.اما webhook به تنهایی کافی نیست. ممکن است گم شود، دیر برسد، دوبار برسد یا کلا غیرفعال شود.الگوی قابل‌اعتماد این است:Webhook = fast path&#xA;Polling = safety netاگر webhook تکراری آمد، پردازش باید idempotent باشد. اگر webhook گم شد، polling باید در نهایت داده را اصلاح کند.استخراج داده محصوللایه extraction داده خام فروشنده را به یک product event ساختاریافته تبدیل می‌کند.مثلا داده خام می‌تواند چنین چیزی باشد:{&#xA;  &#34;title&#34;: &#34;iPhone 15 ProMax 256 Natural - رجیستر شده&#34;,&#xA;  &#34;price&#34;: &#34;715,000,000 ریال&#34;,&#xA;  &#34;availability&#34;: &#34;موجود&#34;,&#xA;  &#34;category&#34;: &#34;Mobile Phones&#34;,&#xA;  &#34;image&#34;: &#34;https://seller.example/cdn/iphone.jpg&#34;,&#xA;  &#34;url&#34;: &#34;https://seller.example/product/123&#34;&#xA;}خروجی normalize شده می‌تواند این باشد:{&#xA;  &#34;seller_id&#34;: &#34;seller_123&#34;,&#xA;  &#34;seller_product_id&#34;: &#34;123&#34;,&#xA;  &#34;title&#34;: &#34;iPhone 15 Pro Max 256GB Natural&#34;,&#xA;  &#34;brand&#34;: &#34;Apple&#34;,&#xA;  &#34;model&#34;: &#34;iPhone 15 Pro Max&#34;,&#xA;  &#34;storage&#34;: &#34;256GB&#34;,&#xA;  &#34;color&#34;: &#34;Natural Titanium&#34;,&#xA;  &#34;price_toman&#34;: 71500000,&#xA;  &#34;in_stock&#34;: true,&#xA;  &#34;product_url&#34;: &#34;https://seller.example/product/123&#34;,&#xA;  &#34;image_url&#34;: &#34;https://seller.example/cdn/iphone.jpg&#34;,&#xA;  &#34;observed_at&#34;: &#34;2026-07-02T10:00:00Z&#34;&#xA;}این لایه باید این موارد را مدیریت کند:تبدیل ریال و توماناعداد فارسی، عربی و انگلیسیقیمت خالی یا صفر«تماس بگیرید» به جای قیمتقیمت اقساطی به جای قیمت نهاییقیمت قبلی و قیمت فعلیvariantهای محصولابهام در موجودیصفحات JavaScript-renderedredirectهاتصویر خرابURLهای تکراریفرمت‌های اختصاصی هر فروشندهسیستم خوب به همه فیلدها به یک اندازه اعتماد نمی‌کند. برای داده‌ها confidence نگه می‌دارد:price_confidence = high | medium | low&#xA;stock_confidence = high | medium | low&#xA;match_confidence = high | medium | lowداده low-confidence می‌تواند مخفی شود، پایین‌تر rank شود یا به human review برود.NormalizationNormalization یعنی تبدیل متن و داده فروشنده به زبان داخلی سیستم.مثال:&#34;اپل&#34;                 -&gt; &#34;Apple&#34;&#xA;&#34;آیفون ۱۵ پرو مکس&#34;    -&gt; &#34;iPhone 15 Pro Max&#34;&#xA;&#34;256 گیگ&#34;             -&gt; &#34;256GB&#34;&#xA;&#34;ناموجود&#34;             -&gt; in_stock = false&#xA;&#34;۷۱۵,۰۰۰,۰۰۰ ریال&#34;   -&gt; 71,500,000 Tomanکارهای رایج در normalization:تبدیل اعدادپاک‌سازی متنتبدیل واحد پولnormalize کردن واحدهاmapping دسته‌بندیتشخیص brandاستخراج modelاستخراج attributeهاحذف stopword برای search و matchingMachine learning می‌تواند کمک کند، اما ruleهای deterministic هنوز خیلی مهم‌اند. در commerce تفاوت‌های کوچک می‌توانند هویت محصول را عوض کنند.Product Matchingسخت‌ترین بخش سیستم product matching است.این عنوان‌ها ممکن است به یک محصول اشاره کنند:فروشنده A: گوشی اپل iPhone 15 Pro Max ظرفیت 256 گیگفروشنده B: Apple iPhone 15 ProMax 256GB رجیستر شدهفروشنده C: آیفون 15 پرومکس 256 نچرال تیتانیوماما این‌ها نباید با هم merge شوند:iPhone 15 Pro Max 256GB&#xA;iPhone 15 Pro Max 512GB&#xA;iPhone 15 Pro 256GB&#xA;iPhone 15 Plus 256GBتفاوت کوچک در عنوان می‌تواند محصول واقعی متفاوتی باشد.Pipeline مربوط به matchingcandidate generation برای scale حیاتی است. به جای اینکه یک محصول جدید با کل catalog مقایسه شود، سیستم اول چند candidate احتمالی پیدا می‌کند.کلیدهای candidate می‌تواند شامل این‌ها باشد:categorybrandmodelbarcode یا شناسه محصولtokenهای normalize شده عنوانattributeهای مهم مثل storage، size، color، capacity یا voltageخطاهای رایج در matchingmatching بد از crawl کند خطرناک‌تر است.اشتباه‌های رایج:گوشی 256GB با 512GB merge شودلپ‌تاپ 8GB RAM با 16GB RAM merge شودعطر 50ml با 100ml merge شودمحصول original با copy merge شودbundle با محصول تکی merge شودaccessory با محصول اصلی merge شودگوشی رجیسترشده با رجیسترنشده merge شودمحصول refurbished با محصول new merge شودبرای دسته‌های حساس، بهتر است سیستم false negative را به false positive ترجیح دهد:بهتر است دو محصول جدا نشان داده شوند تا اینکه دو محصول متفاوت اشتباه merge شوند.Human Review و عملیات mergeاطلاعات عمومی ترب به دسته‌بندی و ادغام محصولات اشاره می‌کند. این یعنی حداقل برای موارد نامطمئن، یک فرایند human-in-the-loop لازم است.ابزار داخلی review باید این قابلیت‌ها را داشته باشد:پیشنهاد matchهای احتمالیpreview تصویر محصولمقایسه attributeهاسابقه فروشندهتوضیح confidenceعملیات bulkaudit logrollbackrollback مهم است. اگر دو محصول اشتباه merge شوند، سیستم باید بتواند آن‌ها را دوباره جدا کند.Freshness قیمت و موجودیfreshness لایه اعتماد در یک price-comparison product است.اگر ترب نشان دهد:قیمت در ترب: ۷۱,۵۰۰,۰۰۰ تومان&#xA;قیمت در سایت فروشنده: ۷۴,۰۰۰,۰۰۰ تومانکاربر معمولا ترب را مقصر می‌داند، حتی اگر فروشنده همین چند دقیقه قبل قیمت را عوض کرده باشد.سیستم نباید همه محصولات را با یک سرعت refresh کند. باید adaptive freshness داشته باشد.Adaptive Refresh Schedulingاولویت refresh می‌تواند به این موارد وابسته باشد:محبوبیت محصولreliability فروشندهنوسان تاریخی قیمتنوسان موجودیحساسیت دسته‌بندیزمان آخرین fetch موفقکلیک‌های اخیر کاربرگزارش mismatch از کاربرانسیگنال webhookFreshness Metadataهر offer باید metadata مربوط به freshness داشته باشد:observed_at&#xA;last_successful_fetch_at&#xA;last_failed_fetch_at&#xA;source = webhook | poll | plugin | manual&#xA;freshness_status = fresh | stale | unknown | seller_unreachableاین metadata می‌تواند رفتار صفحه محصول را کنترل کند:offer خیلی stale مخفی شودفروشنده stale پایین‌تر rank شودبا کلیک کاربر refresh فوری trigger شودبرای داده حساس، last checked نمایش داده شودبرای feedهای مهم alert به تیم پشتیبانی یا فروشنده ارسال شودSearch Indexingجست‌وجو نباید مستقیم از روی database نرمال‌شده انجام شود.search engine برای این موارد لازم است:full-text searchtypo tolerancefiltersortingfacetingprice range querycategory navigationپاسخ سریع به کاربرانیک search document می‌تواند ترکیبی از اطلاعات canonical product و خلاصه offerها باشد:{&#xA;  &#34;product_id&#34;: &#34;p_123&#34;,&#xA;  &#34;title&#34;: &#34;Apple iPhone 15 Pro Max 256GB&#34;,&#xA;  &#34;category&#34;: &#34;mobile-phone&#34;,&#xA;  &#34;brand&#34;: &#34;Apple&#34;,&#xA;  &#34;min_price&#34;: 71500000,&#xA;  &#34;max_price&#34;: 74200000,&#xA;  &#34;available_offer_count&#34;: 8,&#xA;  &#34;updated_at&#34;: &#34;2026-07-02T10:02:00Z&#34;&#xA;}تغییر offer باید event برای reindex ایجاد کند:offer.updated -&gt; reindex product summaryممکن است search index کمی از source of truth عقب باشد. پس search indexing lag باید اندازه‌گیری شود.Ranking فروشنده‌ها و offerهااگر فقط بر اساس کمترین قیمت sort کنیم، سیستم به راحتی قابل سوءاستفاده می‌شود.ranking باید چند سیگنال را در نظر بگیرد:قیمتموجود بودنtrust score فروشندهfreshnessسابقه price mismatchامکان ارسالگارانتیموقعیت کاربرقوانین تبلیغاتی یا تجاری در صورت وجودیک مدل ساده scoring:offer_score =&#xA;  price_score * 0.35&#xA;+ seller_trust_score * 0.25&#xA;+ freshness_score * 0.20&#xA;+ availability_score * 0.15&#xA;+ delivery_score * 0.05وزن‌ها به business بستگی دارد. اصل معماری این است که ranking نباید فروشنده‌ای را که قیمت گمراه‌کننده منتشر می‌کند، پاداش دهد.استراتژی Cachecache مفید است، اما در سیستم مقایسه قیمت می‌تواند خطرناک هم باشد.گزینه‌های مناسب برای cache:canonical product pages&#xA;short-lived search results&#xA;seller config and health&#xA;category and brand dictionaries&#xA;hot offer summaries with short TTLمواردی که باید با احتیاط cache شوند:price&#xA;stock&#xA;seller availability&#xA;ranking dependent on freshnessفرق ترب با دیجی‌کالا اینجاست:در دیجی‌کالا، cache بیشتر از سیستم‌های داخلی محافظت می‌کند.در ترب، cache می‌تواند stale بودن داده بیرونی را پنهان یا حتی تشدید کند.برای ترب، cache invalidation فقط مسئله performance نیست؛ مسئله trust است.مدیریت خطاproduct ingestion همیشه با خطا همراه است. سیستم باید از ابتدا برای failure طراحی شود.خطا: سایت فروشنده down استمثال: timeout یا HTTP 500پاسخ سیستم: retry با backoff، علامت seller_unreachableخطا: محصول حذف شدهمثال: HTTP 404پاسخ سیستم: بعد از تایید، offer غیرفعال شودخطا: bot بلاک شدهمثال: HTTP 403پاسخ سیستم: alert به support/فروشنده، بررسی firewallخطا: parser خراب شدهمثال: HTML سایت تغییر کردهپاسخ سیستم: انتقال به review queue مربوط به parserخطا: token API نامعتبر استمثال: plugin auth failپاسخ سیستم: pause کردن API polling و alertخطا: webhook تکراریمثال: یک event دوبار رسیدپاسخ سیستم: deduplication و idempotencyخطا: webhook گم شدهمثال: event نرسیدپاسخ سیستم: polling به عنوان safety netخطا: search index lag داردمثال: offer آپدیت شده ولی search stale استپاسخ سیستم: retry reindex و alert روی lagretry باید با exponential backoff و jitter باشد:1 دقیقه -&gt; 5 دقیقه -&gt; 30 دقیقه -&gt; کاهش priority و alertjitter مهم است، چون اگر همه workerها هم‌زمان retry کنند، خود سیستم retry storm ایجاد می‌کند.مدل Consistencyترب نمی‌تواند consistency کاملا real-time با همه فروشگاه‌ها تضمین کند. این ذات سیستم aggregator است.مدل واقع‌بینانه این است:eventual consistency with bounded freshnessنمونه هدف‌های داخلی:95% از offerهای مهم در کمتر از 15 دقیقه refresh شوند99% از offerهای فعال در کمتر از 24 ساعت refresh شوندWebhook processing p95 زیر 30 ثانیه باشدSearch index lag p95 زیر 2 دقیقه باشدبرای صفحه محصول، مدل ذهنی امن‌تر این است:ترب آخرین قیمت مشاهده‌شده را نشان می‌دهد.&#xA;قیمت و موجودی نهایی در سایت فروشنده تایید می‌شود.Abuse و کیفیت دادههر سیستم مقایسه قیمت در معرض manipulation فروشنده‌هاست.نمونه abuse:قیمت پایین غیرواقعی برای گرفتن کلیکمحصول ناموجود اما با وضعیت موجودتغییر قیمت بعد از کلیکعنوان گمراه‌کنندهlisting تکراریdiscount fake با old_price غیرواقعیmapping اشتباه به محصول پرطرفدارblock کردن crawler بعد از index شدننمایش قیمت متفاوت به crawler و کاربردفاع‌ها:ثبت گزارش price mismatch کاربرانمقایسه قیمت landing page با قیمت index شدهجریمه فروشنده‌هایی که mismatch زیاد دارندتشخیص الگوهای تخفیف مشکوکنیاز به product ID پایدار تا جای ممکناستفاده از trust score در rankingنگه داشتن تاریخچه تغییرات offerhuman review برای mergeهای حساسObservabilityچنین سیستمی بدون observability قابل اداره نیست.متریک‌های مهم:crawler success rate by seller&#xA;fetch latency by seller&#xA;HTTP status distribution&#xA;parser success rate&#xA;products discovered per day&#xA;products updated per day&#xA;webhook success/failure count&#xA;queue lag&#xA;search indexing lag&#xA;price mismatch reports&#xA;offer freshness percentiles&#xA;match confidence distribution&#xA;manual review backlogداشبوردهای مهم:سلامت فروشنده‌هاbacklog مربوط به crawlerخطاهای parserfreshness به تفکیک categorylag مربوط به search indexbacklog مربوط به matching reviewگزارش‌های price mismatchSecuritycrawler و webhook/API سطح حمله مهمی ایجاد می‌کنند.نیازهای امنیتی:احراز هویت API فروشندهvalidate کردن token یا signature برای webhookrate limit برای ingestion APIsanitize کردن HTML فروشندهvalidate کردن image URLنگهداری امن secretهاaudit کردن تغییرات seller configمحدود کردن دسترسی شبکه crawlerمهم‌ترین ریسک فنی SSRF است.اگر فروشنده بتواند URLهایی را کنترل کند که backend ترب fetch می‌کند، crawler نباید بتواند به آدرس‌های داخلی دسترسی داشته باشد:localhost&#xA;127.0.0.1&#xA;169.254.169.254&#xA;private network ranges&#xA;internal admin domainscrawler workerها بهتر است در محیط شبکه محدود اجرا شوند.Edge Caseهای مهماین‌ها نمونه edge caseهایی هستند که سیستم را در production سخت می‌کنند.Seller Integrationفروشنده domain را عوض می‌کندساختار URL تغییر می‌کندفروشنده CMS را عوض می‌کندplugin غیرفعال می‌شودنسخه plugin قدیمی استfirewall فروشنده IPهای crawler را block می‌کندAPI فروشنده داده stale برمی‌گرداندSSL certificate مشکل داردسایت فقط از IP ایران باز می‌شودچند فروشگاه روی یک domain هستندProduct Discoverysitemap شامل URL غیرمحصولی استsitemap خیلی بزرگ استsitemap محصول حذف‌شده داردcategory pageها infinite pagination دارندیک محصول در چند category دیده می‌شودURL محصول به query parameter وابسته استcanonical URL اشتباه یا خالی استProduct Dataقیمت وجود نداردقیمت صفر استقیمت، مبلغ قسط است نه قیمت نهاییخطای تبدیل ریال و تومانقیمت تخفیفی از قیمت قبلی بیشتر استمحصول موجود است ولی add-to-cart غیرفعال استعنوان محصول SEO spam داردتصویر خراب یا hotlink-protected استVariantsparent product قیمت نداردقیمت variant بر اساس رنگ فرق می‌کندموجودی variant بر اساس سایز فرق می‌کندیک URL چند variant داردهر variant URL جدا داردفروشنده فقط default variant را expose می‌کندMatchingظرفیت متفاوت استگارانتی متفاوت استرجیسترشده و رجیسترنشده فرق دارندbundle با محصول تکی اشتباه گرفته می‌شودoriginal و copy اشتباه merge می‌شوندrefurbished و new اشتباه merge می‌شوندaccessory با محصول اصلی merge می‌شودFreshnesswebhook می‌گوید محصول تغییر کرده ولی API هنوز داده قدیمی می‌دهدpolling به cache stale فروشنده می‌خوردمحصول flash sale خیلی سریع موجودی عوض می‌کندsearch index هنوز min_price قبلی را داردکاربر وقتی refresh در حال انجام است کلیک می‌کندAbuseقیمت پایین fakeredirect به محصول نامرتبطofferهای تکراری با قیمت پایینold_price fake برای نمایش تخفیفblock کردن crawler بعد از index شدنقیمت متفاوت بر اساس user-agentانتخاب تکنولوژیstack دقیق از boundaryهای معماری کم‌اهمیت‌تر است، اما یک پیاده‌سازی منطقی می‌تواند از این جنس ابزارها استفاده کند:نیاز: Backend APIنوع تکنولوژی: Go, Java, Python, Node.jsنیاز: Queue/Eventنوع تکنولوژی: Kafka, RabbitMQ, SQS, Redis Streamsنیاز: Crawler Workerنوع تکنولوژی: autoscaled workers یا Kubernetes jobsنیاز: Raw Response Storageنوع تکنولوژی: object storageنیاز: Catalog DBنوع تکنولوژی: PostgreSQL/MySQLنیاز: Offer Storeنوع تکنولوژی: relational DB، wide-column store یا event-sourced storeنیاز: Searchنوع تکنولوژی: Elasticsearch/OpenSearchنیاز: Cacheنوع تکنولوژی: Redisنیاز: Analyticsنوع تکنولوژی: ClickHouse/BigQuery-style warehouseنیاز: Observabilityنوع تکنولوژی: Prometheus, Grafana, OpenTelemetryخلاصه برای توضیح در مصاحبه یا بحث فنیاگر بخواهیم طراحی را در یک دقیقه توضیح دهیم:سیستم‌هایی شبیه ترب محصولات را از مسیرهای مختلف مثل crawler، feed، plugin، API، webhook و جریان دستی فروشنده وارد می‌کنند. مدل اصلی سیستم، جداسازی canonical product از seller offer است. ingestion به صورت event-driven انجام می‌شود: URL محصول کشف می‌شود، داده خام fetch می‌شود، فیلدها استخراج و normalize می‌شوند، محصول با catalog اصلی match می‌شود، offer فروشنده به‌روزرسانی می‌شود و search index آپدیت می‌شود. چون داده فروشنده‌ها همیشه قابل‌اعتماد و real-time نیست، سیستم به adaptive refresh، freshness metadata، retry، human review، seller trust score و abuse detection نیاز دارد. serving از روی search index و cache انجام می‌شود، در حالی که ingestion eventually consistent باقی می‌ماند.جمع‌بندیمسئله ترب فقط scraping نیست. مسئله اصلی، کیفیت داده، matching، freshness و trust است.مهم‌ترین تصمیم‌های طراحی:جدا کردن canonical product از seller offerاستفاده از چند مسیر ingestion به جای تکیه کامل بر crawlerجدی گرفتن product matching به عنوان سخت‌ترین بخش سیستماستفاده از adaptive freshness به جای refresh یکسان برای همه محصولاتسرو کردن کاربر از search index و cache، نه از سایت فروشنده به صورت liveساختن human review، observability و abuse prevention از ابتداpipeline کلی سیستم را می‌توان این‌طور خلاصه کرد:Discover -&gt; Fetch -&gt; Extract -&gt; Normalize -&gt; Match -&gt; Review -&gt; Index -&gt; Serve -&gt; Monitorاین همان طراحی سیستمی است که می‌تواند هزاران catalog نامنظم فروشنده را به یک تجربه قابل استفاده برای مقایسه محصول و قیمت تبدیل کند.</description><category>افشین یاری نیا</category><author>افشین یاری نیا</author><pubDate>Sun, 05 Jul 2026 20:59:29 +0330</pubDate></item><item><title>معماری پنهان کشینگ در دیجی‌کالا؛ از CDN تا کنترل موجودی</title><link>https://virgool.ir/@m_58197875/معماری-پنهان-کشینگ-در-دیجی-کالا-f25nhzmvsoyl</link><description>یک پلتفرم بزرگ چطور میلیون‌ها بازدید روزانه را مدیریت می‌کند؟وقتی از بیرون به یک فروشگاه اینترنتی بزرگ نگاه می‌کنیم، همه چیز ساده به نظر می‌رسد.کاربر وارد سایت می‌شود، محصولی را جستجو می‌کند، صفحه محصول را باز می‌کند، کالا را به سبد خرید اضافه می‌کند، پرداخت می‌کند و منتظر دریافت سفارش می‌ماند.اما پشت همین مسیر ساده، یکی از پیچیده‌ترین چالش‌های مهندسی نرم‌افزار قرار دارد.در پلتفرمی مثل دیجی‌کالا، ما فقط با چند صفحه محصول و چند کاربر همزمان روبه‌رو نیستیم. با میلیون‌ها بازدید، صدها هزار فروشنده، تعداد زیادی کالا، تغییرات لحظه‌ای قیمت و موجودی، کمپین‌های فروش، پیشنهادهای شخصی‌سازی‌شده و حجم زیادی از درخواست‌های همزمان سروکار داریم.برای مثال، یک کاربر ممکن است وارد دسته‌بندی گوشی موبایل شود و محصولات را بر اساس معیارهای مختلف مرتب کند:مرتبط‌ترینپربازدیدترینجدیدترینپرفروش‌ترینارزان‌ترینگران‌ترینسریع‌ترین ارسالپیشنهاد خریدارانکالاهای منتخبهر کدام از این انتخاب‌ها، از دید کاربر فقط یک کلیک ساده است. اما از دید سیستم، هر کلیک یعنی اجرای چندین درخواست، خواندن داده از چند منبع، اعمال فیلترها، بررسی قیمت، بررسی موجودی، محاسبه زمان ارسال و در نهایت ساختن خروجی مناسب برای کاربر.در چنین مقیاسی، سیستم باید در هر لحظه به چند سؤال مهم پاسخ دهد:در نتایج جستجو چه محصولاتی باید نمایش داده شوند؟قیمت دقیق محصول در همین لحظه چقدر است؟آیا کالا هنوز موجود است؟کدام فروشنده باید به عنوان پیشنهاد اصلی نمایش داده شود؟زمان ارسال برای این کاربر، با توجه به موقعیت مکانی او، چقدر است؟کدام بخش از صفحه را می‌شود کش کرد؟اگر قیمت یا موجودی تغییر کند، چه اتفاقی برای داده‌های کش‌شده می‌افتد؟اینجاست که کشینگ دیگر یک تکنیک ساده برای افزایش سرعت نیست. در این مقیاس، کشینگ به یکی از پایه‌های اصلی معماری سیستم تبدیل می‌شود.این مقاله تلاش می‌کند معماری کشینگ در یک پلتفرم بزرگ تجارت الکترونیک را با زبانی ساده‌تر بررسی کند. تمرکز اصلی روی الگوهایی است که در پلتفرم‌هایی هم‌مقیاس با دیجی‌کالا استفاده می‌شوند.معماری ساده در برابر واقعیت مقیاس بزرگدر یک فروشگاه اینترنتی کوچک، معماری سیستم معمولا ساده است.کاربر صفحه اصلی را باز می‌کند.سرور از دیتابیس محصولات را می‌خواند.نتیجه به کاربر برمی‌گردد.کاربر صفحه محصول را باز می‌کند.سرور دوباره از دیتابیس اطلاعات همان محصول را می‌خواند.کاربر کالا را به سبد خرید اضافه می‌کند.دیتابیس به‌روزرسانی می‌شود.این مدل برای یک فروشگاه کوچک قابل قبول است. اما وقتی ترافیک بالا می‌رود، دیگر نمی‌شود برای هر درخواست مستقیما سراغ دیتابیس اصلی رفت.اگر میلیون‌ها کاربر همزمان وارد سایت شوند و هر کدام چندین بار جستجو کنند، صفحه محصول باز کنند، فیلتر بزنند و قیمت‌ها را بررسی کنند، دیتابیس اصلی زیر فشار شدید درخواست‌های خواندن قرار می‌گیرد.در معماری‌های بزرگ، دیتابیس اصلی باید نقش منبع مرجع حقیقت را داشته باشد. یعنی جایی که داده معتبر و نهایی در آن نگهداری می‌شود. اما همه درخواست‌ها نباید مستقیم به آن برسند.در چنین معماری‌ای، داده‌ها معمولا از چند لایه عبور می‌کنند:دیتابیس اصلیجریان رویدادهاایندکس جستجولایه‌های کشCDNلایه APIکاربرچالش اصلی اینجاست:کدام داده را می‌شود برای چند دقیقه کش کرد؟کدام داده باید همیشه تازه باشد؟کدام داده قبل از پرداخت باید حتما از منبع اصلی بررسی شود؟پاسخ این سؤال‌ها، کیفیت معماری کشینگ را مشخص می‌کند.صفحه محصول فقط یک رکورد ساده در دیتابیس نیستاز نگاه کاربر، صفحه محصول یک صفحه واحد است. اما از نگاه فنی، این صفحه از چندین بخش مستقل ساخته شده است.یک صفحه محصول ممکن است شامل این داده‌ها باشد:عنوان و مشخصات کالاتصاویر محصولاطلاعات فروشندگانقیمت فعلیتخفیف‌ها و کمپین‌هاموجودی انبارزمان و روش ارسالاطلاعات گارانتیامتیازها و نظرات کاربرانپرسش و پاسخ‌هامحصولات مشابهوضعیت سبد خرید کاربروضعیت علاقه‌مندی‌های کاربرهمه این داده‌ها اهمیت یکسانی ندارند. همه آن‌ها هم نیاز به تازگی یکسان ندارند.عنوان محصول ممکن است ساعت‌ها تغییر نکند.تصاویر محصول ممکن است هفته‌ها ثابت بمانند.نظرات کاربران با چند دقیقه تأخیر هم قابل قبول هستند.اما قیمت و موجودی حساس‌اند.اگر کاربر قیمتی قدیمی ببیند، مشکل ایجاد می‌شود. اگر کالایی را موجود ببیند ولی هنگام خرید متوجه شود ناموجود است، اعتماد او به سیستم کم می‌شود.پس یک صفحه محصول نباید فقط یک سیاست کش داشته باشد. باید هر بخش صفحه جداگانه بررسی شود.بخش‌های پایدار، کش بلندتر می‌گیرند.بخش‌های نیمه‌پویا، کش کوتاه‌تر می‌گیرند.بخش‌های حساس مثل قیمت و موجودی، یا کش نمی‌شوند یا با دقت زیادی مدیریت می‌شوند.این یکی از مهم‌ترین اصول کشینگ در مقیاس بزرگ است.لایه‌های اصلی کشینگ در یک پلتفرم بزرگمعماری کشینگ معمولا فقط یک Redis جلوی دیتابیس نیست. در سیستم‌های بزرگ، کش در چند لایه مختلف قرار می‌گیرد. هر لایه هدف خاص خودش را دارد.۱. کش CDNاولین لایه مهم، CDN است.CDN برای نگهداری و ارائه فایل‌هایی استفاده می‌شود که تغییرات کمی دارند. مثل:تصاویر محصولاتفایل‌های JavaScriptفایل‌های CSSفونت‌هاآیکون‌هاصفحات عمومی و لندینگ‌هادر یک فروشگاه تصویرمحور، حجم زیادی از ترافیک مربوط به تصاویر است. اگر همه تصاویر مستقیم از سرور اصلی خوانده شوند، سرورهای اصلی بخش زیادی از توان خود را صرف ارسال فایل‌های استاتیک می‌کنند.CDN این بار را از روی سرور اصلی برمی‌دارد. فایل‌ها در نقاط مختلف شبکه نگهداری می‌شوند و کاربر از نزدیک‌ترین نقطه پاسخ می‌گیرد.برای مدیریت تغییر تصاویر هم معمولا از نسخه‌گذاری در آدرس فایل استفاده می‌شود.مثلا به جای اینکه تصویر اصلی محصول همیشه یک URL ثابت داشته باشد، با تغییر تصویر، آدرس آن هم نسخه جدید می‌گیرد:/products/123/main_v4.webpبا این کار نیازی نیست همیشه کش قبلی به صورت دستی پاک شود. چون آدرس فایل جدید است، CDN آن را به عنوان فایل جدید می‌شناسد.۲. کش صفحه و Edge Cacheبعضی صفحات ترافیک زیادی دارند و برای همه کاربران تقریبا مشابه هستند.مثلا:صفحه اصلیصفحات دسته‌بندیلندینگ‌های کمپینصفحات سئوصفحات عمومی برندهااین صفحات گزینه‌های خوبی برای کش در لبه شبکه یا در لایه رندر سمت سرور هستند.اما مشکل اینجاست که همه بخش‌های این صفحات ثابت نیستند.مثلا در صفحه دسته‌بندی، پوسته صفحه ممکن است ثابت باشد، ولی قیمت محصولات، موجودی و زمان ارسال باید تازه‌تر باشند.راهکار مناسب، جدا کردن بخش‌های صفحه است.پوسته صفحه می‌تواند TTL طولانی‌تر داشته باشد.لیست محصولات می‌تواند TTL کوتاه‌تری داشته باشد.قیمت و موجودی می‌توانند از API جداگانه با کش کوتاه یا داده زنده خوانده شوند.اطلاعات اختصاصی کاربر نباید در کش عمومی قرار بگیرد.این جداسازی باعث می‌شود هم سرعت بالا بماند، هم ریسک نمایش داده اشتباه کاهش پیدا کند.۳. کش پاسخ APIدر معماری‌های امروزی، بخش زیادی از ترافیک از طریق APIها مدیریت می‌شود.اپلیکیشن موبایل، وب‌سایت، پنل‌ها و سرویس‌های داخلی همگی به APIها درخواست می‌زنند. اگر هر درخواست API مستقیم به دیتابیس یا سرویس‌های سنگین برسد، سیستم به سرعت تحت فشار قرار می‌گیرد.بعضی APIها گزینه‌های خوبی برای کش هستند.مثلا:جستجوی کلمات محبوبدسته‌بندی‌های پربازدیدلیست محصولات یک دستهاطلاعات عمومی محصولبنرها و کمپین‌های عمومیاما بعضی APIها نباید در کش عمومی قرار بگیرند.مثلا:سبد خرید کاربرآدرس‌های کاربرکیف پولسفارش‌هااطلاعات حساب کاربریدر کش API، طراحی Cache Key اهمیت زیادی دارد.مثلا اگر کاربر در تهران و شیراز نتیجه متفاوتی برای زمان ارسال یا موجودی می‌گیرد، شهر باید بخشی از کلید کش باشد.نمونه ساده:search:v1:q=iphone:city=tehranاگر شهر در کلید کش نباشد، ممکن است کاربر تهرانی نتیجه‌ای را ببیند که برای شهر دیگری ساخته شده است. این خطا در مقیاس بزرگ می‌تواند تجربه کاربری را خراب کند.۴. کش در لایه جستجوموتورهای جستجو مثل Elasticsearch یا OpenSearch معمولا برای خواندن سریع داده‌ها استفاده می‌شوند. اما این موتورها منبع اصلی حقیقت نیستند.در فروشگاه‌های بزرگ، جستجو فقط پیدا کردن یک کلمه نیست. سیستم باید فیلترها، مرتب‌سازی‌ها، وضعیت کالا، دسته‌بندی، برند، قیمت، موجودی و رفتار کاربران را هم در نظر بگیرد.برای کاهش فشار روی موتور جستجو، معمولا نتیجه جستجو به چند مرحله تقسیم می‌شود.در مرحله اول، موتور جستجو فقط شناسه محصولات مرتبط را برمی‌گرداند.مثلا:[123, 456, 789]این لیست می‌تواند برای مدت کوتاهی کش شود.در مرحله دوم، سرویس‌های دیگر اطلاعات کارت محصول را بر اساس این شناسه‌ها کامل می‌کنند.در مرحله سوم، قیمت و موجودی تازه روی کارت‌ها قرار می‌گیرد.این مدل باعث می‌شود موتور جستجو درگیر ساختن کامل همه داده‌های محصول نشود. همچنین سیستم کنترل بیشتری روی تازگی داده‌های حساس دارد.۵. کش بخش‌های مختلف محصولیکی از اشتباه‌های رایج در کشینگ این است که کل صفحه محصول به عنوان یک واحد کش شود.این کار در ظاهر ساده است، اما در عمل مشکل ایجاد می‌کند.فرض کنید فقط قیمت محصول تغییر کرده است. اگر کل صفحه محصول کش شده باشد، باید کل کش صفحه پاک شود. در حالی که بسیاری از بخش‌های صفحه، مثل توضیحات، تصاویر و مشخصات فنی، تغییری نکرده‌اند.راه بهتر این است که صفحه محصول به بخش‌های کوچک‌تر تقسیم شود.مثلا:کش مشخصات محصولکش تصاویرکش توضیحاتکش نظراتکش پرسش و پاسخکش پیشنهادهای مشابهکش فروشندگانکش قیمت و موجودیدر این مدل، اگر فقط قیمت تغییر کند، لازم نیست همه بخش‌ها از کش خارج شوند. فقط همان بخش مرتبط به قیمت به‌روزرسانی می‌شود.این کار باعث کاهش فشار روی دیتابیس، افزایش سرعت پاسخ‌دهی و کنترل بهتر روی داده‌های حساس می‌شود.۶. کش قیمت و موجودیحساس‌ترین بخش کشینگ در فروشگاه اینترنتی، قیمت و موجودی است.قیمت و موجودی مستقیما با اعتماد کاربر و فرآیند پرداخت ارتباط دارند. اگر این داده‌ها اشتباه باشند، آسیب جدی به تجربه کاربر وارد می‌شود.موجودی هم معمولا فقط یک عدد ساده نیست.موجودی واقعی می‌تواند از چند بخش تشکیل شود:موجودی اعلام‌شده فروشندهموجودی انبارموجودی رزروشدهموجودی فروخته‌شدهموجودی قابل ارسال در شهر یا بازه زمانی خاصبه همین دلیل، سیستم‌های بزرگ معمولا بین دو مرحله تفاوت می‌گذارند.مرحله اول، زمانی است که کاربر در حال گشت‌وگذار در سایت است. در این مرحله، سیستم می‌تواند از داده کش‌شده یا کمی تقریبی استفاده کند.مرحله دوم، زمانی است که کاربر وارد پرداخت و ثبت سفارش می‌شود. در این مرحله، سیستم باید قیمت و موجودی را از منبع معتبر بررسی کند.در مرحله نهایی، موجودی باید به صورت اتمیک رزرو شود. یعنی سیستم باید مطمئن شود چند کاربر همزمان نمی‌توانند آخرین موجودی یک کالا را همزمان بخرند.پس کش قیمت و موجودی باید با احتیاط طراحی شود. سرعت مهم است، اما دقت در لحظه خرید مهم‌تر است.ابطال کش، سخت‌ترین بخش ماجراکش کردن داده‌ها ساده است. اما اینکه بدانیم چه زمانی باید کش را پاک کنیم، بخش سخت ماجراست.در یک مارکت‌پلیس بزرگ، داده‌ها مدام تغییر می‌کنند.قیمت عوض می‌شود.موجودی کم یا زیاد می‌شود.فروشنده فعال یا غیرفعال می‌شود.کمپین جدید شروع می‌شود.کالا از دسترس خارج می‌شود.الگوریتم مرتب‌سازی تغییر می‌کند.برای مدیریت این تغییرات، چند روش رایج وجود دارد.۱. انقضا بر اساس زماندر این روش، برای هر داده یک زمان اعتبار مشخص می‌شود.مثلا نظرات کاربران برای ۱۰ دقیقه کش شوند.لیست محصولات یک دسته برای ۳۰ ثانیه کش شود.بنرهای صفحه اصلی برای چند دقیقه کش شوند.این روش ساده و مقاوم است. حتی اگر سیستم ابطال کش درست کار نکند، داده بعد از مدتی خودبه‌خود منقضی می‌شود.اما این روش برای داده‌های حساس کافی نیست. چون ممکن است در همان بازه کوتاه، داده اشتباه به کاربر نمایش داده شود.۲. ابطال بر اساس رویداددر این روش، هر تغییر مهم یک رویداد تولید می‌کند.مثلا وقتی قیمت تغییر می‌کند، رویدادی با این مفهوم منتشر می‌شود:ProductPriceChangedسپس Workerها این رویداد را دریافت می‌کنند و کلیدهای کش مرتبط را پاک می‌کنند یا به‌روزرسانی می‌کنند.این روش دقیق‌تر است، اما پیچیدگی بیشتری دارد.باید مطمئن شویم پیام‌ها از بین نمی‌روند.باید تأخیر پردازش رویدادها کنترل شود.باید در برابر تکرار پیام‌ها مقاوم باشیم.باید بدانیم هر رویداد دقیقا کدام کلیدهای کش را تحت تأثیر قرار می‌دهد.در سیستم‌های بزرگ، معمولا TTL و Event-Based Invalidation کنار هم استفاده می‌شوند. TTL نقش لایه ایمنی را دارد و رویدادها دقت سیستم را بالا می‌برند.۳. کلیدهای نسخه‌دارگاهی پاک کردن میلیون‌ها کلید کش کار درستی نیست. مخصوصا هنگام تغییرات بزرگ، مثل شروع کمپین یا تغییر الگوریتم مرتب‌سازی.در این شرایط، نسخه کلید کش تغییر می‌کند.مثلا:search:v1:q=iphoneبه این تبدیل می‌شود:search:v2:q=iphoneبا این کار، سیستم به جای پاک کردن همه کلیدهای قبلی، از نسخه جدید کلیدها استفاده می‌کند. کلیدهای قدیمی هم بعد از پایان TTL خودشان از بین می‌روند.این روش برای تغییرات گسترده، امن‌تر و کم‌هزینه‌تر است.Hot Key و خطر فروپاشی کشدر روزهای عادی، درخواست‌ها بین محصولات مختلف پخش می‌شوند. اما در کمپین‌ها، فروش‌های ویژه یا زمان معرفی یک کالای محبوب، ممکن است همه کاربران همزمان به یک محصول خاص هجوم بیاورند.در این حالت، یک کلید کش بیش از حد پرمصرف می‌شود. به این وضعیت Hot Key می‌گویند.حالا فرض کنید همان کلید کش ناگهان منقضی شود. هزاران درخواست همزمان متوجه می‌شوند داده در کش وجود ندارد. اگر همه آن‌ها مستقیم به دیتابیس بروند، دیتابیس تحت فشار شدید قرار می‌گیرد.به این مشکل Cache Stampede می‌گویند.برای جلوگیری از این اتفاق، چند الگوی مهم استفاده می‌شود.Request Coalescingاگر داده‌ای در کش وجود نداشته باشد، فقط یک درخواست اجازه دارد به دیتابیس برود و داده را دوباره بسازد.بقیه درخواست‌ها منتظر می‌مانند تا همان درخواست اول نتیجه را برگرداند و کش را پر کند.این روش جلوی هجوم همزمان درخواست‌ها به دیتابیس را می‌گیرد.Stale While Revalidateدر این روش، اگر داده کمی قدیمی شده باشد، سیستم همان داده قدیمی را به کاربر نشان می‌دهد، اما همزمان در پس‌زمینه کش را به‌روزرسانی می‌کند.این روش برای داده‌هایی مناسب است که کمی تأخیر در آن‌ها قابل قبول است.مثلا نمایش تعداد تقریبی بازدید، نظرات، پیشنهادهای مشابه یا بعضی لیست‌های عمومی.Jitterاگر هزاران کلید کش TTL یکسان داشته باشند، ممکن است همه در یک زمان منقضی شوند.برای جلوگیری از این اتفاق، به زمان انقضای کش مقدار کمی تصادفی اضافه می‌شود.مثلا به جای اینکه همه کلیدها دقیقا ۶۰ ثانیه عمر کنند، بعضی ۵۲ ثانیه، بعضی ۶۸ ثانیه و بعضی ۷۴ ثانیه عمر می‌کنند.این کار باعث می‌شود فشار روی سیستم پخش شود.کشینگ در فروش‌های شگفت‌انگیزفروش‌های شگفت‌انگیز یا Flash Sale یکی از سخت‌ترین سناریوها برای معماری کشینگ هستند.در این شرایط، تعداد زیادی کاربر همزمان برای خرید یک کالای محدود تلاش می‌کنند. اگر سیستم درست طراحی نشده باشد، مشکل فقط کندی سایت نیست. ممکن است oversell رخ دهد، یعنی تعداد فروش از موجودی واقعی بیشتر شود.برای کنترل این وضعیت، معمولا چند مکانیزم کنار هم استفاده می‌شود.صف انتظار برای کاربرانRate Limiting برای کنترل تعداد درخواست‌هاToken Bucket برای محدود کردن تعداد خرید معتبررزرو اتمیک موجودی در مرحله نهاییبررسی قطعی قیمت و موجودی هنگام پرداختکش کوتاه‌مدت برای نمایش سریع داده در مرحله مرور سایتدر این مدل، کشینگ به تنهایی کافی نیست. کش باید کنار صف، محدودیت نرخ درخواست، قفل موجودی و پردازش تراکنش امن قرار بگیرد.هدف اصلی این است که کاربر پاسخ سریع بگیرد، اما هسته اصلی سفارش‌گذاری قربانی ترافیک بالا نشود.شخصی‌سازی بدون نابود کردن کششخصی‌سازی برای فروشگاه اینترنتی مهم است. کاربر دوست دارد پیشنهادهایی ببیند که با علاقه و رفتار او مرتبط باشد.اما اگر کل صفحه برای هر کاربر به شکل اختصاصی ساخته شود، نرخ موفقیت کش کاهش پیدا می‌کند. در این حالت، هر کاربر خروجی جداگانه دارد و کش عمومی کمتر استفاده می‌شود.راه‌حل بهتر، شخصی‌سازی جزئی است.یعنی بخش‌های عمومی صفحه از کش مشترک خوانده شوند و فقط بخش‌های کوچک اختصاصی برای هر کاربر جداگانه بارگذاری شوند.مثلا:پوسته صفحه از کش عمومیبنرهای کمپین از کش عمومیدسته‌بندی‌های محبوب از کش عمومیلیست محصولات پربازدید از کش عمومیتعداد آیتم‌های سبد خرید از API اختصاصیپیشنهادهای مبتنی بر بازدیدهای اخیر از API اختصاصیوضعیت علاقه‌مندی‌ها از API اختصاصیاین مدل تعادل خوبی ایجاد می‌کند. هم سایت سریع می‌ماند، هم تجربه کاربر شخصی‌تر می‌شود.نمای کلی معماری کشینگیک معماری حرفه‌ای برای کشینگ در پلتفرم‌های بزرگ می‌تواند به شکل زیر دیده شود:کاربرCDN و Edge Cacheلایه فرانت‌اند یا BFFAPI Gatewayسرویس محصولسرویس جستجوسرویس قیمت و موجودیکش‌های اختصاصی هر سرویسدیتابیس‌های اصلیبستر رویدادهاWorkerهای ابطال کشسیستم مانیتورینگدر این معماری، هر سرویس مسئول بخشی از داده است.سرویس محصول اطلاعات پایدارتر محصول را مدیریت می‌کند.سرویس جستجو نتایج قابل جستجو و فیلتر را آماده می‌کند.سرویس قیمت و موجودی داده‌های حساس‌تر را کنترل می‌کند.بستر رویدادها تغییرات مهم را به بخش‌های مختلف اطلاع می‌دهد.Workerها کش‌های مرتبط را پاک یا به‌روزرسانی می‌کنند.سیستم مانیتورینگ هم خطاها، تأخیرها، نرخ موفقیت کش و فشار روی سرویس‌ها را بررسی می‌کند.بدون مانیتورینگ، کشینگ به یک جعبه سیاه خطرناک تبدیل می‌شود. تیم فنی باید بداند:Cache Hit Rate چقدر است؟کدام کلیدها Hot Key شده‌اند؟کدام کش‌ها بیشترین خطا را دارند؟ابطال کش چقدر تأخیر دارد؟چند درصد درخواست‌ها به دیتابیس اصلی می‌رسند؟در زمان کمپین فشار روی کدام سرویس بیشتر می‌شود؟کشینگ بدون مشاهده‌پذیری، قابل اعتماد نیست.جمع‌بندیمدیریت میلیون‌ها بازدید روزانه در یک مارکت‌پلیس بزرگ فقط با اضافه کردن Redis به پروژه حل نمی‌شود.در چنین مقیاسی، کشینگ باید بخشی از معماری اصلی محصول باشد. هر نوع داده باید سیاست خودش را داشته باشد. بعضی داده‌ها می‌توانند چند دقیقه قدیمی باشند. بعضی داده‌ها باید تقریبا تازه بمانند. بعضی داده‌ها مثل قیمت نهایی و موجودی هنگام پرداخت، باید حتما از منبع معتبر بررسی شوند.هنر مهندسی در این نیست که همه چیز را کش کنیم. هنر اصلی این است که بدانیم چه چیزی را کجا کش کنیم، برای چه مدت کش کنیم، چه زمانی کش را باطل کنیم و چه زمانی اصلا نباید به کش اعتماد کنیم.برای پلتفرم‌هایی مثل دیجی‌کالا، کشینگ فقط یک ابزار افزایش سرعت نیست. کشینگ بخشی از طراحی اعتماد، پایداری و مقیاس‌پذیری سیستم است.</description><category>افشین یاری نیا</category><author>افشین یاری نیا</author><pubDate>Tue, 30 Jun 2026 15:01:12 +0330</pubDate></item><item><title>طراحی یک Pipeline پایدار برای Transcoding ویدیو در Cloud</title><link>https://virgool.ir/@m_58197875/طراحی-یک-pipeline-پایدار-برای-transcoding-ویدیو-در-cloud-lfqrmlqsuzov</link><description>اگر حتی یک بار سراغ ساختن یک pipeline برای استریم ویدیو رفته باشید، احتمالاً کار را با یک تصور ساده شروع کرده‌اید:یک worker بالا می‌آورم، ویدیو را دانلود می‌کنم و با یک subprocess ساده، FFmpeg را اجرا می‌کنم.روی سیستم local همه‌چیز خوب کار می‌کند. بعد پروژه وارد production می‌شود. یک کاربر ویدیوی ۲ ساعته 4K آپلود می‌کند. دیسک NVMe پر می‌شود. worker در ۹۹ درصد پردازش crash می‌کند. وضعیت دیتابیس هم با واقعیت sync نیست.Transcoding ویدیو یکی از سنگین‌ترین و غیرقابل‌پیش‌بینی‌ترین workloadهایی است که روی زیرساخت cloud اجرا می‌شود. اگر پردازش ویدیو را مثل یک اسکریپت monolithic ببینید، در عمل سرویس نمی‌سازید. یک بمب ساعتی می‌سازید.اینجا هدف این است که pipeline را طوری طراحی کنیم که input را درست stream کند، خروجی HLS یا DASH قابل اعتماد بسازد، ظرفیت NVENC را دقیق مدیریت کند، state را در دیتابیس نگه دارد و قبل از publish شدن خروجی، کیفیت را واقعاً verify کند.نقشه اصلی: جدا کردن مغز از عضلهبرای ساختن سیستمی که scale شود، باید Control Plane را از Data Plane جدا کنید.Control Plane: مغز سیستمControl Plane مسئول تصمیم‌گیری و هماهنگی است. با byteهای ویدیو کاری ندارد.این بخش API requestها، برنامه‌ریزی jobها، وضعیت دیتابیس، heartbeat workerها، retryها و ظرفیت encoderها را مدیریت می‌کند. Go برای این بخش انتخاب خوبی است، چون concurrency سبک و efficient دارد و برای مدیریت هزاران event هماهنگی مناسب است.Data Plane: عضله سیستمData Plane جایی است که کار سنگین انجام می‌شود.این بخش source ویدیو را stream می‌کند، با hardware acceleration کار می‌کند، فضای scratch دیسک را مدیریت می‌کند و ابزارهایی مثل FFmpeg و ffprobe را اجرا می‌کند.Python برای این لایه انتخاب عملی و سریع است. کار با CLI toolها، parse کردن JSON خروجی ffprobe و ساختن ابزارهای عملیاتی در Python ساده‌تر و سریع‌تر جلو می‌رود.سه درس سخت از Production۱. خطر jobهای بزرگ و بدون checkpointاشتباه رایج این است که کل پردازش را فقط به یک subprocess کور بسپاریم و هیچ checkpoint معتبری در دیتابیس نداشته باشیم.فرض کنید یک ویدیوی ۶۰ دقیقه‌ای روی یک worker پردازش می‌شود. اگر Kubernetes در دقیقه ۵۸ آن pod را evict کند، سیستم باید دقیقاً بداند job در چه stateای بوده، روی چه workerای اجرا می‌شده و آیا باید retry شود یا failed بماند.راه‌حل اصلی اینجا stateful orchestration است، نه الزاماً خرد کردن transcoding به jobهای کوچک.قبل از اجرا، metadata ویدیو با ffprobe خوانده می‌شود. Job Planner بر اساس پروفایل‌های موردنیاز، یک job قابل retry می‌سازد. Worker هنگام شروع job، دیتابیس را update می‌کند، heartbeat می‌فرستد و خروجی را فقط وقتی ready اعلام می‌کند که upload و QC کامل شده باشد.اگر worker بمیرد، Control Plane از روی heartbeat و status دیتابیس تصمیم می‌گیرد job را retry کند. این مدل ساده، قابل فهم و برای بسیاری از pipelineهای production عملی است.۲. فشار S3، دیسک local و بحران scratch spaceاشتباه رایج بعدی این است که worker اول فایل خام ۵۰ گیگابایتی را کامل دانلود کند و بعد FFmpeg را اجرا کند.این کار دو مشکل می‌سازد. اول، workerها مدام روی disk I/O bottleneck می‌شوند. دوم، مجبور می‌شوید برای هر worker دیسک local بزرگ و گران provision کنید.راه‌حل: Input Network Streamingدانلود اولیه را حذف کنید.FFmpeg می‌تواند فایل source را مستقیم از روی presigned S3 URL با HTTP stream بخواند. روش دیگر، استفاده از UNIX pipe است.در این مدل، local storage فقط نقش scratch space موقت را دارد. worker خروجی‌های موقت و فایل‌های لازم برای packaging را کوتاه‌مدت روی NVMe می‌نویسد و بلافاصله خروجی encode شده را دوباره به S3 می‌فرستد.دیسک local نباید محل نگهداری فایل اصلی باشد. باید فقط یک بافر سریع و موقت باشد.۳. قاتل خاموش GPU: اشباع شدن NVENC sessionهاخیلی‌ها برای کنترل فشار روی workerها فقط به prefetch queue تکیه می‌کنند. مثلاً در RabbitMQ مقدار basic.qos را کم می‌کنند.مشکل اینجاست که queue تعداد messageها را می‌فهمد، نه ظرفیت واقعی encoder سخت‌افزاری را.در NVIDIA، همیشه GPU usage معیار درستی نیست. ممکن است CUDA coreها فقط ۲۰ درصد درگیر باشند، ولی encoder سخت‌افزاری یعنی NVENC به سقف sessionهای قابل استفاده رسیده باشد.پس بهتر است ظرفیت worker را بر اساس NVENC sessionهای آزاد مدل کنیم. وقتی jobها از h264_nvenc یا hevc_nvenc استفاده می‌کنند، contention اصلی تعداد sessionهای هم‌زمان NVENC است.Control Plane قبل از dispatch کردن job، یک session lease برای worker می‌گیرد. وقتی job تمام شد یا worker heartbeat خود را از دست داد، lease آزاد می‌شود.{&#xA;  &#34;worker_id&#34;: &#34;gpu-worker-us-east-12&#34;,&#xA;  &#34;gpu_model&#34;: &#34;NVIDIA L4&#34;,&#xA;  &#34;max_nvenc_sessions&#34;: 14,&#xA;  &#34;active_nvenc_sessions&#34;: 9,&#xA;  &#34;supported_encoders&#34;: [&#34;h264_nvenc&#34;, &#34;hevc_nvenc&#34;]&#xA;}اگر هر job یک NVENC session مصرف می‌کند، scheduler فقط بررسی می‌کند آیا session آزاد وجود دارد یا نه.البته resolution هنوز برای زمان اجرا، bitrate، VRAM و هزینه مهم است؛ اما برای قفل کردن ظرفیت encoder، معیار اصلی session است. این تفاوت کوچک در production جلوی مدل‌سازی اشتباه و over-engineering را می‌گیرد.معماری داده: State Machine را جدی بگیریددیتابیس relational باید single source of truth کل pipeline باشد.اگر چیزی در دیتابیس ثبت نشده، از نگاه سیستم اتفاق نیفتاده است.CREATE TYPE video_status AS ENUM (&#39;uploaded&#39;, &#39;probing&#39;, &#39;processing&#39;, &#39;ready&#39;, &#39;failed&#39;);&#xA;CREATE TYPE job_status AS ENUM (&#39;queued&#39;, &#39;running&#39;, &#39;succeeded&#39;, &#39;failed&#39;);&#xA;CREATE TYPE package_format AS ENUM (&#39;hls&#39;, &#39;dash&#39;);&#xA;&#xA;CREATE TABLE videos (&#xA;    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),&#xA;    source_s3_path VARCHAR(512) NOT NULL,&#xA;    duration_seconds NUMERIC(10, 2),&#xA;    input_resolution VARCHAR(20),&#xA;    current_status video_status NOT NULL DEFAULT &#39;uploaded&#39;,&#xA;    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP&#xA;);&#xA;&#xA;CREATE TABLE transcode_jobs (&#xA;    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),&#xA;    video_id UUID NOT NULL REFERENCES videos(id) ON DELETE CASCADE,&#xA;    task_type VARCHAR(50) NOT NULL,&#xA;    package_format package_format NOT NULL DEFAULT &#39;hls&#39;,&#xA;    encoder_backend VARCHAR(50) NOT NULL DEFAULT &#39;h264_nvenc&#39;,&#xA;    required_nvenc_sessions INT NOT NULL DEFAULT 1,&#xA;    status job_status NOT NULL DEFAULT &#39;queued&#39;,&#xA;    retry_attempts INT NOT NULL DEFAULT 0,&#xA;    max_retries INT NOT NULL DEFAULT 3,&#xA;    worker_id VARCHAR(100),&#xA;    error_log TEXT,&#xA;    started_at TIMESTAMP WITH TIME ZONE,&#xA;    finished_at TIMESTAMP WITH TIME ZONE&#xA;);&#xA;&#xA;CREATE INDEX idx_jobs_status ON transcode_jobs(status, package_format, encoder_backend);این مدل چند مزیت مهم دارد.اول، هر ویدیو status مشخص دارد. دوم، هر job قابل retry است. سوم، worker فعلی و خطاها ثبت می‌شوند. چهارم، scheduler می‌تواند jobهای منتظر را سریع پیدا کند و قبل از dispatch ظرفیت encoder را lease کند.در workloadهای رسانه‌ای، failure یک اتفاق نادر نیست. بخشی از رفتار طبیعی سیستم است. طراحی دیتابیس باید از ابتدا این واقعیت را بپذیرد.مدل اجرایی بهتر: ABR برای HLS و DASHدر یک video platform، خروجی نهایی معمولاً یک فایل MP4 ساده نیست. برای playback در scale، خروجی باید به شکل adaptive bitrate یا ABR ساخته شود.یعنی به‌جای یک کیفیت ثابت، چند rendition می‌سازید: مثلاً 1080p، 720p، 480p و 360p. Player بر اساس سرعت اینترنت، device و buffer کاربر بین این renditionها جابه‌جا می‌شود.HLS و DASH هر دو همین ایده را اجرا می‌کنند:HLS با playlistهای m3u8 و segmentها کار می‌کند.DASH با manifest فایل mpd و segmentهای fMP4 کار می‌کند.Codec هنوز مهم است، اما لایه delivery چیز دیگری است. ممکن است داخل HLS یا DASH از H.264 یا H.265 استفاده کنید، اما معماری scale بیشتر در packaging، manifest، segmentها، object storage، CDN و player behavior خودش را نشان می‌دهد.سیاست ساده برای rendition ladderبرای بیشتر محصول‌ها، یک ladder ساده بهتر از یک طراحی بیش از حد پیچیده است.1080p، 720p، 480p و 360p برای شروع کافی است.اگر source خودش 720p است، خروجی 1080p نسازید.اگر bitrate ورودی پایین است، با encode دوباره معجزه کیفیت نمی‌سازید؛ فقط artifact را تثبیت می‌کنید.segment duration را ثابت نگه دارید؛ ۴ تا ۶ ثانیه معمولاً انتخاب عملی است.GOP و keyframe interval باید با segment boundary هماهنگ باشد.manifest را فقط بعد از آماده شدن segmentهای معتبر publish کنید.این سیاست ساده باعث می‌شود تیم به جای بازی با codecها، روی reliability pipeline تمرکز کند.Blueprint اول: خروجی HLS با ABR Ladderاین pipeline decode را به GPU می‌سپارد، frame buffer را در VRAM split می‌کند، scale را داخل memory سخت‌افزار انجام می‌دهد و چند کیفیت را هم‌زمان encode می‌کند.هدف، کم کردن I/O و ساختن یک master playlist برای playback adaptive است.ffmpeg -y -hwaccel cuda -hwaccel_output_format cuda -i input.mp4 \&#xA;  -filter_complex &#34;\&#xA;    [0:v]split=4[v1][v2][v3][v4]; \&#xA;    [v1]scale_cuda=1920:1080[v1080]; \&#xA;    [v2]scale_cuda=1280:720[v720]; \&#xA;    [v3]scale_cuda=854:480[v480]; \&#xA;    [v4]scale_cuda=640:360[v360]&#34; \&#xA;  -map &#34;[v1080]&#34; -c:v:0 h264_nvenc -preset p4 -b:v:0 6000k -maxrate:v:0 6500k -bufsize:v:0 12000k \&#xA;  -map &#34;[v720]&#34;  -c:v:1 h264_nvenc -preset p4 -b:v:1 3000k -maxrate:v:1 3300k -bufsize:v:1 6000k \&#xA;  -map &#34;[v480]&#34;  -c:v:2 h264_nvenc -preset p4 -b:v:2 1200k -maxrate:v:2 1350k -bufsize:v:2 2400k \&#xA;  -map &#34;[v360]&#34;  -c:v:3 h264_nvenc -preset p4 -b:v:3 700k  -maxrate:v:3 800k  -bufsize:v:3 1400k \&#xA;  -map 0:a -c:a aac -b:a 128k \&#xA;  -g 180 -keyint_min 180 -sc_threshold 0 \&#xA;  -f hls -hls_time 6 -hls_playlist_type vod \&#xA;  -master_pl_name master.m3u8 \&#xA;  -hls_segment_filename &#34;stream_%v/data_%03d.ts&#34; stream_%v/playlist.m3u8نکته مهم: فایل‌های HLS اینجا خروجی packaging هستند و worker همچنان job اصلی را به شکل یک جریان کنترل‌شده اجرا می‌کند.Blueprint دوم: خروجی DASH با fMP4DASH هم همان ایده adaptive playback را دارد، اما خروجی اصلی آن manifest با پسوند mpd است. در بسیاری از stackها، segmentهای fMP4 برای DASH انتخاب تمیزتری هستند.ffmpeg -y -hwaccel cuda -hwaccel_output_format cuda -i input.mp4 \&#xA;  -filter_complex &#34;\&#xA;    [0:v]split=3[v1][v2][v3]; \&#xA;    [v1]scale_cuda=1920:1080[v1080]; \&#xA;    [v2]scale_cuda=1280:720[v720]; \&#xA;    [v3]scale_cuda=854:480[v480]&#34; \&#xA;  -map &#34;[v1080]&#34; -c:v:0 h264_nvenc -preset p4 -b:v:0 6000k -maxrate:v:0 6500k -bufsize:v:0 12000k \&#xA;  -map &#34;[v720]&#34;  -c:v:1 h264_nvenc -preset p4 -b:v:1 3000k -maxrate:v:1 3300k -bufsize:v:1 6000k \&#xA;  -map &#34;[v480]&#34;  -c:v:2 h264_nvenc -preset p4 -b:v:2 1200k -maxrate:v:2 1350k -bufsize:v:2 2400k \&#xA;  -map 0:a -c:a aac -b:a 128k \&#xA;  -g 180 -keyint_min 180 -sc_threshold 0 \&#xA;  -f dash -seg_duration 6 \&#xA;  -adaptation_sets &#34;id=0,streams=v id=1,streams=a&#34; \&#xA;  manifest.mpdنکته مهم این است که HLS و DASH فقط فرمت خروجی نیستند؛ قرارداد playback با player هستند. segment duration، keyframe alignment، MIME typeها، CDN cache behavior و manifest correctness روی تجربه کاربر اثر مستقیم دارند.Edge Caseهایی که واقعاً در Scale می‌بینیدورودی همیشه تمیز نیستفایل کاربر ممکن است metadata غلط داشته باشد، duration اشتباه گزارش کند، audio track ناقص داشته باشد، rotation tag داشته باشد، variable frame rate باشد یا وسط فایل corruption داشته باشد.قبل از transcoding، با ffprobe حداقل این موارد را بخوانید:duration واقعیcodec و resolutionframe rateوجود یا نبود audiorotation و display aspect ratiobitrate تقریبیهر خروجی نباید ساخته شوداگر input ویدیو 854x480 است، ساختن 1080p فقط storage و compute را هدر می‌دهد. Job Planner باید ladder را بر اساس source بسازد، نه یک لیست ثابت.segmentها باید با keyframe شروع شونددر HLS و DASH، player باید بتواند از ابتدای هر segment decode را شروع کند. اگر segment وسط GOP شروع شود، بعضی playerها stutter، black frame یا seek مشکل‌دار نشان می‌دهند. برای همین -g و segment duration باید با هم طراحی شوند.manifest آخرین چیزی است که publish می‌شوداگر master playlist یا MPD قبل از upload کامل segmentها روی S3/CDN visible شود، کاربر manifest سالم می‌بیند ولی segmentها 404 می‌دهند. خروجی را اول در prefix موقت بسازید، QC کنید، بعد pointer نهایی را در دیتابیس update کنید.HLS و DASH اینجا header درست می‌خواهندروی object storage و CDN، Content-Type مهم است:application/vnd.apple.mpegurl برای m3u8application/dash+xml برای mpdvideo/mp2t برای tsvideo/mp4 برای m4s یا fMP4header اشتباه همیشه در تست local دیده نمی‌شود، ولی روی device واقعی یا browser خاص playback را خراب می‌کند.audio را فراموش نکنیدخیلی از failureهای media از video نیستند. audio ممکن است چند stream داشته باشد، sample rate عجیب داشته باشد، یا از video کوتاه‌تر باشد. برای خروجی عمومی، AAC با bitrate ثابت و sample rate استاندارد انتخاب امن‌تری است.خروجی ناقص می‌تواند exit code صفر داشته باشدFFmpeg ممکن است بدون crash تمام شود، ولی manifest ناقص، segment صفر بایتی، duration mismatch یا خروجی بدون video stream بسازد. status دیتابیس فقط بعد از QC باید ready شود.برای HLS و DASH، QC فقط ffprobe روی یک فایل نیست. باید manifest را parse کنید، segmentها را بشمارید، existence آن‌ها را چک کنید و حداقل چند segment را probe کنید.idempotency مهم استRetry نباید خروجی قبلی را کورکورانه خراب کند. هر job باید output prefix موقت خودش را داشته باشد. بعد از موفقیت کامل، pointer نهایی در دیتابیس تغییر کند. این کار جلوی publish شدن خروجی نصفه را می‌گیرد.lease باید timeout داشته باشداگر worker وسط encode بمیرد، NVENC session lease نباید برای همیشه قفل بماند. lease را با heartbeat تمدید کنید و اگر heartbeat قطع شد، آن را expire کنید.backpressure فقط queue نیستQueue depth به‌تنهایی کافی نیست. باید metrics واقعی مثل active sessions، job age، retry rate، average encode speed، scratch disk pressure و S3 upload latency را ببینید.مهندسی دفاعی: Quality Control برنامه‌نویسی‌شدهنباید فرض کنید چون FFmpeg با exit code صفر تمام شده، خروجی سالم است.در پردازش فایل‌های آپلودی کاربران، corruption خاموش زیاد رخ می‌دهد. فایل صفر بایتی، audio track حذف‌شده، frame stutter، اختلاف مدت audio و video و خروجی‌های ناقص، همگی در production دیده می‌شوند.قبل از اینکه status ویدیو را در دیتابیس ready کنید، خروجی‌ها را با ffprobe اعتبارسنجی کنید.import sys&#xA;import json&#xA;import subprocess&#xA;&#xA;def verify_media_integrity(file_path):&#xA;    cmd = [&#xA;        &#34;ffprobe&#34;, &#34;-v&#34;, &#34;quiet&#34;,&#xA;        &#34;-print_format&#34;, &#34;json&#34;,&#xA;        &#34;-show_format&#34;, &#34;-show_streams&#34;,&#xA;        file_path&#xA;    ]&#xA;&#xA;    res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)&#xA;&#xA;    if res.returncode != 0:&#xA;        return False, f&#34;ffprobe execution failure: {res.stderr}&#34;&#xA;&#xA;    data = json.loads(res.stdout)&#xA;    streams = data.get(&#34;streams&#34;, [])&#xA;&#xA;    has_video = any(s[&#34;codec_type&#34;] == &#34;video&#34; for s in streams)&#xA;&#xA;    if not has_video:&#xA;        return False, &#34;Corrupt file: No video streams found.&#34;&#xA;&#xA;    if int(data[&#34;format&#34;][&#34;size&#34;]) == 0:&#xA;        return False, &#34;Corrupt file: File size is 0 bytes.&#34;&#xA;&#xA;    try:&#xA;        v_dur = next(float(s[&#34;duration&#34;]) for s in streams if s[&#34;codec_type&#34;] == &#34;video&#34;)&#xA;        a_dur = next((float(s[&#34;duration&#34;]) for s in streams if s[&#34;codec_type&#34;] == &#34;audio&#34;), None)&#xA;&#xA;        if a_dur and abs(v_dur - a_dur) &gt; 0.5:&#xA;            return False, f&#34;A/V sync drift out of bounds: {abs(v_dur - a_dur)}s&#34;&#xA;&#xA;    except KeyError:&#xA;        pass&#xA;&#xA;    return True, &#34;Passed&#34;&#xA;&#xA;if __name__ == &#34;__main__&#34;:&#xA;    is_valid, reason = verify_media_integrity(&#34;output.mp4&#34;)&#xA;&#xA;    if not is_valid:&#xA;        print(f&#34;QC Rejected: {reason}&#34;)&#xA;        sys.exit(1)&#xA;&#xA;    sys.exit(0)جمع‌بندیساختن یک video platform فقط نوشتن چند wrapper دور FFmpeg نیست. مسئله اصلی، هماهنگی زیرساخت است.باید از روز اول فرض کنید workerها می‌میرند، فایل‌ها خراب‌اند، GPUها محدودیت واقعی دارند، queueها ظرفیت encoder را نمی‌فهمند و دیتابیس باید حقیقت نهایی سیستم باشد.اگر input را stream کنید، workerها را stateful و قابل retry طراحی کنید، ظرفیت NVENC را با session lease مدیریت کنید، ladder را بر اساس source بسازید، HLS و DASH را با manifest و segment سالم publish کنید و قبل از ready کردن خروجی QC واقعی انجام دهید، pipeline شما از یک اسکریپت شکننده به یک سیستم قابل اعتماد و قابل scale تبدیل می‌شود.Transcoding ویدیو جنگ با FFmpeg نیست. جنگ با resource، failure و cost است.</description><category>افشین یاری نیا</category><author>افشین یاری نیا</author><pubDate>Sat, 27 Jun 2026 15:59:59 +0330</pubDate></item><item><title>🗺️ اپلیکیشن‌های نقشه‌محور چطور وندورهای نزدیک را در مقیاس میلیونی پیدا می‌کنند؟ (نگاهی به سیستم دیزاین اسنپ‌فود)</title><link>https://virgool.ir/@m_58197875/بررسی-معماری-فنی-و-سیستم-دیزاین-اپلیکیشن-های-نقشه-محور-o5w3accmdvtu</link><description>وقتی اپلیکیشن‌هایی مثل اسنپ‌فود را باز می‌کنید، همه‌چیز در ظاهر بسیار ساده به نظر می‌رسد: موقعیت خود را مشخص می‌کنید، لیست رستوران‌ها یا سوپرمارکت‌های اطرافتان را می‌بینید، غذای خود را انتخاب و سفارش را ثبت می‌کنید.اما زیر پوسته این تجربه کاربری روان، یکی از پیچیده‌ترین چالش‌های سیستم دیزاین (System Design) در مقیاس بالا نهفته است. در دنیای واقعی، پیدا کردن «نزدیک‌ترین رستوران» صرفاً یک محاسبه مسافت ساده نیست. سیستم باید در کسری از ثانیه به این سؤال کلیدی پاسخ دهد:کدام وندورهای واجد شرایط می‌توانند در همین لحظه به این کاربر خاص سرویس‌دهی کنند و کدام‌یک باید در بالای لیست نمایش داده شوند؟پاسخ به این سؤال یعنی ترکیب هم‌زمان نقشه‌ها، ایندکس‌های جغرافیایی، سیستم‌های کشینگ، وضعیت دسترسی لحظه‌ای وندورها، زون‌های تحویل، تخمین زمان رسیدن پیک (ETA)، رتبه‌بندی (Ranking) و ولیدیشن نهایی در زمان پرداخت.چرخه حیات یک درخواست (Request Lifecycle)وقتی کاربر اپلیکیشن را باز می‌کند، کلاینت اطلاعات زیر را به سمت سرور می‌فرستد:مختصات جغرافیایی (Lat/Lng)آدرس انتخاب‌شدهشهر یا منطقهفیلترهای فعال (مانند دسته‌بندی، رتبه‌بندی، بازه قیمتی)سپس بک‌اند برای پردازش این درخواست مراحل زیر را طی می‌کند:تشخیص محدوده: شهر یا زون تحویل کاربر را مشخص می‌کند.یافتن کاندیداها: با استفاده از یک ایندکس جغرافیایی، لیستی از وندورهای نزدیک یا وندورهایی که به آن محدوده سرویس می‌دهند را پیدا می‌کند.فیلتر وضعیت: رستوران‌های بسته یا غیرفعال را حذف می‌کند.بررسی قابلیت سرویس‌دهی: بررسی می‌کند که آیا آدرس دقیق کاربر در پلیگون (Polygon) تحویل هر رستوران قرار دارد یا خیر.تخمین زمان (ETA): زمان تقریبی آماده‌سازی و ارسال را محاسبه می‌کند.رتبه‌بندی: وندورها را بر اساس الگوریتم‌های بیزینسی و ترجیحات کاربر مرتب می‌کند.صفحه‌بندی (Pagination): صفحه اول نتایج را به کاربر برمی‌گرداند.مهم‌ترین نکته این است که سیستم در هر بار باز شدن اپلیکیشن، کل دیتابیس را اسکن نمی‌کند؛ بلکه از ایندکس‌های جغرافیایی و لیست‌های کاندیدای کش‌شده استفاده می‌کند.معماری سیستم: منبع حقیقت در برابر جستجوی سریعبرای پاسخگویی به میلیون‌ها کاربر هم‌زمان، یک ساختار معماری رایج جداسازی دیتابیس لایه پایدار (Durable Data) از لایه جستجوی سریع (Fast Lookup) است.                  [ مسیر درخواست پرترافیک کاربر ]&#xA;                                │&#xA;                                ▼&#xA;                       ┌────────────────┐&#xA;                       │  API Gateway   │&#xA;                       └────────┬───────┘&#xA;                                │&#xA;                                ▼&#xA;               ┌─────────────────────────────────┐&#xA;               │    Vendor Discovery Service     │&#xA;               └────┬───────────┬───────────┬────┘&#xA;                    │           │           │&#xA;     ┌──────────────┘           │           └──────────────┐&#xA;     ▼                          ▼                          ▼&#xA;┌─────────┐               ┌───────────┐              ┌───────────┐&#xA;│  Redis  │               │OpenSearch │              │  PostGIS  │&#xA;│ (Cache) │               │ (Search)  │              │ (Truth)   │&#xA;└─────────┘               └───────────┘              └───────────┘&#xA;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) وندورهای کاندید را بر اساس سلول‌های نقشه کش کنیم، اما رتبه‌بندی نهایی را به ازای هر کاربر شخصی‌سازی کنیم. با این کار، دیتای پایه بین همه کاربران یک محله مشترک است، اما چیدمان رستوران‌ها بر اساس علایق، اشتراک‌های ویژه و تاریخچه سفارش‌های هر فرد تغییر می‌کند. خط لوله (Pipe-Line) رتبه‌بندی چندمرحله‌ای (Multi-Stage Ranking)اجرای محاسبات سنگین هوش مصنوعی یا ماتریس‌های پیچیده ETA روی ۱۰۰۰ رستوران برای هر اسکرول کاربر، هزینه زیرساخت را به شدت بالا می‌برد و سیستم را کند می‌کند. برای حل این مشکل، فیلترینگ و رتبه‌بندی در چند مرحله (Pipeline) انجام می‌شود:[ مرحله ۱: لوکاپ جغرافیایی و محدوده ]  ──►  کاهش از ۱۰۰۰ وندور به ۳۰۰ کاندید اولیه&#xA;                   │&#xA;                   ▼&#xA;[   مرحله ۲: فیلترهای سبک بیزینسی    ]  ──►  کاهش به ۱۰۰ رستوران باز و واجد شرایط&#xA;                   │&#xA;                   ▼&#xA;[    مرحله ۳: امتیازدهی اولیه و سبک  ]  ──►  انتخاب ۳۰ وندور برتر بر اساس معیارهای کلی&#xA;                   │&#xA;                   ▼&#xA;[مرحله ۴: محاسبات سنگین ETA و شخصی‌سازی] ──►  تولید ۲۰ نتیجه نهایی و مرتب‌شده برای کاربر&#xA;این رویکرد گام‌به‌گام به سیستم اجازه می‌دهد تا دیتای ورودی به بخش‌های سنگین الگوریتم را به حداقل برساند.مدیریت هماهنگی دیتای کش و واقعیت (Cache Freshness)وضعیت دسترسی به یک رستوران مدام تغییر می‌کند؛ یک آشپزخانه ممکن است به خاطر اتمام مواد اولیه یا حجم بالای سفارشات، چند دقیقه‌ای منو را ببندد. لایه سرچ و لیست اصلی اپلیکیشن می‌تواند کمی تاخیر در به‌روزرسانی (Eventual Consistency) را تحمل کند، اما صفحه پرداخت (Checkout) هرگز نباید دیتای منقضی‌شده داشته باشد.صفحه جستجو و لیست‌ها: اولویت با سرعت بالا و استفاده حداکثری از کش است.صفحه پرداخت نهایی: اولویت با دقت ۱۰۰٪ است؛ در این مرحله سیستم مستقیماً موجودی منو، قیمت لحظه‌ای، ظرفیت آشپزخانه و زون تحویل را به صورت زنده ولیدیت می‌کند.استراتژی زمان انقضای کش‌ها (Cache TTLs)لایه‌های مختلف داده باید TTLهای متفاوتی داشته باشند:نوع داده زمان انقضای پیشنهادی (TTL)علت چیدمان تصاویر و لوگوها : چند روز تا چند هفته کاملاً استاتیک هستند و روی CDN کش می‌شوند.پروفایل و اطلاعات کلی وندور۵ الی ۳۰ دقیقه : نام و آدرس رستوران‌ها به ندرت تغییر می‌کند.خلاصه منو و قیمت‌ها۱ الی ۱۰ دقیقه : تغییرات منو معمولاً چند بار در روز رخ می‌دهد.لیست کاندیداهای نزدیک محله۳۰ الی ۳۰۰ ثانیه : بستگی به چگالی وندورها و ترافیک منطقه دارد.وضعیت باز/بسته بودن لحظه‌ای۱۰ الی ۶۰ ثانیه : برای جلوگیری از نمایش رستوران‌هایی که تازه سفارشات را بسته‌اند.تخمین زمان رسیدن (ETA)۱۰ الی ۶۰ ثانیه : به دلیل نوسانات ترافیکی و تعداد پیک‌های فعال در منطقه.پایداری سیستم در شرایط بحرانی (Graceful Degradation)در سیستم‌های بزرگ، قطعی یا کندی یک سرویس جانبی نباید کل اپلیکیشن را پایین بیاورد. سیستم دیزاین اسنپ‌فود باید به گونه‌ای باشد که در صورت بروز مشکل،کاربری را متوقف نکند:اگر Redis قطع شد: سیستم موقتاً کوئری‌ها را مستقیماً به OpenSearch یا لایه هندسی PostGIS می‌فرستد.اگر سرویس هوش مصنوعی و شخصی‌سازی لوپ داد: سیستم لیست را بر اساس یک رتبه‌بندی عمومی (مثل پرفروش‌ترین‌های منطقه) مرتب می‌کند.اگر میکرسرویس محاسبات ETA از دسترس خارج شد: سیستم یک زمان تقریبی ثابت بر اساس میانگین مسافت نشان می‌دهد.شاید کاربر لیست کاملاً ایده‌آلی نبیند، اما هسته اصلی بیزینس حفظ می‌شود و کاربر همچنان می‌تواند سفارش خود را ثبت کند.مهم‌ترین متریک‌های فنی و محصولییک سیستم توزیع‌شده جغرافیایی موفق، باید معیارهای زیر را مدام پایش کند:میزان تاخیر جستجو (p95/p99 Latency): سرعت پاسخ‌دهی در ساعات پیک شام و ناهار.نرخ خطای مغایرت وضعیت (Availability Mismatch Rate): چه تعداد کاربر رستورانی را دیده‌اند اما بعد از کلیک متوجه شده‌اند بسته است؟نرخ لغو سفارش در مرحله پرداخت (Checkout Rejection Rate): چه میزان از سفارش‌ها در گام آخر به دلیل دیتای منقضی‌شده لایه کش ریجکت می‌شوند؟ (این عدد باید نزدیک به صفر باشد). نتیجه‌گیری و خلاصه معماریاگر بخواهیم بزرگ‌ترین درس مهندسی در طراحی سیستم‌های لوکیشن‌محور را خلاصه کنیم، باید بگوییم:تفکر خود را از «پیدا کردن رستوران‌های نزدیک به لوکیشن کاربر» تغییر دهید به «پیدا کردن وندورهای فعالی که در حال حاضر اجازه و توانایی سرویس‌دهی به سلول نقشه‌ای کاربر را دارند».همین تغییر کوچک در پارادایم فکری، ساختار دیتابیس‌ها، استراتژی کشینگ و کل معماری فنی شما را دگرگون و آماده پذیرش میلیون‌ها درخواست در ثانیه می‌کند.</description><category>افشین یاری نیا</category><author>افشین یاری نیا</author><pubDate>Thu, 25 Jun 2026 17:58:48 +0330</pubDate></item></channel></rss>