لمنصات التوصيل نمط حركة قاسٍ: لا شيء تقريبًا في الرابعة فجرًا، ثم جدار من الطلبات عند 12:00 و19:00. يجب أن يمتص النظام الخلفي هذه الذروات دون فقدان طلب واحد، وأن يبقي المندوبين والتجار والعملاء متزامنين، وأن يبقى بسيطًا بما يكفي ليشغّله فريق صغير. هذا هو الشكل الذي نستخدمه.
ابدأ بآلة حالة الطلب
كل خلل توصيل صحّحناه يومًا كان في جذره انتقالًا غير مشروع بين الحالات. لذا نجعل الانتقالات صريحة وقابلة لاختبار الوحدات قبل كتابة أي متحكم.
// Order state machine — explicit, testable transitions
public enum OrderStatus { Placed, Accepted, Preparing, ReadyForPickup, PickedUp, Delivered, Cancelled }
static readonly Dictionary<OrderStatus, OrderStatus[]> Allowed = new()
{
[OrderStatus.Placed] = new[] { OrderStatus.Accepted, OrderStatus.Cancelled },
[OrderStatus.Accepted] = new[] { OrderStatus.Preparing, OrderStatus.Cancelled },
[OrderStatus.Preparing] = new[] { OrderStatus.ReadyForPickup },
[OrderStatus.ReadyForPickup] = new[] { OrderStatus.PickedUp },
[OrderStatus.PickedUp] = new[] { OrderStatus.Delivered },
};
public void Transition(OrderStatus next)
{
if (!Allowed.TryGetValue(Status, out var ok) || !ok.Contains(next))
throw new InvalidOperationException($"{Status} -> {next} is not allowed");
Status = next;
_events.Add(new OrderStatusChanged(Id, next, DateTime.UtcNow));
}كل انتقال يُصدر حدث مجال. وهذه الأحداث تقود كل شيء آخر — الإشعارات، تعيين المندوب، التحليلات — دون أن تعرف خدمة الطلبات من يستمع.
نشر الأحداث بموثوقية: نمط Outbox
الكتابة في قاعدة البيانات ثم النشر إلى ناقل الرسائل مشكلة الكتابة المزدوجة الكلاسيكية: إن فشلت الخطوة الثانية فلديك طلب مدفوع لا يوصّله أحد. نمط Outbox التعاملي يحلها بكتابة الحدث في نفس المعاملة مع الطلب.
// Transactional outbox — publish events reliably
await using var tx = await db.Database.BeginTransactionAsync();
db.Orders.Update(order);
db.OutboxMessages.Add(new OutboxMessage("order.status-changed", JsonSerializer.Serialize(evt)));
await db.SaveChangesAsync();
await tx.CommitAsync();
// A background worker (Hangfire / hosted service) drains the outbox to Azure Service Bus or Amazon SQS.خدمات لا نظام أحادي — لكن ليس كثيرة
نقسّم عادة النظام الخلفي للتوصيل إلى خمس خدمات قابلة للنشر: الطلبات، التوزيع (مطابقة المندوبين والمسارات)، الكتالوج (التجار والقوائم)، المدفوعات، والإشعارات. كل منها تملك بياناتها. أي تقسيم أصغر يضيف قفزات شبكية وتكلفة تشغيلية دون فائدة تُذكر لفريق دون العشرين مهندسًا.
مطابقة المكونات مع Azure وAWS
| الحاجة | Microsoft Azure | Amazon Web Services |
|---|---|---|
| الحاويات | Azure Kubernetes Service / Container Apps | Amazon EKS / ECS Fargate |
| ناقل الرسائل | Azure Service Bus | Amazon SQS + SNS |
| قاعدة بيانات علائقية | Azure SQL / Azure Database for MySQL | Amazon RDS / Aurora |
| التخزين المؤقت والمكاني | Azure Cache for Redis | Amazon ElastiCache |
| الملفات (إثبات التسليم) | Blob Storage | Amazon S3 |
| المراقبة | Application Insights | CloudWatch + X-Ray |
كود التطبيق متطابق على كليهما — .NET مع تجريدات فوق الناقل والتخزين — وهذا بالضبط سبب توصيتنا بإبقاء استدعاءات SDK الخاصة بالسحابة عند الأطراف.
امتصاص ذروة الغداء
- توسّع حسب عمق الطابور لا المعالج. عمال التوزيع يتوسعون تلقائيًا من عدد الطلبات غير المعيّنة.
- خزّن الكتالوج مؤقتًا. القوائم تتغير نادرًا وتُقرأ باستمرار؛ Redis بمدد صلاحية قصيرة يزيل 80% من حمل قاعدة البيانات.
- حدّ معدل طلبات جهاز التاجر. مطعم يحدّث كل ثانية أثناء الازدحام هجوم حجب خدمة ذاتي.
- سخّن قبل الذروة. توسّع مجدول عند 11:30 و18:30 أرخص من توسّع تفاعلي عند 12:05.
وقت الوصول المباشر والمسارات
تتدفق مواقع المندوبين عبر مجموعات Redis الجغرافية؛ ويأتي وقت الوصول من Google Maps Platform Routes API ويُخزّن مؤقتًا لكل مقطع مسار. نجمّع الاستدعاءات — طلب واحد لعدة مندوبين مرشحين — فتبقى فاتورة الخرائط متوقعة.
الخلاصة
حالة صريحة، أحداث موثوقة، حفنة من الخدمات محددة الحدود، وتوسّع مقود بالطوابير: هذه وصفة كل منصة توصيل سلّمناها. مزود السحابة أقل أهمية من الانضباط.