خدماتي كانت مقيدة ببعضها البعض: كيف أنقذتني ‘المعمارية القائمة على الأحداث’ (EDA) من جحيم التشابك الخانق؟

يا جماعة الخير، السلام عليكم ورحمة الله. معكم أخوكم أبو عمر.

بتذكر قبل عدة سنوات، كنت شغال على مشروع متجر إلكتروني لأحد الزبائن. في البداية، كان المشروع بسيطاً وجميلاً، “زي الليرة الذهب”. خدمة للمنتجات، خدمة للطلبات، خدمة للمستخدمين. الأمور كانت ماشية تمام التمام. لكن مع الوقت، بدأ المشروع يكبر والطلبات تزيد. طلب مني الزبون إضافة ميزة إرسال إشعار عبر الإيميل والرسائل القصيرة عند إنشاء طلب جديد، وتحديث المخزون، وإبلاغ قسم الشحن، وإضافة نقاط ولاء للمستخدم.

قلت في نفسي “بسيطة”. ورحت على خدمة الطلبات (Orders Service) وبديت أبرمج. عند إنشاء طلب جديد، الكود صار يعمل التالي بالترتيب:

  1. يتصل بخدمة الدفع (Payment Service) للتأكد من عملية الدفع.
  2. إذا نجح الدفع، يتصل بخدمة المخزون (Inventory Service) لخصم الكمية.
  3. إذا نجح خصم الكمية، يتصل بخدمة الإشعارات (Notification Service) لإرسال إيميل ورسالة قصيرة.
  4. إذا نجحت الإشعارات، يتصل بخدمة الشحن (Shipping Service) لتجهيز الطلب.
  5. وأخيراً، يتصل بخدمة الولاء (Loyalty Service) لإضافة النقاط.

في يوم من الأيام، وفي عز ضغط الطلبات، توقفت خدمة الإشعارات عن العمل بسبب مشكلة في مزود خدمة الرسائل القصيرة. والنتيجة؟ كل عملية إنشاء طلب جديد صارت تفشل! المستخدم يضع الطلب، يدفع، ثم تظهر له رسالة خطأ. أنا قعدت قدام الشاشة، صافن وبحكي لحالي “يا زلمة، شو دخل خدمة الإشعارات في عملية الدفع والمخزون؟ ليش فشل إرسال رسالة يوقفلي كل البيزنس؟”. الوضع كان “كرباج” بمعنى الكلمة، وكل خدمة صارت مربوطة بالثانية زي خيوط العنكبوت. أي تعديل في خدمة كان يتطلب تعديلات في خدمات أخرى. كنت عايش في جحيم اسمه “التشابك الخانق” (Tight Coupling).

هنا كانت بداية رحلتي مع منقذتي: المعمارية القائمة على الأحداث (Event-Driven Architecture).

ما هو التشابك اللعين (Tight Coupling)؟

قبل ما نحكي عن الحل، خلينا نفهم أصل المشكلة. التشابك الشديد أو الاقتران المحكم (Tight Coupling) في البرمجيات يعني أن مكونات النظام (الخدمات، الكلاسات، الوحدات) تعتمد على بعضها البعض بشكل كبير ومباشر.

تخيلها كأنها مجموعة من الأشخاص يقفون في صف طويل لتمرير دلاء الماء لإطفاء حريق. كل شخص يعتمد بشكل مباشر على الشخص الذي قبله. إذا توقف شخص واحد في هذا الصف لأي سبب (تعب، انزلق، أو حتى راح يشرب قهوة)، فإن عملية نقل الماء كلها تتوقف. هذا بالضبط ما كان يحدث في نظامي: خدمة الإشعارات توقفت، فتوقفت عملية إنشاء الطلبات بأكملها.

أعراض هذا المرض الخبيث:

  • تأثير الدومينو (Domino Effect): فشل في خدمة واحدة غير حرجة يمكن أن يؤدي إلى انهيار خدمات أخرى حرجة.
  • صعوبة التعديل والتطوير: أي تغيير في خدمة (مثلاً، تغيير طريقة استدعاء دالة في خدمة الإشعارات) يتطلب منك الذهاب وتعديل الكود في كل الخدمات التي تستدعيها.
  • صعوبة التوسع (Scaling): إذا كانت خدمة الطلبات بطيئة بسبب الضغط، لكن خدمة الإشعارات سريعة، لا يمكنك توسيع خدمة الطلبات لوحدها بسهولة لأنها تنتظر رداً مباشراً من الخدمات الأخرى.
  • صعوبة الاختبار: لاختبار خدمة واحدة، تحتاج إلى تشغيل كل الخدمات التي تعتمد عليها.

المعمارية القائمة على الأحداث (EDA): طوق النجاة

المعمارية القائمة على الأحداث (Event-Driven Architecture أو EDA) هي نمط معماري يروج لإنتاج واكتشاف واستهلاك “الأحداث” (Events). ببساطة، بدل ما الخدمات تتصل ببعضها البعض بشكل مباشر، صارت تتواصل بشكل غير مباشر.

الفكرة بسيطة وعبقرية: عندما يحدث شيء مهم في خدمة ما (مثل إنشاء طلب جديد)، هذه الخدمة لا تقوم باستدعاء الخدمات الأخرى. بدلاً من ذلك، تقوم بنشر “حدث” (Event) يصف ما حدث للتو، وتلقيه في قناة مركزية (مثل وسيط رسائل). ثم، أي خدمة أخرى مهتمة بهذا النوع من الأحداث يمكنها “الاستماع” إلى هذه القناة، وعندما يصل الحدث، تقوم بتنفيذ مهمتها.

تخيلها مثل محطة راديو. المذيع (الخدمة المنتجة للحدث) يرسل البث عبر الأثير. هو لا يعرف من يستمع، ولا يهمه. المستمعون (الخدمات المستهلكة للحدث) يمكنهم تشغيل الراديو والاستماع متى أرادوا. إذا تعطل راديو أحد المستمعين، فلن يتوقف البث عن الآخرين.

المكونات الأساسية للـ EDA

لنستعرض المكونات الرئيسية لهذا العالم الجميل:

  • المنتج (Producer): هو التطبيق أو الخدمة التي تنشئ الحدث وتنشره. في قصتنا، “خدمة الطلبات” هي المنتج.
  • الحدث (Event): هو رسالة تحتوي على بيانات حول ما حدث. إنها حقيقة غير قابلة للتغيير حدثت في الماضي. مثلاً، حدث اسمه OrderCreated يحتوي على رقم الطلب، قيمة الطلب، ومعلومات المستخدم.
  • وسيط الأحداث (Event Broker): هو “ساعي البريد” أو “محطة الراديو”. قناة مركزية تستقبل الأحداث من المنتجين وتوجهها إلى المستهلكين المهتمين. أشهر الأمثلة: RabbitMQ, Apache Kafka, AWS SQS.
  • المستهلك (Consumer): هو التطبيق أو الخدمة التي تشترك في نوع معين من الأحداث وتستمع له. عندما تستلم الحدث، تقوم بتنفيذ منطق معين. في قصتنا، خدمات “الإشعارات”، “المخزون”، “الشحن”، و”الولاء” كلها أصبحت مستهلكين.

تطبيق عملي: من التشابك إلى فك الارتباط

طيب يا أبو عمر، حكي نظري جميل، ورّينا كيف طبقت هالحكي على أرض الواقع.

لنقارن بين السيناريو القديم والجديد.

السيناريو القديم (الاقتران المحكم – Synchronous)

كان الكود داخل خدمة الطلبات (Order Service) يشبه هذا (كود توضيحي بلغة تشبه JavaScript):


// داخل OrderService.js
async function createOrder(orderData) {
    try {
        // 1. الاتصال المباشر بخدمة الدفع
        const paymentResult = await paymentService.processPayment(orderData.payment);
        if (!paymentResult.success) throw new Error("Payment failed");

        // 2. الاتصال المباشر بخدمة المخزون
        const inventoryResult = await inventoryService.decreaseStock(orderData.items);
        if (!inventoryResult.success) throw new Error("Inventory update failed");

        // 3. الاتصال المباشر بخدمة الإشعارات
        const notificationResult = await notificationService.sendOrderConfirmation(orderData.user);
        // ها هي الكارثة! إذا فشل هذا، كل ما سبق قد يحتاج إلى إلغاء (rollback)
        if (!notificationResult.success) throw new Error("Notification failed");

        // ... وهكذا مع باقي الخدمات

        return { success: true, message: "Order created successfully!" };

    } catch (error) {
        // هنا تكمن المشكلة: أي فشل يوقف كل شيء
        console.error("Failed to create order:", error.message);
        // يجب هنا تطبيق منطق معقد لإلغاء العمليات السابقة (e.g., refund payment)
        return { success: false, message: error.message };
    }
}

المشكلة واضحة. العملية طويلة، ومتسلسلة، وأي خطأ يكسر السلسلة كلها.

السيناريو الجديد (فك الارتباط – Asynchronous with EDA)

مع المعمارية القائمة على الأحداث، تغير المنطق تماماً. أصبحت مسؤولية خدمة الطلبات أقل بكثير.

الخطوة 1: خدمة الطلبات (المنتج)

الآن، خدمة الطلبات تقوم فقط بالتحقق من صحة الطلب، حفظه في قاعدة بياناتها الخاصة، ثم تنشر حدثاً اسمه OrderCreated. وانتهت مهمتها! لا تعرف شيئاً عن المخزون أو الإشعارات.

مثال باستخدام Node.js ومكتبة amqplib للتعامل مع RabbitMQ:


// داخل OrderService.js
const amqp = require('amqplib');

async function createOrder(orderData) {
    // 1. التحقق من صحة الطلب وحفظه في قاعدة البيانات
    const newOrder = await db.orders.create(orderData);

    // 2. الاتصال بوسيط الرسائل (RabbitMQ)
    const connection = await amqp.connect('amqp://localhost');
    const channel = await connection.createChannel();
    const exchange = 'orders_exchange';

    // 3. إنشاء الحدث (رسالة)
    const event = {
        type: 'OrderCreated',
        payload: {
            orderId: newOrder.id,
            userId: newOrder.userId,
            items: newOrder.items,
            totalAmount: newOrder.totalAmount
        }
    };
    const msg = JSON.stringify(event);

    // 4. نشر الحدث في الـ exchange
    await channel.assertExchange(exchange, 'fanout', { durable: false });
    channel.publish(exchange, '', Buffer.from(msg));
    
    console.log(`[OrderService] Event published: ${msg}`);

    await channel.close();
    await connection.close();

    // 5. إعادة رد فوري للمستخدم
    return { success: true, message: "Your order is being processed!" };
}

الخطوة 2: الخدمات الأخرى (المستهلكون)

كل خدمة أخرى (الإشعارات، المخزون، الشحن) الآن تشترك في orders_exchange وتستمع لوصول الأحداث. كل واحدة تعمل بشكل مستقل.

مثال لكود خدمة الإشعارات (Notification Service):


// داخل NotificationService.js
const amqp = require('amqplib');

async function listenForOrderEvents() {
    const connection = await amqp.connect('amqp://localhost');
    const channel = await connection.createChannel();
    const exchange = 'orders_exchange';

    await channel.assertExchange(exchange, 'fanout', { durable: false });
    
    // إنشاء queue خاصة بهذه الخدمة
    const q = await channel.assertQueue('', { exclusive: true });
    console.log(`[NotificationService] Waiting for events in queue: ${q.queue}`);

    // ربط الـ queue بالـ exchange
    channel.bindQueue(q.queue, exchange, '');

    // بدء الاستهلاك
    channel.consume(q.queue, (msg) => {
        if (msg.content) {
            const event = JSON.parse(msg.content.toString());

            // التأكد من أن الحدث هو الذي نهتم به
            if (event.type === 'OrderCreated') {
                console.log(`[NotificationService] Received OrderCreated event for order ID: ${event.payload.orderId}`);
                // هنا نكتب منطق إرسال الإيميل والرسالة القصيرة
                sendEmail(event.payload.userId, "Your order has been confirmed!");
                sendSMS(event.payload.userId, "Your order is confirmed!");
            }
        }
    }, { noAck: true }); // noAck: true للتسهيل، في الواقع يجب استخدام acks
}

listenForOrderEvents();

بهذه الطريقة، إذا فشلت خدمة الإشعارات، فهذا لا يؤثر إطلاقاً على خدمة المخزون أو الشحن. الحدث يبقى في الـ queue (إذا تم تصميمها بشكل صحيح)، ويمكن لخدمة الإشعارات معالجته لاحقاً عند عودتها للعمل. انحلت القصة وصار النظام مرناً وقادراً على تحمل الأخطاء.

نصائح من خبرة أبو عمر

التحول إلى EDA ليس مجرد تغيير في الكود، بل هو تغيير في طريقة التفكير. وهذه شوية نصائح من القلب اكتسبتها من التجربة:

1. صمم “عقد الحدث” (Event Contract) بعناية

شكل الحدث (الـ Event Schema) هو عقد بين المنتج والمستهلك. لا تستهينوا بتصميمه. أي تغيير فيه قد يكسر كل الخدمات المستهلكة. استخدموا أدوات لفرض هذا العقد مثل JSON Schema أو Apache Avro. وخططوا لإصدارات مختلفة من الحدث (Versioning) منذ اليوم الأول.

2. صمم المستهلك ليكون “Idempotent”

هاي الشغلة مش مزحة! في عالم الأنظمة الموزعة، هناك احتمال (ولو ضئيل) أن يستلم المستهلك نفس الحدث مرتين. يجب أن يكون الكود الخاص بك قادراً على التعامل مع هذا الموقف. مثلاً، لو وصل حدث ChargeCustomer مرتين، يجب ألا تخصم المبلغ من العميل مرتين! الطريقة الشائعة هي حفظ معرف كل حدث تمت معالجته في قاعدة بيانات، والتحقق منه قبل تنفيذ أي عملية.

3. لا تهمل المراقبة والتتبع (Monitoring & Observability)

عندما تصبح العمليات غير متزامنة، يصبح تتبع مسار طلب واحد عبر النظام أصعب. كيف تعرف أن الطلب رقم 123 قد تمت معالجته بنجاح في جميع الخدمات؟ استخدم “معرف الارتباط” (Correlation ID). قم بإنشاء ID فريد في بداية العملية (مثلاً، في خدمة الطلبات) ومرره داخل كل حدث. بهذه الطريقة، يمكنك البحث في سجلات (logs) كل الخدمات عن هذا الـ ID لترى مسار العملية كاملاً. أدوات مثل Jaeger أو Zipkin ضرورية هنا.

4. اختر الوسيط المناسب لمهمتك

ليس كل وسطاء الرسائل متشابهين:

  • RabbitMQ: ممتاز للـ routing المعقد. يمكنك توجيه الرسائل بناءً على قواعد معقدة. جيد للمهام التقليدية.
  • Apache Kafka: مصمم للتعامل مع كميات هائلة من البيانات (high-throughput). يعمل كسجل دائم للأحداث (log)، مما يسمح للمستهلكين بإعادة قراءة الأحداث من أي نقطة في الماضي. ممتاز لتحليل البيانات والـ event sourcing.
  • AWS SQS / Google Pub/Sub: حلول سحابية مُدارة، بسيطة وسهلة الاستخدام. خيار رائع إذا كنت لا تريد إدارة البنية التحتية بنفسك.

الخلاصة: الحرية تأتي مع المسؤولية 🕊️

المعمارية القائمة على الأحداث منحتني وفريقي “حرية” لم نكن نحلم بها. حرية تطوير ونشر كل خدمة على حدة، حرية التوسع بشكل مستقل، والأهم من ذلك، حرية النوم ليلاً دون الخوف من أن يؤدي فشل خدمة صغيرة إلى انهيار النظام بأكمله.

لكن هذه الحرية تأتي مع مسؤولية. EDA تقدم تعقيدات جديدة: إدارة وسيط الرسائل، التعامل مع العمليات غير المتزامنة، وضمان اتساق البيانات في النهاية (Eventual Consistency). هي ليست حلاً سحرياً لكل المشاكل، ولكنها أداة قوية جداً في ترسانة أي مهندس برمجيات عند استخدامها في السياق الصحيح.

نصيحتي الأخيرة لك: ابدأ صغيراً. لا تحاول إعادة كتابة نظامك بالكامل دفعة واحدة. اختر أكثر جزء مؤلم ومتشابك في تطبيقك، وحاول فصله باستخدام حدث واحد بسيط. شاهد الفوائد بنفسك. الشعور بالسيطرة والمرونة الذي ستحصل عليه يستحق كل دقيقة من الجهد. بالتوفيق يا أبطال! 🚀

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

أدوات وانتاجية

إعداد فريقي كان يستغرق أيامًا: كيف أنقذتني ‘حاويات التطوير’ (Dev Containers) من جحيم التضارب بين البيئات؟

أتذكر جيداً أياماً من الإحباط ضاعت في إعداد بيئات التطوير لفريقي، فكل جهاز جديد كان يعني معركة جديدة مع الإصدارات والاعتماديات. في هذه المقالة، أشارككم...

29 مارس، 2026 قراءة المزيد
أتمتة العمليات

بيئات العمل كانت تتغير من تلقاء نفسها: كيف أنقذتني ‘البنية التحتية كشفرة’ (IaC) من جحيم التكوينات الشبحية؟

أشارككم قصة حقيقية عن ليلة كابوسية قضيتها في تصحيح خطأ غامض في خوادم الإنتاج، وكيف كانت "البنية التحتية كشفرة" (IaC) هي المنقذ. اكتشفوا معي كيف...

29 مارس، 2026 قراءة المزيد
تسويق رقمي

حملاتي كانت تخاطب الجميع ولا أحد: كيف أنقذني التخصيص المدعوم بالذكاء الاصطناعي من جحيم معدلات التحويل المنخفضة؟

كنت أظن أن التسويق للجميع هو الحل الأمثل، حتى رأيت معدلات التحويل تنهار أمامي. في هذه المقالة، أشارككم قصتي مع التخصيص (Personalization) وكيف غير الذكاء...

29 مارس، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

تطبيقي كان يستبعد المستخدمين دون قصد: كيف أنقذتني ‘إرشادات الوصول إلى محتوى الويب’ (WCAG) من جحيم الإقصاء الرقمي؟

أشارككم قصتي كمطور برمجيات، وكيف اكتشفت أن تطبيقي الذي كنت فخورًا به كان يقصي فئة كبيرة من المستخدمين دون علمي. هذه رحلتي من "جحيم الإقصاء...

29 مارس، 2026 قراءة المزيد
الحوسبة السحابية

خوادمي كانت تلتهم الميزانية وهي خاملة: كيف أنقذتني ‘الحوسبة بدون خوادم’ (Serverless) من جحيم الفواتير؟

أشارككم قصتي مع الفواتير السحابية المرتفعة التي كادت أن تقتل مشروعي الجانبي. اكتشفوا كيف كانت "الحوسبة بدون خوادم" (Serverless) وتحديداً AWS Lambda هي طوق النجاة...

29 مارس، 2026 قراءة المزيد
البودكاست