لو تطبيقك بيكتب على PostgreSQL وبيبعت رسالة على Kafka في نفس الـ handler، فيه احتمال حقيقي 0.3% إلى 0.5% إن الـ DB transaction يعمل commit والرسالة تضيع في الطريق. على 4 مليون طلب يوميًا، ده 16,000 رسالة ضايعة. كل رسالة منهم بتخلق order يتيم بدون شحن أو فاتورة. Outbox Pattern بـ 40 سطر بيقفل الفجوة دي بضمان رياضي، مش "احتمال أحسن".
المستوى: محترف. المقال بيفترض إن عندك خبرة سابقة بـ PostgreSQL transactions و Kafka producers و event-driven architecture. لو لسه مبتدئ في الـ messaging، ابدأ بمقال "Liveness و Readiness Probes" قبل ما ترجع هنا.
المشكلة باختصار: dual-write بين قاعدتين
تخيّل سيناريو واقعي: عميل بيعمل order. التطبيق محتاج (1) يحفظ الـ order في PostgreSQL، و(2) يبعت رسالة OrderCreated على Kafka علشان نظام الشحن وفواتير الـ accounting يتفعّلوا. الكود الساذج بيشتغل كده:
async def create_order(payload):
async with db.transaction():
order = await db.insert_order(payload)
await kafka.send("orders", OrderCreated(order.id))
return order
المشكلة: الكتابتين دول مش atomic. أربع حالات ممكنة:
- DB ينجح، Kafka ينجح ✓
- DB يفشل، Kafka ميتنفّذش ✓ (الكود وقف بدري)
- DB ينجح، الـ process بيموت قبل Kafka ✗ — order موجود بدون شحن
- DB ينجح، Kafka يرجّع timeout والـ retry بيفشل ✗ — نفس النتيجة
الحالتين الأخيرتين دول الـ "lost messages". على نظام بـ 4M طلب يوميًا و SLA 99.6%، ده 16,000 رسالة ضايعة كل يوم. الـ MTTD (متوسط زمن الاكتشاف) عادةً 4 ساعات، يعني لما العميل يتصل ويسأل فين شحنته.
ليه retry لوحده مش كافي
أول رد فعل لأي مهندس: "نعمل retry على Kafka". الفكرة دي بتفشل لسببين دقيقين:
- لو الـ process مات بعد ما الـ DB commit وقبل ما Kafka send، الـ retry نفسه ضاع مع الـ process. مفيش حد فاكر إنه كان لازم يبعت.
- لو خلّينا الـ retry يحصل قبل الـ commit، ممكن نبعت رسالة عن order اتعمل rollback. حالة أسوأ من الأولى — عندنا "phantom orders" في الـ downstream services.
الحل الصح اسمه Outbox Pattern، وهو موثّق في كتاب Designing Data-Intensive Applications لـ Martin Kleppmann (الفصل 11)، وموقع microservices.io لـ Chris Richardson كجزء من Saga Pattern.
الفكرة بمثال: مكتب البريد قبل الإنترنت
قبل الإنترنت، لو تاجر طلب سلعة من شركة في بلد تاني، كان بيعمل خطوتين: يكتب الطلب في دفتره الداخلي (السجل المحلي = DB)، وياخد نسخة كربون يحطها في صندوق البريد الخاص (Outbox). ساعي البريد بيمر كل ساعتين، يفتح الصندوق، يأخد كل اللي فيه ويوصّله. لو ساعي البريد اتأخر ساعة، مفيش طلب بيضيع — هو بس بيتأخر شوية. ولو الدفتر اتمزّق قبل ما الكربون يتحط، الطلب أصلًا ميتسجلش في الدفتر.