معظم rewrites الـevent-driven التي رأيتُها تموت وقد بُني 80% من message bus، و20% من consumers رُسمت هياكلها كرسومات هيكلية (stubs)، ولم يُحَل أيٌّ من المشاكل الصعبة. كنتُ المهندس في اثنتين من تلك الوفيات. كنتُ أيضاً المهندس في تحديث event-driven واحد نجح فعلاً في الشحن، في Bytro، على مدى سنتين. هذا المنشور هو الفرق بين النتيجتين، وهو ليس ما تقوله مخطّطات المعمارية.

الكذبة المريحة عن event-driven

المحادثات والمدوّنات تجعل event-driven يبدو كنمط: اختر message broker، عرّف events، ابعثها من جانب الكتابة، عزّها على جانب القراءة، انتهى. الأدوات في كل مكان. Apache Kafka، NATS، RabbitMQ، SQS — يمكنك إقامة البنية التحتية في عطلة نهاية أسبوع.

لهذا تبدأ rewrites. ولهذا تموت.

الكذبة المريحة هي أن المعمارية هي الجزء الصعب. ليست كذلك. الجزء الصعب هو كل ما تُجبرك المعمارية على جعله صريحاً. النظام المتزامن كان يحتوي على كثير من الافتراضات الضمنية: ترتيب الطلبات، حدود المعاملات، “هذه الكتابة مرئية فوراً لهذه القراءة”، “إن حدث A، فـB مكتمل.” اللحظة التي تفصل فيها الكتابة عن القراءة، تفصل أيضاً كل تلك الافتراضات — وإن لم تُحلّها بتعمّد، نظامك الجديد نسخة أسوأ من القديم لها أيضاً message bus لتشغيله.

ما سار بشكل صحيح في Bytro

في Bytro، حدّثنا backend لعبة متعدّد اللاعبين في الوقت الحقيقي كان تشابك بمرور السنين إلى نظام legacy. انتهجنا event-driven، وشحن، ونجح. إليك لماذا:

1. اخترنا نطاقاً واحداً، لا النظام كاملاً

الإغراء هو القول “نحن ننتهج event-driven كمبدأ معماري.” لم نفعل. اخترنا نقطة ألم محدّدة واحدة — latency الانضمام إلى مباراة — وقلنا “هذا التدفّق سيكون الأول event-driven. الباقي يبقى متزامناً حتى يُثبَت العكس.”

هذا يعني أننا شحننا أول مسار event-driven حقيقي في أشهر، لا سنوات. يعني أيضاً أن عمل الميزات لكل فريق آخر استمرّ في الشحن على النظام القديم. التحديث كان خدمة، لا اضطراباً.

2. شغّلنا shadow لـ11 شهراً

المسار الجديد كان قيد التشغيل في الإنتاج من الأسبوع الرابع — بنسبة 0% من حركة المرور، يُتابع كل طلب من المسار القديم، ينتج نتائج لا تُقدَّم للمستخدمين لكن تُقارَن بالنتائج القديمة كل ليلة.

التناقضات كانت المواصفات. كل “النظام الجديد ينتج جواباً مختلفاً” كان خطأً إما في النظام الجديد، أو القديم، أو فهمنا للنطاق. صحّحنا ما يقارب 200 منها على مدى العام. كل واحدة كانت ستكون حادثة لو قطعنا مبكراً.

3. أبقينا حدود المعاملات صادقة

Event-driven لا يعني “كل شيء eventually consistent والمستخدمون يتحمّلون.” بعض العمليات ما زالت يجب أن تكون ذرّية. إن انضمّ لاعب إلى مباراة، ثلاثة أشياء يجب أن تحدث معاً: حالة اللاعب تُحدَّث، قائمة المباراة تُحدَّث، خطّاف الفوترة يُطلَق. هذه لا يمكن أن تكون ثلاثة events منفصلة مع دعاء بينها.

استخدمنا أنماط transactional outbox لتلك: الكتابة تحدث داخل معاملة DB، الـevent أيضاً يُكتَب (في جدول outbox) داخل نفس المعاملة، وعملية منفصلة تُشحن الـevent إلى الـbus لاحقاً. تحصل على ذرّية حيث تحتاجها، وتسليم غير متزامن حيث لا تحتاجها.

كل محادثة “لا نحتاج معاملات، نحن event-driven” هي كارثة ستة أشهر تنتظر أن تحدث. أبقِ حدود معاملاتك صريحة وصغيرة.

4. صمّمنا events لـconsumers لم نبنها بعد

أصعب قرار في event-driven هو ماذا نضع في event. القليل جداً، وكل consumer يجب أن يُضمّن قاعدة بيانات جانب الكتابة لإثراء الحمولة (مما يهزم الغرض). الكثير جداً، وتُدمج مخطّط اليوم في كود كل consumer ولا تستطيع التطوّر.

القاعدة التي استقررنا عليها: تحمل events الحمولة الأدنى التي تتيح لـconsumer معقول رسم النتيجة المرئية للمستخدم دون جولة إضافية. لا “كل الحقول التي عندها جانب الكتابة.” لا “فقط الـID.” مكان ما في الوسط، والشكل الدقيق كان عملاً خاصاً بالنطاق استغرق أشهراً للوصول إليه.

Events هي عقد عام. عامل مخطّط events بنفس الجدية التي تعامل بها مع REST API. إصدارها. وثّقها. غيّرها بشكل إضافي فقط.

5. عندنا قصة rollback في كل خطوة

الأسبوع الرابع: أطفئ feature flag → عودة إلى 100% المسار القديم. الأسبوع الثاني عشر: أطفئ الـflag → عودة إلى القديم. الأسبوع الأربعين: أطفئ الـflag → عودة إلى القديم. لم يكن الـrollback في أي وقت أكثر من تغيير إعداد واحد، و اختبرنا ذاك الـrollback شهرياً في تدريب حقيقي.

اليوم الذي احتجناه فعلاً — الشهر الثامن، حالة حافّة في ترتيب events — أخذ الـrollback دقيقتين. لأننا تدرّبنا عليه.

ما قتل الإعادتين الأخريين

التحديثان الـevent-driven اللذان شاهدتُهما يموتان كان لديهما معظم البنية التحتية في مكانها. Brokers منشورة، topics معرّفة، serializers محدّدة. ماتا على الأشياء التي لا تحلّها البنية التحتية:

  • لا shadow. ذهبا مباشرة للإنتاج بنسبة صغيرة وأملوا الأفضل.
  • حدود المعاملات مُغطّاة بـ”eventual.” أي: العمليات التي كانت تحتاج أن تكون ذرّية أصبحت race conditions في الإنتاج.
  • Events صُمّمت بلجنة. كل event كان فيه 40 حقلاً لأن كل فريق أراد شيئه بداخله. النتيجة كانت firehose غير قابل للتحليل لم يستطع أحد الاشتراك فيه دون خدمة خاصة به لمعالجته مسبقاً.
  • لا rollback. التحويل كان “اقلب الـflag.” إن لم ينجح، لم ينجح.

كل هذه الإخفاقات الأربعة تنظيمية ومعمارية، لا بنية تحتية. اختيار broker مختلف لا يُصلح أياً منها.

حاشية 2026

معظم عملي OSS الحالي (Fulcrum) هو نفسه event-driven بطريقة محدّدة — تشغيلات الوكلاء تُنتج events تتدفّق عبر bus الذاكرة/المهام/السياق. نفس القواعد تنطبق على النطاق الصغير كما على نطاق Bytro: اختر تدفّقاً واحداً، ظلّله، أبقِ المعاملات صادقة، صمّم events للـconsumers، تدرّب على الـrollback.

النمط مستقل عن النطاق. الانضباط هو ما يتدرّج، لا الـbroker.