كانت خدماتنا ككرة صوف: كيف حررتنا ‘المعمارية الموجهة بالأحداث’ (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) صعبة في البداية، وتطلبت منا تعلم أنماط جديدة في التفكير والبرمجة. لكن النتيجة كانت مذهلة. تحول نظامنا من “كرة صوف” هشة ومتشابكة إلى مجموعة من الخدمات المستقلة، المرنة، والقادرة على التطور والتوسع بشكل منفصل. أصبح بإمكاننا إضافة خدمات جديدة تستمع للأحداث الموجودة دون لمس أي سطر كود في الخدمات القديمة.

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

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

أبو عمر

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

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

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

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

آخر المدونات

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

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

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

26 أبريل، 2026 قراءة المزيد
أتمتة العمليات

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

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

26 أبريل، 2026 قراءة المزيد
ذكاء اصطناعي

كان أداء نماذجنا يتدهور بصمت: كيف أنقذنا رصد انحراف البيانات (Data Drift) من جحيم التنبؤات الفاسدة؟

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

26 أبريل، 2026 قراءة المزيد
خوارزميات

كنا نسأل قاعدة البيانات عن كل شاردة وواردة: كيف أنقذتنا ‘مرشحات بلوم’ (Bloom Filters) من جحيم استعلامات التحقق؟

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

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

ميزانيتنا التسويقية كانت ثقباً أسود: كيف أنقذنا ‘نموذج الإحالة المبني على البيانات’ من جحيم تخمين العائد على الاستثمار؟

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

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

واجهاتنا كانت فوضى بصرية: كيف أنقذنا ‘نظام التصميم’ (Design System) من جحيم عدم الاتساق؟

أشارككم قصتي كـ"أبو عمر" مع الفوضى التي عمت واجهاتنا الرقمية، وكيف كان "نظام التصميم" هو طوق النجاة. هذه ليست مجرد مقالة تقنية، بل هي حكاية...

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

كان بحثنا بطيئاً وغير دقيق: كيف أنقذنا البحث كامل النص (Full-Text Search) من جحيم استعلامات LIKE؟

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

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