نمط Outbox في PostgreSQL لتكاملات API موثوقة
تعلّم نمط outbox لحفظ الأحداث في PostgreSQL ثم تسليمها لواجهات برمجة طرف ثالث مع إعادة محاولات، ترتيب، وإزالة التكرار.

لماذا تفشل التكاملات حتى لو كان تطبيقك يعمل
من الشائع أن ترى إجراءً "ناجحًا" في تطبيقك بينما يفشل التكامل خلفه بصمت. عملية الكتابة إلى قاعدة البيانات سريعة وموثوقة. استدعاء واجهة برمجة طرف ثالث ليس كذلك. هذا يخلق عالمين مختلفين: نظامك يقول إن التغيير حدث، لكن النظام الخارجي لم يسمع عنه.
مثال نموذجي: يضع عميل طلبًا، يحفظ تطبيقك الطلب في PostgreSQL، ثم يحاول إخطار مزود الشحن. إذا تعطل المزود أو انتهت المهلة بعد 20 ثانية واستسلم طلبك، فالطلب لا يزال موجودًا، لكن الشحنة لم تُنشأ.
المستخدمون يواجهون هذا كسلوك مربك وغير متسق. الأحداث المفقودة تبدو وكأن "لا شيء حدث". الأحداث المكررة تبدو كـ "لماذا تم تحميلي مرتين؟" كما تكافح فرق الدعم لتحديد ما إذا كانت المشكلة من تطبيقك، الشبكة، أو الشريك.
المحاولات (retries) تساعد، لكن وحدها لا تضمن الصحة. إذا أعدت المحاولة بعد مهلة، قد ترسل نفس الحدث مرتين لأنك لا تعرف إن كان الشريك استلم الطلب الأول. إذا أعدت المحاولة بترتيب خاطئ، قد ترسل "تم شحن الطلب" قبل "تم دفع الطلب".
تأتي هذه المشاكل عادة من التزامن الطبيعي: عمال متعددون يعالجون بالتوازي، خوادم تطبيق متعددة تكتب بنفس الوقت، و"قوائم انتظار بتقدير الأفضل" حيث يتغير التوقيت تحت الحمل. أوضاع الفشل متوقعة: الواجهات تنهار أو تبطئ، الشبكات تفقد الطلبات، العمليات تتعطل في لحظة خاطئة، وإعادة المحاولة تولد نسخًا مكررة عندما لا توجد آلية تفرض عدم التكرار.
نمط outbox موجود لأن هذه الفشلات طبيعية.
ما هو نمط outbox ببساطة
نمط outbox بسيط: عندما يجري تطبيقك تغييرًا مهمًا (مثل إنشاء طلب)، يكتب أيضًا سجلًا صغيرًا "حدث لإرساله" في جدول قاعدة البيانات، داخل نفس المعاملة. إذا نجح الالتزام بالمعاملة (commit)، فستعرف أن بيانات العمل وسجل الحدث موجودان معًا.
بعد ذلك، يقرأ عامل منفصل جدول outbox ويسلم تلك الأحداث لواجهات برمجة الطرف الثالث. إذا كانت واجهة بطيئة أو متوقفة أو انتهت مهلة الاتصال، يظل الطلب الرئيسي للمستخدم ناجحًا لأنه لا ينتظر المكالمة الخارجية.
هذا يتجنب الحالات المحرجة عند استدعاء API داخل معالج الطلب:
- تم حفظ الطلب، لكن استدعاء API فشل.
- نجح استدعاء API، لكن تطبيقك تعطل قبل حفظ الطلب.
- يعيد المستخدم المحاولة، فتُرسل نفس البيانات مرتين.
نمط outbox يساعد بشكل أساسي في معالجة الأحداث المفقودة، الفشلات الجزئية (قاعدة البيانات بخير، API الخارجي لا يعمل)، الإرسالات المزدوجة العرضية، وإعادة المحاولات الآمنة (يمكنك المحاولة لاحقًا دون التخمين).
لا يصلح كل شيء. إذا كانت الحمولة (payload) خاطئة، أو قواعد العمل خاطئة، أو رفضت API الطرف الثالث البيانات، فستظل بحاجة إلى تحقق جيد من الصحة، معالجة أخطاء مناسبة، وطريقة لفحص وتصحيح الأحداث الفاشلة.
تصميم جدول outbox في PostgreSQL
جدول outbox الجيد ممل عن قصد. يجب أن يكون سهل الكتابة، سهل القراءة، وصعب الاستخدام الخاطئ.
إليك مخطط أساسي عملي يمكنك تكييفه:
create table outbox_events (
id bigserial primary key,
aggregate_id text not null,
event_type text not null,
payload jsonb not null,
status text not null default 'pending',
created_at timestamptz not null default now(),
available_at timestamptz not null default now(),
attempts int not null default 0,
locked_at timestamptz,
locked_by text,
meta jsonb not null default '{}'::jsonb
);
اختيار معرف
استخدام bigserial (أو bigint) يحافظ على الترتيب بسيطًا والفهارس سريعة. UUIDs ممتازة للتفرد عبر الأنظمة، لكنها لا ترتب بترتيب الإنشاء، مما قد يجعل الاستطلاع أقل توقعًا ويزيد ثقل الفهارس.
حل شائع: احتفظ بـ id كـ bigint للترتيب، وأضف event_uuid منفصلًا إذا كنت بحاجة إلى معرف ثابت للمشاركة بين الخدمات.
الفهارس المهمة
عاملُك سيستعلم بنمط متكرر طوال اليوم. معظم الأنظمة تحتاج:
- فهرس مثل
(status, available_at, id)لجلب الأحداث المعلقة التالية بالترتيب. - فهرس على
(locked_at)إذا كنت تخطط لانتهاء صلاحية الأقفال القديمة. - فهرس مثل
(aggregate_id, id)إذا كنت تقدم أحيانًا لكل aggregate بترتيب.
حافظ على الحمولات صغيرة ومحددة
احتفظ بالحمولات صغيرة ومتوقعة. خزّن ما يحتاجه المستلم فعليًا، لا صفك الكامل. أضف نسخة صريحة للإصدار (مثلاً في meta) حتى يمكنك تطوير الحقول بأمان.
استخدم meta للتوجيه وسياق التصحيح مثل معرف المستأجر، معرف الترابط، trace ID، ومفتاح التمييز. هذا السياق الإضافي مفيد لاحقًا عندما يحتاج الدعم للإجابة على "ماذا حدث لهذا الطلب؟".
كيفية تخزين الأحداث بأمان مع كتابة العمل
القاعدة الأهم بسيطة: اكتب بيانات العمل وصف حدث outbox في نفس معاملة قاعدة البيانات. إذا تم الالتزام، فالكلاهما موجود. إذا تم التراجع، فلا شيء منهما موجود.
مثال: يضع عميل طلبًا. في معاملة واحدة تدخل صفّ الطلب، صفوف عناصر الطلب، وصف outbox واحد مثل order.created. إذا فشل أي خطوة، فلن تريد حدث "created" يهرب إلى العالم.
حدث واحد أم عدة؟
ابدأ بحدث واحد لكل فعل عمل عندما تستطيع. هذا أسهل للفهم وأرخص للمعالجة. قسم إلى أحداث متعددة فقط عندما تحتاج المستهلكات المختلفة توقيتًا أو حمولة مختلفة حقًا (مثل order.created للتجهيز وpayment.requested للفوترة). إنشاء العديد من الأحداث لنفس النقرة يزيد من المحاولات ومشاكل الترتيب والتعامل مع التكرار.
أي بيانات تحفظ في الحِمل؟
عادة تختار بين:
- Snapshot: حفظ الحقول الأساسية كما كانت وقت الحدث (إجمالي الطلب، العملة، معرف العميل). هذا يتجنب قراءات إضافية لاحقًا ويحافظ على ثبات الرسالة.
- Reference ID: تخزين معرف الطلب فقط ويجعل العامل يحمل التفاصيل لاحقًا. هذا يبقي outbox صغيرًا لكنه يضيف قراءات وقد يتغير إذا تم تعديل الطلب.
الوسيلة العملية: معرفات بالإضافة إلى لقطة صغيرة من القيم الحرجة. يساعد المستلمين على التصرف بسرعة ويسهل التصحيح.
احفظ حدود المعاملة ضيقة. لا تستدع APIs خارجية داخل نفس المعاملة.
تسليم الأحداث لواجهات الطرف الثالث: حلقة العامل
بمجرد أن تكون الأحداث في outbox، تحتاج إلى عامل يقرأها وينادي API الطرف الثالث. هذا الجزء يحول النمط إلى تكامل موثوق.
الاستطلاع عادةً أبسط خيار. LISTEN/NOTIFY يمكن أن يقلل الكمون، لكنه يضيف عناصر متحركة ويحتاج احتياطًا عندما تُفقد الإشعارات أو يعاد تشغيل العامل. لمعظم الفرق، الاستطلاع المستمر مع دفعة صغيرة أسهل في التشغيل والتتبع.
كيف تَطالب الصفوف بأمان
يجب أن يطالب العامل بالصفوف حتى لا يعالج عاملان نفس الحدث في نفس الوقت. في PostgreSQL، النهج الشائع هو اختيار دفعة باستخدام أقفال الصفوف وSKIP LOCKED، ثم وضع علامة عليها بأنها قيد المعالجة.
تدفق حالات عملي مثالياً:
pending: جاهز للإرسالprocessing: مقفول بواسطة عامل (استخدمlocked_byوlocked_at)sent: تم التسليم بنجاحfailed: توقف بعد الحد الأقصى للمحاولات (أو نقل للمراجعة اليدوية)
اجعل الدُفعات صغيرة لتكون لطيفًا مع قاعدة البيانات. دفعة من 10 إلى 100 صف، تعمل كل 1 إلى 5 ثوانٍ، هي نقطة بداية شائعة.
عندما ينجح الاتصال، عين الصف sent. عندما يفشل، زد attempts، اضبط available_at لوقت مستقبلي (backoff)، امسح القفل، وأعده إلى pending.
سجلات مفيدة (دون تسريب أسرار)
سجلات جيدة تجعل الفشل قابلاً للمعالجة. سجّل id الخاص بالـ outbox، نوع الحدث، اسم الوجهة، عد المحاولات، الزمن، وحالة HTTP أو فئة الخطأ. تجنب أجسام الطلبات، رؤوس المصادقة، والاستجابات الكاملة. إذا احتجت ترابطًا، خزّن معرف طلب آمن أو تجزئة بدلًا من البيانات الخام.
قواعد الترتيب التي تعمل في الأنظمة الحقيقية
يبدأ كثيرون بـ "أرسل الأحداث بنفس الترتيب الذي أنشأناها فيه". المشكلة أن "نفس الترتيب" نادرًا ما يكون عالميًا. إذا فرضت قائمة انتظار عالمية واحدة، فإن عميلًا بطيئًا أو API متقلب يمكن أن يوقف الجميع.
قاعدة عملية: حافظ على الترتيب لكل مجموعة، لا للنظام كله. اختر مفتاح تجميع يتطابق مع كيفية تفكير العالم الخارجي ببياناتك، مثل customer_id, account_id، أو aggregate_id مثل order_id. ثم ضمن الترتيب داخل كل مجموعة بينما تُسلم مجموعات متعددة بالتوازي.
عمال متوازيون دون كسر الترتيب
شغّل عمالًا متعددين، لكن تأكد ألا يعالج عاملان نفس المجموعة في نفس الوقت. النهج المعتاد هو دائمًا تسليم أقدم حدث غير مرسل لمجموعة معينة والسماح بالتوازي عبر مجموعات مختلفة.
اجعل قواعد المطالَبة بسيطة:
- سلّم فقط أقدم حدث معلق لكل مجموعة.
- اسمح بالتوازي عبر المجموعات، لا داخل المجموعة.
- اطلب حدثًا واحدًا، أرسله، حدّث الحالة، ثم تابع.
عندما يعيق حدث واحد الباقي
في وقت ما، سيظهر حدث «سام» يفشل لساعات (حمولة خاطئة، رمز مرفوض، تعطل المزود). إذا طبقت ترتيبًا صارمًا داخل المجموعة، يجب أن تنتظر الأحداث اللاحقة لتلك المجموعة، لكن المجموعات الأخرى يجب أن تستمر.
حل عملي: وضع حد لعدد المحاولات لكل حدث. بعد ذلك، عيّنه failed وأوقف فقط تلك المجموعة حتى يصلح شخص سبب المشكلة. هذا يحافظ على بقية النظام دون تباطؤ.
إعادة المحاولات دون تفاقم المشكلة
إعادة المحاولات هي المكان الذي يصبح فيه إعداد outbox جيدًا إما موثوقًا أو مزعجًا. الهدف: حاول مرة أخرى عندما يكون احتمال النجاح أكبر، وتوقف سريعًا عندما لا يكون كذلك.
استخدم تراجعًا أُسّيًا وحدًا صارمًا. مثال: دقيقة، دقيقتان، 4 دقائق، 8 دقائق، ثم توقف (أو استمر مع حد أقصى مثل 15 دقيقة). دائمًا عيّن حدًا أقصى لعدد المحاولات حتى لا يملأ حدث واحد النظام إلى الأبد.
لا يجب إعادة المحاولة لكل فشل. ضع قواعد واضحة:
- أعد المحاولة: مهلات الشبكة، إعادة تعيين الاتصال، مشاكل DNS، وHTTP 429 أو 5xx.
- لا تعيد المحاولة: HTTP 400 (طلب سيئ)، 401/403 (مشاكل مصادقة)، 404 (نقطة نهاية خاطئة)، أو أخطاء تحقق يمكنك اكتشافها قبل الإرسال.
خزن حالة إعادة المحاولة في صف outbox. زد attempts، اضبط available_at للمحاولة التالية، وسجل ملخصًا آمنًا وقصيرًا للخطأ (كود الحالة، فئة الخطأ، رسالة مقصوصة). لا تخزن الحمولات الكاملة أو البيانات الحساسة في حقول الخطأ.
قيود المعدل تحتاج تعاملًا خاصًا. إذا تلقيت HTTP 429، احترم Retry-After عندما يكون موجودًا. وإلا، تراجع بشكل أكثر عدوانية لتجنب موجة إعادة محاولات.
أساسيات عدم التكرار (deduplication) وعدم التغيير بالخطأ (idempotency)
إذا بنيت تكاملات موثوقة، افترض أن نفس الحدث قد يُرسل مرتين. قد ينهار عامل بعد المكالمة HTTP وقبل تسجيل النجاح. قد تُخفي المهلة نجاحًا. قد تتداخل محاولة إعادة مع محاولة بطيئة أولى. نمط outbox يقلل فقدان الأحداث، لكنه لا يمنع التكرارات بمفرده.
النهج الأكثر أمانًا هو idempotency: عمليات التسليم المتكررة تعطي نفس النتيجة كأنها تسليم واحد. عند استدعاء API طرف ثالث، أدرج مفتاح idempotency ثابت لذلك الحدث وتلك الوجهة. تدعم العديد من APIs رأسًا لهذا؛ إن لم يكن، ضع المفتاح في جسم الطلب.
مفتاح بسيط هو الوجهة زائد معرف الحدث. لحدث بمعرف evt_123 استخدم دائمًا شيئًا مثل destA:evt_123.
على جانبك، امنع الإرسالات المكررة بالاحتفاظ بسجل توصيل خارجي وفرض قيد فريد مثل (destination, event_id). حتى لو تسابق عاملان، لن يستطيع إلا واحد إنشاء سجل "نحن نرسل هذا".
الويب هوكس قد تكرر أيضًا
عند استلام ردود webhook (مثل "تم التأكيد" أو "تحديث الحالة") تعامل معها بنفس الطريقة. المزودون يعيدون المحاولة، وقد ترى نفس الحمولة عدة مرات. خزّن معرفات الويب هوك المعالجة، أو احسب تجزئة ثابتة من معرف رسالة المزود ورفض التكرارات.
كم مدة الاحتفاظ بالبيانات
احتفظ بصفوف outbox حتى تسجل النجاح (أو فشل نهائي تقبله). احتفظ بسجلات التوصيل لفترة أطول لأنها سجل التدقيق عندما يسأل أحدهم: "هل أرسلناها؟".
نهج شائع:
- صفوف outbox: احذفها أو أرشفها بعد النجاح زائد نافذة أمان قصيرة (أيام).
- سجلات التوصيل: احتفظ بها لأسابيع أو شهور، بناءً على الامتثال واحتياجات الدعم.
- مفاتيح idempotency: احتفظ بها على الأقل طالما يمكن أن تحدث إعادة محاولات (ولمدة أطول لتكرارات الويب هوك).
خطوة بخطوة: تطبيق نمط outbox
قرر ما ستنشره. احفظ الأحداث صغيرة، مركّزة، وسهلة الإعادة لاحقًا. قاعدة جيدة: حقيقة عمل واحدة لكل حدث، مع بيانات كافية ليتصرف المستلم.
بناء الأساس
اختر أسماء أحداث واضحة (مثل order.created, order.paid) وأصدر نسخة مخطط الحمولة (مثل v1, v2). الترحيل بالإصدار يتيح إضافة حقول لاحقًا دون كسر المستهلكين الأقدم.
أنشئ جدول outbox في PostgreSQL وأضف فهارس للاستعلامات التي سيجريها العامل عادة، خصوصًا (status, available_at, id).
حدّث مسار الكتابة بحيث يحدث إدخال outbox جنبًا إلى جنب مع تغيير البيانات الرئيسي في نفس معاملة قاعدة البيانات. هذه هي الضمانة الأساسية.
أضف التسليم والضوابط
خطة تنفيذ بسيطة:
- عرّف أنواع الأحداث وإصدارات الحمولة التي ستدعمها طويلًا.
- أنشئ جدول outbox والفهارس.
- أدخل صف outbox جنبًا إلى جنب مع التغيير الرئيسي للبيانات.
- ابنِ عاملًا يطالب الصفوف، يرسل إلى API الطرف الثالث، ثم يحدث الحالة.
- أضف جدولة إعادة المحاولة مع backoff وحالة
failedعندما تنتهي المحاولات.
أضف مقاييس أساسية لتلاحظ المشكلات مبكرًا: التأخر (عمر أقدم حدث غير مرسل)، معدل الإرسال، ومعدل الفشل.
مثال بسيط: إرسال أحداث الطلب إلى خدمات خارجية
يضع عميل طلبًا في تطبيقك. هناك شيئان يجب أن يحدثا خارج نظامك: مزود الفوترة يجب أن يخصم البطاقة، ومزود الشحن يجب أن ينشئ شحنة.
مع نمط outbox، لا تستدعي تلك الواجهات داخل طلب إتمام الشراء. بدلاً من ذلك تحفظ الطلب وصف outbox في نفس معاملة PostgreSQL، حتى لا تنتهي في حالة "تم حفظ الطلب لكن لا إخطار" (أو العكس).
صف outbox نموذجي لحدث طلب قد يتضمن aggregate_id (معرف الطلب)، event_type مثل order.created، وحمولة JSONB مع الإجماليات، البنود، وتفاصيل الوجهة.
يأخذ عامل الدفعات المعلقة ثم ينادي الخدمات الخارجية (إما بترتيب معين أو عن طريق إصدار أحداث منفصلة مثل payment.requested و shipment.requested). إذا كان مزود واحد متعطلًا، يسجل العامل المحاولة، يحدد المحاولة التالية بدفع available_at إلى المستقبل، ويواصل. الطلب يبقى موجودًا، وسيُعاد المحاولة لاحقًا دون منع عمليات الخروج الجديدة.
عادةً ما يكون الترتيب "لكل طلب" أو "لكل عميل". اجعل الأحداث بنفس aggregate_id تُعالج واحدة تلو الأخرى حتى لا يصل order.paid قبل order.created.
التمييز (deduplication) يمنع تحصيل المبلغ مرتين أو إنشاء شحنتين. أرسل مفتاح idempotency عندما يدعم الطرف الثالث ذلك، واحتفظ بسجل توصيل لكل وجهة حتى لا يؤدي إعادة المحاولة بعد مهلة إلى إجراء ثانٍ.
فحوصات سريعة قبل الإطلاق
قبل أن تثق بتكامل ما في نقل الأموال، إبلاغ العملاء، أو مزامنة البيانات، اختبر الحواف: الانهيارات، إعادة المحاولات، التكرارات، والعمال المتعددين.
فحوصات تلتقط الفشلات الشائعة:
- أكد أن صف outbox يُنشأ في نفس معاملة تغيير العمل.
- تحقق أن المُرسل آمن للتشغيل على عدة مثيلات. يجب ألا يرسل عاملان نفس الحدث في نفس الوقت.
- إذا كان الترتيب مهمًا، عرّف القاعدة في جملة واحدة وفرضها بمفتاح ثابت.
- لكل وجهة، قرر كيف تمنع التكرار وكيف تثبت "أننا أرسلناها".
- حدد الخروج: بعد N محاولات، انقل الحدث إلى
failed، احتفظ بآخر ملخص خطأ، ووفّر إجراءً بسيطًا لإعادة المعالجة.
فحص واقعي: قد يقبل Stripe الطلب لكن ينهار العامل قبل حفظ النجاح. بدون idempotency، قد تسبب إعادة المحاولة إجراءً مزدوجًا. مع idempotency وسجل توصيل محفوظ، تصبح إعادة المحاولة آمنة.
الخطوات التالية: نشر هذا دون تعطيل تطبيقك
الطرح هو الموضع الذي ينجح أو يتعثر فيه معظم مشاريع outbox. ابدأ صغيرًا لترى السلوك الحقيقي دون تعريض كامل طبقة التكامل للمخاطر.
ابدأ بتكامل واحد ونوع حدث واحد. مثلاً، أرسل فقط order.created إلى واجهة بائع واحدة بينما يبقى كل شيء آخر كما هو. هذا يمنحك خط أساس واضح للقياس من حيث الإنتاجية، الكمون، ومعدلات الفشل.
اجعل المشاكل مرئية مبكرًا. أضف لوحات تحكم وإنذارات لتأخر outbox (كم عدد الأحداث التي تنتظر، وما عمر أقدمها) ومعدل الفشل (كم منها عالق في إعادة المحاولة).
ضع خطة آمنة لإعادة المعالجة قبل أول حادث. قرر ماذا يعني "إعادة المعالجة": إعادة إرسال نفس الحمولة، إعادة بناء الحمولة من البيانات الحالية، أو إرسالها للمراجعة اليدوية. وثق الحالات الآمنة لإعادة الإرسال وتلك التي تحتاج تدخل بشري.
إذا كنت تبني هذا بمنصة بدون كود مثل AppMaster (appmaster.io)، فإن الهيكل نفسه ينطبق: اكتب بيانات العمل وصف outbox معًا في PostgreSQL، ثم شغّل عملية خلفية منفصلة لتسليم، إعادة المحاولة، ووضع علامات على الأحداث كـ sent أو failed.
الأسئلة الشائعة
استخدم نمط outbox عندما يحدث تغيير في قاعدة بياناتك نتيجة فعل مستخدم ويجب أن يُشغّل عملاً في نظام خارجي آخر. يكون مفيدًا خاصةً عندما تؤدي مهلات الاتصال أو الشبكات غير المستقرة أو أعطال الطرف الثالث إلى حالات «تم الحفظ لدينا لكن لم يتم لدى الطرف الآخر».
كتابة صف العمل والصف الخاص بـ outbox في نفس معاملة قاعدة البيانات تمنحك ضمانًا واضحًا: إما أن كلاهما موجودان أو لا شيء منهما موجود. هذا يمنع حالات الفشل الجزئي مثل "استدعاء API نجح لكن الطلب لم يُحفَظ" أو "تم حفظ الطلب لكن استدعاء API لم يحدث".
إعداد افتراضي عملي يتضمن الحقول id, aggregate_id, event_type, payload, status, created_at, available_at, attempts، بالإضافة إلى حقول القفل مثل locked_at وlocked_by. هذه الحقول تجعل الإرسال، جدولة إعادة المحاولة، والتزامن آمنًا دون تعقيد زائد.
قاعدة جيدة هي وجود فهرس على (status, available_at, id) لتمكين العامل من جلب الدفعة التالية من الأحداث القابلة للإرسال بالترتيب بسرعة. أضف فهارس أخرى فقط إذا كنت تستعلم فعلاً بتلك الحقول لأن الفهارس الإضافية تبطئ الإدخالات.
المسح الدوري (polling) هو الأبسط والأكثر قابلية للتنبؤ لمعظم الفرق. ابدأ بدفعات صغيرة وفاصل زمني قصير، ثم ظبط بناءً على الحمل والتأخر؛ يمكنك إضافة تحسينات لاحقًا، لكن حلقة بسيطة أسهل للتتبع عند حدوث مشاكل.
اطلب الصفوف مستخدمًا أقفال مستوى الصف حتى لا يعالج عاملان نفس الحدث في نفس الوقت، عادةً باستخدام SKIP LOCKED. ثم عين الصف كـ processing مع طابع زمني واسم العامل، أرسله، وأخيرًا عيّنه sent أو أعده إلى pending مع available_at مستقبلية.
استخدم تراجعًا أُسّيًا (exponential backoff) مع حد أقصى لعدد المحاولات، وأعد المحاولة فقط للفشل الذي يحتمل أن يكون مؤقتًا. مهلات الاتصال، أخطاء الشبكة، وHTTP 429/5xx هي مرشحة جيدة لإعادة المحاولة؛ أخطاء التحقق من الصحة ومعظم استجابات 4xx تعامل كأخطاء نهائية حتى تصحح البيانات أو الإعداد.
تفترض وجود تكرارات محتملة، خصوصًا إذا تعطل العامل بعد الاتصال HTTP وقبل تسجيل النجاح. استخدم مفتاح عدم التكرار (idempotency key) ثابتًا لكل حدث ولهذا الوجهة، واحتفظ بسجل توصيل خارجي مع قيد فريد مثل (destination, event_id) حتى لا تسمح التنافسات بإنشاء إرسالين.
حافظ على الترتيب داخل مجموعة وليست على مستوى النظام كله. استخدم مفتاح تجميع مثل aggregate_id أو customer_id، عالج حدثًا واحدًا في كل مرة داخل المجموعة، واسمح بالتوازي عبر مجموعات مختلفة حتى لا يوقف عميل واحد الباقي.
بعد عدد محاولات أقصى، عيّنه failed، احتفظ بملخص خطأ قصير وآمن، وأوقف معالجة الأحداث التالية لنفس المجموعة حتى يصلح شخص سبب الخطأ. هذا يقلل تأثير الحدث الخاطئ ويمنع تكرار المحاولات بلا نهاية بينما تستمر المجموعات الأخرى.


