كانت خدماتنا ككرة صوف: كيف حررتنا ‘المعمارية الموجهة بالأحداث’ (EDA) من جحيم التبعيات؟

يا أهلاً وسهلاً فيكم جميعاً. اسمحوا لي اليوم أحكي لكم قصة صارت معي ومع فريقي قبل كم سنة، قصة علّمتنا درس قاسي لكنه ثمين جداً في عالم هندسة البرمجيات.

كنا في شركة ناشئة، قلوبنا مليئة بالحماس والطموح، ونعمل على منتجنا ليل نهار. النظام كان عبارة عن مجموعة من الخدمات المصغرة (Microservices) اللي بتتواصل مع بعضها. كان عنا خدمة للمستخدمين (Users)، وخدمة للطلبات (Orders)، وخدمة للمخزون (Inventory)، وخدمة للإشعارات (Notifications). الوضع كان ماشي، والأمور تبدو تحت السيطرة.

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

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

في تلك الليلة، وبينما كنا نحاول إعادة كل شيء للعمل يدوياً، صرخ أحد الزملاء في وجهي، “يا أبو عمر، ليش كل شي مربوط ببعضه زي هيك؟ لازم نلاقي حل!”. كان صراخه هو شرارة البداية لرحلتنا نحو عالم جديد، عالم “المعمارية الموجهة بالأحداث” أو الـ Event-Driven Architecture.

ما هي المعمارية الموجهة بالأحداث (EDA)؟

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

النمط التقليدي (Request/Response) هو مثل طرق الأبواب. خدمة (أ) تحتاج معلومة من خدمة (ب)، فتطلبها منها مباشرة وتنتظر الرد. هذا يخلق اقتراناً قوياً (Tight Coupling)، فإذا كانت خدمة (ب) مشغولة أو متوقفة، تتعطل خدمة (أ) معها.

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

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

المشكلة قبل EDA: عالم التبعيات المباشرة

لنعد إلى مثالنا الكارثي. هكذا كان تسلسل العمليات قبل EDA:

  1. المستخدم يضغط “إتمام الطلب”.
  2. خدمة الطلبات (Orders) تستقبل الطلب.
  3. خدمة الطلبات تستدعي خدمة المستخدمين (Users) للتحقق من بياناته (نقطة فشل 1).
  4. خدمة الطلبات تستدعي خدمة المخزون (Inventory) لخصم الكمية (نقطة فشل 2).
  5. خدمة الطلبات تستدعي خدمة الإشعارات (Notifications) لإرسال إيميل تأكيد (نقطة فشل 3).
  6. إذا نجحت كل هذه الخطوات، يتم تأكيد الطلب.

أي مشكلة في أي من هذه الخدمات التابعة كانت كفيلة بإيقاف عملية إنشاء الطلب بأكملها. هذا ما نسميه بالاقتران المحكم أو “كرة الصوف”.

مثال بالكود (الطريقة القديمة)

هكذا كان يبدو الكود في خدمة الطلبات (باستخدام Node.js كمثال):


// في خدمة الطلبات (Orders Service)
async function createOrder(orderData) {
  // 1. التحقق من المستخدم عبر استدعاء مباشر
  try {
    const userResponse = await axios.get(`http://users-service/api/users/${orderData.userId}`);
    if (!userResponse.data.is_active) {
      throw new Error('User is not active');
    }
  } catch (error) {
    console.error('Users service is down or user is invalid', error);
    // العملية كلها تفشل هنا!
    return { success: false, message: 'Failed to validate user.' };
  }

  // 2. تحديث المخزون عبر استدعاء مباشر
  try {
    await axios.post('http://inventory-service/api/decrease-stock', { items: orderData.items });
  } catch (error) {
    console.error('Inventory service is down', error);
    // العملية كلها تفشل هنا!
    return { success: false, message: 'Failed to update inventory.' };
  }
  
  // ... حفظ الطلب في قاعدة البيانات ...

  // 3. إرسال إشعار عبر استدعاء مباشر
  try {
    await axios.post('http://notifications-service/api/send-email', { ... });
  } catch (error) {
    // ماذا نفعل هنا؟ الطلب تم إنشاؤه لكن الإشعار فشل. مشكلة!
    console.error('Notifications service is down', error);
  }

  return { success: true, order };
}

لاحظ كمية المشاكل والتعقيدات في التعامل مع الأخطاء. إنه جحيم حقيقي.

الحل مع EDA: فك الارتباط والعيش بحرية!

بعد تلك الليلة، قررنا إعادة التفكير في كل شيء. قمنا بتبني معمارية EDA. إليكم كيف أصبح تسلسل العمليات الجديد:

  1. المستخدم يضغط “إتمام الطلب”.
  2. خدمة الطلبات (Orders) تستقبل الطلب، تتحقق من صحته مبدئياً، وتحفظه في قاعدة بياناتها بحالة “قيد المعالجة”.
  3. خدمة الطلبات تنشر حدثاً (Event) اسمه OrderPlaced إلى وسيط الرسائل (Message Broker). هذا الحدث يحتوي على كل تفاصيل الطلب.
  4. انتهى دور خدمة الطلبات! تعود للمستخدم برسالة “تم استلام طلبك بنجاح وجاري معالجته”.

الآن، في الخلفية، وبشكل غير متزامن (Asynchronously)، يحدث السحر:

  • خدمة المخزون (Inventory): كانت “تستمع” للأحداث من نوع OrderPlaced. عندما تستلم الحدث، تقوم بخصم الكميات من المخزون. إذا نجحت، تنشر حدثاً جديداً اسمه InventoryUpdated.
  • خدمة الإشعارات (Notifications): هي أيضاً تستمع لحدث OrderPlaced. عندما تستلمه، تقوم بإرسال بريد إلكتروني للمستخدم.
  • خدمة التحليلات (Analytics): خدمة جديدة لم تكن موجودة من قبل، يمكننا إضافتها بسهولة. تستمع لحدث OrderPlaced لتسجيل بيانات عن المبيعات بدون أن يؤثر ذلك على أي خدمة أخرى.

مثال بالكود (الطريقة الجديدة مع EDA)

هنا الكود الجديد في خدمة الطلبات. لاحظ البساطة والتركيز على مهمة واحدة.


// في خدمة الطلبات (Orders Service)
import messageBroker from './message-broker-client'; // مكتبة وسيط الرسائل

async function createOrder(orderData) {
  // لا يوجد استدعاءات مباشرة للخدمات الأخرى!
  // فقط حفظ الطلب في قاعدة البيانات الخاصة بالخدمة
  const newOrder = await db.orders.create({ ...orderData, status: 'PROCESSING' });

  // إنشاء الحدث
  const event = {
    type: 'OrderPlaced',
    timestamp: new Date(),
    payload: {
      orderId: newOrder.id,
      userId: newOrder.userId,
      items: newOrder.items,
      totalAmount: newOrder.totalAmount
    }
  };

  // نشر الحدث... وانتهى الموضوع! (Fire and Forget)
  await messageBroker.publish('orders_topic', event);

  console.log(`Event OrderPlaced published for order ${newOrder.id}`);
  
  // نرجع للمستخدم مباشرة بنجاح
  return { success: true, order: newOrder };
}

والآن، في خدمة الإشعارات، الكود يبدو هكذا:


// في خدمة الإشعارات (Notifications Service)
import messageBroker from './message-broker-client';

// الاشتراك في الموضوع والاستماع للأحداث
messageBroker.subscribe('orders_topic', (event) => {
  // التأكد من أن الحدث هو ما نهتم به
  if (event.type === 'OrderPlaced') {
    const { userId, orderId } = event.payload;
    
    // ... منطق إرسال البريد الإلكتروني ...
    console.log(`Sending confirmation email to user ${userId} for order ${orderId}`);
    // sendEmail(userId, 'Your order has been placed!');
  }
});

console.log('Notifications service is listening for events...');

لاحظوا الجمال هنا. لو كانت خدمة الإشعارات متوقفة، لا مشكلة! الحدث سيبقى في “قائمة الانتظار” (Queue) لدى وسيط الرسائل. عندما تعود الخدمة للعمل، ستأخذ الحدث وتعالجه وكأن شيئاً لم يكن. هذا ما يسمى بالمرونة (Resilience).

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

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

  • ابدأ بسيطًا: لا تحتاج إلى استخدام أدوات معقدة مثل Apache Kafka من اليوم الأول. يمكنك البدء بوسيط رسائل أبسط مثل RabbitMQ أو حتى خدمات سحابية مُدارة مثل AWS SQS/SNS أو Google Pub/Sub. “مش كل إشي بده مدفع يا جماعة”، أحياناً مسدس صغير يكفي.
  • عرّف عقود الأحداث (Event Contracts): الحدث هو عقد بين المنتج والمستهلك. قم بتوثيق شكل وبيانات كل حدث بشكل واضح. أي تغيير غير مدروس في بنية الحدث قد يؤدي إلى انهيار الخدمات المستهلكة له. استخدم أدوات مثل Avro أو Protobuf لضمان ذلك.
  • جهز نفسك للاتساق النهائي (Eventual Consistency): في عالم EDA، البيانات لا تتحدث فوراً في كل مكان. قد يستغرق الأمر بضع أجزاء من الثانية حتى يتم تحديث المخزون بعد إنشاء الطلب. هذا جيد لمعظم الحالات، لكن يجب أن تكون واعياً لهذا المبدأ وتصمم واجهات المستخدم للتعامل معه (مثلاً، عرض رسالة “جاري المعالجة”).
  • المراقبة والرصد أهم من أي وقت مضى: تتبع مسار عملية ما يصبح أصعب في EDA. “وين راحت الرسالة؟” هو سؤال ستطرحه كثيراً. استثمر في أدوات التتبع الموزع (Distributed Tracing) والمراقبة التي تمكنك من رؤية تدفق الأحداث عبر النظام بأكمله.
  • التعامل مع الفشل بذكاء: ماذا لو فشلت خدمة مستهلكة في معالجة حدث ما بشكل متكرر؟ يجب أن يكون لديك آلية للتعامل مع هذه “الرسائل الميتة”، مثل إرسالها إلى قائمة انتظار خاصة (Dead-Letter Queue) لتحليلها لاحقاً دون إيقاف بقية النظام.

الخلاصة: حرية من قيود التبعية

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

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

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

أبو عمر

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

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

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

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

آخر المدونات

نصائح برمجية

كانت أرقامي السحرية طلاسم لا تُقرأ: كيف أنقذتنا ‘الثوابت المسماة’ من جحيم ‘ماذا يعني هذا الرقم؟’

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

25 مايو، 2026 قراءة المزيد
​معمارية البرمجيات

تحديث المونوليث كجراحة قلب مفتوح: كيف أنقذنا نمط الخانق (Strangler Fig) من جحيم “إياك أن تلمس هذا الكود”؟

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

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

ما وراء الكلمات المفتاحية: كيف حولنا بيانات Schema.org إلى أسلحة سرية في حرب نتائج البحث؟

أنا أبو عمر، مبرمج فلسطيني، وفي هذه المقالة سأشارككم قصة حقيقية حول كيف أنقذنا مشروعًا من الضياع في صفحات جوجل الخلفية باستخدام البيانات المنظمة (Schema.org)....

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

كانت شاشاتنا الفارغة مقبرة للتفاعل: كيف أنقذتنا ‘الحالات الفارغة الذكية’ من جحيم ‘وماذا الآن؟’

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

25 مايو، 2026 قراءة المزيد
برمجة وقواعد بيانات

كانت استعلاماتنا تزحف: كيف أنقذتنا فهارس قواعد البيانات من جحيم البحث البطيء؟

قصة من الميدان عن كيفية تحويل استعلامات SQL البطيئة التي تشبه السلحفاة إلى عمليات فائقة السرعة باستخدام أداة بسيطة وقوية: فهارس قواعد البيانات. مقالة عملية...

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