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

عند بناء تطبيق، من السهل أن تتخيل الطلبات تأتي مرتبة، واحدة تلو الأخرى، وبالترتيب الصحيح. الشبكات الواقعية لا تعمل هكذا. يضغط المستخدم "ادفع" مرتين لأن الشاشة تجمّدت. تنقطع شبكة الهاتف مباشرة بعد الضغط. يصل webhook متأخراً، أو يصل مرتين. وأحياناً لا يصل أصلاً.
فكرة بات هيلاند عن البيانات في الخارج مقابل الداخل طريقة واضحة للتفكير في هذه الفوضى.
"الخارج" هو كل ما لا يتحكم به نظامك. هنا تتواصل مع أشخاص وأنظمة أخرى، وحيث التسليم غير مضمون: طلبات HTTP من المتصفحات والتطبيقات المحمولة، رسائل من الطوابير، webhooks من طرف ثالث (دفعات، بريد إلكتروني، شحن)، ومحاولات إعادة الإرسال التي يطلقها العملاء أو البروكسي أو مهام الخلفية.
على الخارج، افترض أن الرسائل قد تتأخّر، تُكرّر، أو تصل خارج الترتيب. حتى لو كان شيء ما "عادةً موثوقًا"، صمّم ليوم لا يكون فيه كذلك.
"الداخل" هو ما يمكنك جعله موثوقاً. هو الحالة الدائمة التي تخزنها، القواعد التي تفرضها، والحقائق التي يمكنك إثباتها لاحقاً:
الداخل هو المكان الذي تحمي فيه الثوابت. إذا وعدت بـ"دفع واحد لكل طلب"، يجب فرض هذا الوعد داخل النظام، لأن الخارج لا يمكن الوثوق به.
التحول في التفكير بسيط: لا تفترض تسليمًا مثاليًا أو توقيتًا مثاليًا. عامل كل تفاعل خارجي كاقتراح غير موثوق قد يتكرر، واجعل الداخل يتعامل بأمان.
هذا مهم حتى للفرق الصغيرة والتطبيقات البسيطة. المرة الأولى التي يتسبب فيها خلل شبكي في خصم مكرر أو طلب عالق، يتوقف الأمر عن كونه نظرية ويصبح استردادًا، تذكرة دعم، وفقدان ثقة.
مثال ملموس: يضغط المستخدم "أرسل الطلب"، يرسل التطبيق طلبًا، وتنقطع الاتصال. يحاول المستخدم مرة أخرى. إذا لم يكن لدى الداخل طريقة للتعرّف على "هذه المحاولة نفسها"، قد تنشئ طلبين، تحتجز المخزون مرتين، أو ترسل بريدي تأكيد مرتين.
نقطة هيلاند مباشرة: العالم الخارجي غير مؤكد، لكن داخل نظامك يجب أن يبقى متسقًا. الشبكات تفقد حزمًا، الهواتف تفقد الإشارة، الساعات تنحرف، والمستخدمون يضغطون تحديث. تطبيقك لا يمكنه التحكم في أي من ذلك. ما يمكنه التحكم به هو ما يقبل كـ"حقيقة" بمجرد عبور البيانات لحد واضح.
تخيل شخصًا يطلب قهوة على هاتفه أثناء المرور في مبنى ذو واي‑فاي سيئ. يضغط "ادفع". يدور المؤشر. تنقطع الشبكة. يضغط مرة أخرى.
ربما وصل الطلب الأول إلى خادمك، لكن الرد لم يصل إلى العميل. أو ربما لم يصل أي طلب. من منظور المستخدم، كل الاحتمالات تبدو متماثلة.
هذا هو الوقت والشك: أنت لا تعرف ما حدث بعد، وقد تتعلم لاحقاً. يحتاج نظامك إلى التصرف بعقلانية أثناء الانتظار.
بمجرد أن تقبل أن الخارج غير موثوق، تصبح بعض السلوكيات "الغريبة" عادية:
البيانات الخارجية هي ادعاء، ليست حقيقة. "أنا دفعت" مجرد بيان مرسل عبر قناة غير موثوقة. تصبح حقيقة فقط بعد أن تسجلها داخل نظامك بطريقة دائمة ومتسقة.
هذا يدفعك إلى ثلاث عادات عملية: حدد حدودًا واضحة، اجعل إعادة المحاولة آمنة عبر عدم التكرار، وخطط للمصالحة عندما لا تتطابق الحقيقة الخارجية مع داخلك.
فكرة "الخارج مقابل الداخل" تبدأ بسؤال عملي: أين تبدأ وتنتهي حقيقة نظامك؟
داخل الحد يمكنك تقديم ضمانات قوية لأنك تتحكم بالبيانات والقواعد. خارج الحد تبذل محاولات بأفضل جهد وتفترض أن الرسائل قد تُفقد أو تُكرر أو تتأخّر أو تصل خارج الترتيب.
في التطبيقات الحقيقية، يظهر هذا الحد عادة في أماكن مثل:
بعد رسم هذا الخط، قرر أي الثوابت غير قابلة للتفاوض داخل الحدود. أمثلة:
الحد يحتاج أيضاً لغة واضحة للمعاني "أين نحن". الكثير من الأخطاء تعيش في الفجوة بين "استلمناك" و"أنجزناه". نمط مفيد هو فصل ثلاث معاني:
عندما يتخطى الفريقون هذا، ينتهي بهم الأمر إلى أخطاء تظهر فقط تحت الحمل أو أثناء الانقطاعات الجزئية. نظام واحد يستخدم "مدفوع" ليعني المال المحتجز؛ وآخر يستخدمه ليعني محاولة دفع بدأت. هذا التباين يخلق تكرارات وطلبات عالقة وتذاكر دعم لا يمكن لأحد إعادة إنتاجها.
عدم التكرار يعني: إذا أُرسل نفس الطلب مرتين، يعامله النظام كما لو كان طلبًا واحدًا ويعيد نفس النتيجة.
الإعادات طبيعية. تحدث المهلات. يكرر العملاء أنفسهم. إذا كان الخارج يمكنه التكرار، فعلى الداخل تحويل ذلك إلى تغييرات حالة ثابتة.
مثال بسيط: يرسل تطبيق محمول "ادفع 20$" وينقطع الاتصال. يعيد التطبيق المحاولة. بدون عدم التكرار، قد تُخصم من العميل مرتين. مع عدم التكرار، يعيد الطلب الثاني نتيجة الشحنة الأولى.
تستخدم معظم الفرق واحدًا من هذه الأنماط (أحيانًا مزيج):
Idempotency-Key: ...). يسجل الخادم المفتاح والرد النهائي.عندما يصل مكرر، السلوك الأفضل عادة ليس "409 conflict" أو خطأ عام. هو إرجاع نفس النتيجة التي أعدتها في المرة الأولى، بما في ذلك نفس معرف المورد والحالة. هذا ما يجعل إعادة المحاولات آمنة للعملاء والمهام الخلفية.
سجل عدم التكرار يجب أن يعيش داخل حدودك في تخزين دائم، ليس في الذاكرة. إذا أعيد تشغيل API ونُسيت السجلات، تختفي ضمانات الأمان.
احتفظ بالسجلات مدة كافية لتغطية عمليات إعادة المحاولة الواقعية والتسليمات المتأخرة. النافذة تعتمد على مخاطرك التجارية: دقائق إلى ساعات للإنشاءات منخفضة المخاطر، أيام للمدفوعات/الرسائل/الشحن حيث التكرار مكلف، وأطول إذا كان لدى الشركاء فترة إعادة محاولة ممتدة.
تبدو المعاملات الموزعة مريحة: عملية التزام كبيرة عبر خدمات وطوابير وقواعد بيانات. عمليا، غالبًا ما تكون غير متاحة أو بطيئة أو هشة جدًا للاعتماد عليها. بمجرد وجود قفزة شبكية، لا يمكنك افتراض أن كل شيء التزم معًا.
فخ شائع هو بناء سير عمل يعمل فقط إذا نجحت كل خطوة الآن: حفظ الطلب، تحصيل البطاقة، حجز المخزون، إرسال تأكيد. إذا تعطل الخطوة 3 أو انتهت مهلة، هل فشلت أم نجحت؟ إذا أعدت المحاولة، هل ستخصم مرتين أو تحجز مرتين؟
نهجان عمليان يتجنّبان هذا:
اختر نمطًا واحدًا لكل سير عمل والتزم به. المزج بين "أحيانًا نستخدم outbox" و"أحيانًا نفترض النجاح المتزامن" يخلق حوافًا يصعب اختبارها.
قاعدة بسيطة تساعد: إذا لم تستطع الالتزام عبر الحدود بشكل ذري، صمّم لإعادة المحاولات والتكرارات والتأخير.
المصالحة هي الاعتراف بحقيقة أساسية: عندما يتحدث تطبيقك إلى أنظمة أخرى عبر الشبكة، سترتكبون أحيانًا اختلافات في ما حدث. تنتهي المهلات، تصل callbacks متأخرة، ويعيد الناس الإجراءات. المصالحة هي كيف تكتشف الاختلافات وتصلحها مع الوقت.
عامل الأنظمة الخارجية كمصادر حقيقة مستقلة. يحتفظ تطبيقك بسجل داخلي، لكنه يحتاج طريقة لمقارنة ذلك السجل بما فعله الشركاء والمزودون والمستخدمون بالفعل.
تستخدم معظم الفرق مجموعة أدوات مملة وصغيرة (والممل جيد): عامل يعيد محاولة الإجراءات المعلقة ويتحقق من الحالة الخارجية، فحص مجدول للعدم التناسق، وإجراء إصلاح بسيط للدعم لإعادة المحاولة أو الإلغاء أو وسم كمراجَع.
المصالحة تنجح فقط إذا عرفت ما تقارن به: دفتر حسابات داخلي مقابل دفتر الموفر (المدفوعات)، حالة الطلب مقابل حالة الشحنة (التنفيذ)، حالة الاشتراك مقابل حالة الفوترة.
اجعل الحالات قابلة للإصلاح. بدلاً من القفز مباشرة من "created" إلى "completed"، استخدم حالات انتظار مثل pending، on hold، أو needs review. هذا يجعل من الآمن القول "لسنا متأكدين بعد"، ويعطي المصالحة مكانًا واضحًا للهبوط.
سجّل أثر تدقيق صغير على التغييرات المهمة:
مثال: إذا طلب تطبيقك ملصق شحن وانقطعت الشبكة، قد ينتهي بك الأمر بـ"لا ملصق" داخليًا بينما الموفر أنشأ واحدًا فعليًا. عامل المصالحة يمكنه البحث بمعرّف الترابط، اكتشاف وجود الملصق، وتحريك الطلب إلى الأمام (أو وسمه للمراجعة إذا لم تتطابق المعلومات).
بمجرد أن تفترض أن الشبكة ستفشل، يتغير الهدف. لست تحاول جعل كل خطوة تنجح من المحاولة الأولى. تحاول جعل كل خطوة آمنة لإعادة المحاولة وسهلة الإصلاح.
اكتب بيان حد من جملة واحدة. كن صريحًا بشأن ما يملكه نظامك (مصدر الحقيقة)، ما هو مجرد مرآة، وما تطلبه فقط من الآخرين.
ادرج أوضاع الفشل قبل مسار النجاح. على الأقل: مهلات (لا تعرف إن نجح)، طلبات مكررة، نجاح جزئي (حدثت خطوة ولم تحدث التالية)، وأحداث خارجة عن الترتيب.
اختر استراتيجية عدم التكرار لكل مدخل. لواجهات API المتزامنة، غالبًا مفتاح idempotency مع نتيجة مخزنة. للرسائل/الأحداث، عادةً معرّف رسالة فريد وسجل "هل عالجتُ هذا؟".
احفظ النية، ثم افعل. أولاً خزّن شيئًا دائمًا مثل "PaymentAttempt: pending" أو "ShipmentRequest: queued"، ثم نفّذ الاتصال الخارجي، ثم خزّن النتيجة. أعد مرجعًا ثابتًا حتى تشير المحاولات إلى نفس النية بدلًا من إنشاء نية جديدة.
ابنِ المصالحة ومسار الإصلاح، واجعلها مرئية. يمكن أن تكون المصالحة مهمة تفحص السجلات الـ"معلقة لفترة طويلة" وتتحقق من الحالة الخارجية. مسار الإصلاح يمكن أن يكون إجراء دعم آمن مثل "retry"، "cancel"، أو "mark resolved"، مع ملاحظة تدقيق. أضف رصدًا أساسيًا: معرّفات ترابط، حقول حالة واضحة، وبعض العدّادات (معلقة، محاولات، إخفاقات).
مثال: إذا انتهت مهلة الخروج من الدفع بعد الاتصال بمزوِّد الدفع، لا تخمن. خزّن المحاولة، أعد معرف المحاولة، ودَع المستخدم يعيد المحاولة بنفس مفتاح عدم التكرار. لاحقًا، يمكن للمصالحة التأكد مما إذا كان الموفر قد خصم أم لا وتحديث المحاولة دون خصم مزدوج.
ضغط الزبون "أرسل الطلب". خدمتك ترسل طلب دفع إلى مزوّد، لكن الشبكة متقلبة. لدى الموفر حقيقته، ولدى قاعدتك بياناتك. سيفترقان ما لم تصمّم للتعامل مع ذلك.
من وجهة نظرك، الخارج تيار من الرسائل التي قد تكون متأخرة أو مكررة أو مفقودة:
لا تضمن أي من هذه الخطوات "مرة واحدة بالضبط". هي فقط "ربما".
داخل الحد، خزّن الحقائق الدائمة والحد الأدنى اللازم لربط الأحداث الخارجية بتلك الحقائق.
عندما يضع الزبون الطلب أول مرة، أنشئ سجل order بحالة واضحة مثل pending_payment. أنشئ أيضاً سجل payment_attempt مع مرجع موفر فريد بالإضافة إلى idempotency_key مربوط بفعل الزبون.
إذا انتهت مهلة العميل وأعاد المحاولة، يجب ألا ينشئ API طلبًا ثانيًا. يجب أن يبحث عن idempotency_key ويعيد نفس order_id والحالة الحالية. هذا الاختيار الوحيد يمنع التكرارات عند فشل الشبكات.
الآن يصل webhook مرتين. التحديث الأول يغيّر payment_attempt إلى authorized وينقل الطلب إلى paid. الضربة الثانية تصل لنفس المعالج، لكنك تكتشف أنك عالجت بالفعل حدث المزود (بحفظ معرف حدث المزود، أو بفحص الحالة الحالية) ولا تفعل شيئًا. يمكنك مع ذلك الرد 200 OK لأن النتيجة قد تحققت بالفعل.
أخيرًا، تتولى المصالحة الحالات المعقّدة. إذا ظل الطلب pending_payment بعد مهلة، تعمل وظيفة خلفية على استعلام الموفر باستخدام المرجع المخزن. إذا قال الموفر "authorized" لكنك فاتك الwebhook، تحدث سجلاتك. إذا قال الموفر "failed" بينما سجلت أنك مدفوع، وسمها للمراجعة أو أشغِل تعويضًا مثل استرداد.
معظم السجلات المكررة وتدفقات العمل "العالقة" تأتي من خلط ما حدث خارج نظامك (وصل طلب، وصلت رسالة) مع ما التزمته بأمان داخل نظامك.
فشل كلاسيكي: يرسل العميل "أرسل الطلب"، يخدمك يبدأ العمل، تنقطع الشبكة، ويعيد العميل المحاولة. إذا عاملت كل محاولة كحقيقة جديدة، ستحصل على خصومات مزدوجة، طلبات مكررة، أو رسائل بريد إلكتروني متعددة.
الأسباب المعتادة:
أمر واحد يجعل كل شيء أسوأ: لا أثر تدقيقي. إذا كتبت فوق الحقول واحتفظت فقط بالحالة الأخيرة، تفقد الأدلة التي تحتاجها للمصالحة لاحقًا.
فحص جيد للمعقولية: "إذا شغّلت هذا المعالج مرتين، هل أحصل على نفس النتيجة؟" إذا كان الجواب لا، فالتكرارات ليست حافة نادرة، بل مؤكدة.
إذا تذكرت شيئًا واحدًا: يجب أن يبقى تطبيقك صحيحًا حتى لو وصلت الرسائل متأخرة، مرتين، أو لم تصل أبداً.
استخدم هذه القائمة لاكتشاف النقاط الضعيفة قبل أن تتحول لمشاكل مكررة أو حالات مفقودة أو تدفقات عمل عالقة:
إذا لم تستطع الإجابة عن أحد هذه بسرعة، فهذا مفيد عادة. يعني غالبًا أن الحد غير واضح أو أن انتقال حالة مفقود.
خطوات عملية مقترحة:
ارسم الحدود والحالات أولاً. عرّف مجموعة صغيرة من الحالات لكل سير عمل (مثال: Created, PaymentPending, Paid, FulfillmentPending, Completed, Failed).
أضف عدم التكرار حيث يهم أكثر. ابدأ بالكتابات الأعلى مخاطرة: إنشاء طلب، تحصيل دفع، إصدار استرداد. خزّن مفاتيح عدم التكرار في PostgreSQL مع قيد فريد ليُرفض التكرار بأمان.
عامل المصالحة كميزة عادية. جدولة مهمة تبحث عن السجلات "المعلقة لفترة طويلة"، تتحقق من الأنظمة الخارجية مرة أخرى، وتصلح الحالة المحلية.
كرر بأمان. عدّل الانتقالات وقواعد إعادة المحاولة، ثم اختبر بإعادة إرسال نفس الطلب وإعادة معالجة نفس الحدث عن عمد.
إذا كنت تبني بسرعة على منصة مدفوعة بالدردشة مثل Koder.ai، ما يزال من الجدير إدخال هذه القواعد في الخدمات الناتجة مبكراً: السرعة تأتي من الأتمتة، لكن الموثوقية تأتي من الحدود الواضحة، المعالجات غير القابلة للتكرار، والمصالحة.
“الخارج” هو أي شيء لا تسيطر عليه: المتصفحات، شبكات الهواتف المحمولة، الطوابير، webhooks من طرف ثالث، عمليات إعادة المحاولة والمهلات.
“الداخل” هو ما تسيطر عليه: الحالة المخزنة لديك، القواعد التي تفرضها، والحقائق التي يمكنك إثباتها لاحقاً (عادة في قاعدة البيانات).
لأن الشبكة لا تقول الحقيقة دائماً.
انتهاء مهلة العميل لا يعني أن الخادم لم يعالج الطلب. وصول webhook مرتين لا يعني أن المزود نفّذ الإجراء مرتين. إذا عاملت كل رسالة على أنها "حقيقة جديدة" ستنشئ سجلات مكررة، خصومات مزدوجة، وتدفقات عمل عالقة.
الحدّ هو النقطة التي تتحول عندها رسالة غير موثوقة إلى حقيقة دائمة.
حدود شائعة:
بعد عبور البيانات للحد، تفرض داخل الحدود قواعِد صارمة (مثلاً: "لا يمكن دفع الطلب أكثر من مرة").
استعمل عدم التكرار (idempotency). المبدأ: نفس النية يجب أن تُنتج نفس النتيجة حتى لو أُرسلت مرات متعددة.
أنماط عملية:
لا تخزنها في الذاكرة فقط. احتفظ بها داخل حدودك (مثلاً PostgreSQL) حتى لا تمحى عند إعادة تشغيل الخدمة.
قاعدة عامة للاحتفاظ:
احتفظ بالمدة الكافية لتغطية إعادة المحاولات المعقولة والتأخيرات.
استخدم حالات تعترف بعدم اليقين.
مجموعة بسيطة وعملية:
pending_* (قبل معرفة النتيجة النهائية)succeeded / failed (تسجيل نتيجة نهائية)needs_review (اكتشفنا تفاوتاً يحتاج تدخلاً بشرياً أو عملية خاصة)هذا يمنع التخمين أثناء المهلات ويسهّل المصالحة.
لأنه من الصعب الالتزام معاملة واحدة عبر أنظمة متعددة عبر الشبكة.
إذا فعلت "حفظ الطلب → تحصيل البطاقة → حجز المخزون" متزامناً وخطأ أحدهم أو أبطأ، فلن تعرف إن كان يجب إعادة المحاولة. إعادة المحاولة قد تسبّب تكراراً؛ وعدم المحاولة قد يترك عملًا ناقصًا.
تصمم للنجاح الجزئي: احفظ النية أولاً، ثم نفّذ الأفعال الخارجية، ثم سجّل النتائج.
نمط outbox/inbox يجعل المراسلة بين الأنظمة موثوقة دون الادعاء بأن الشبكة مثالية.
المصالحة هي كيف تسترد النظام عندما تختلف سجلاتك مع نظام خارجي.
افتراضيات جيدة:
needs_reviewالمصالحة ليست اختيارية للمدفوعات، التنفيذ، الاشتراكات، أو أي شيء يتعامل مع webhooks.
خطوتان: لا تحاول جعل كل خطوة تنجح دفعة واحدة؛ اجعل كل خطوة قابلة للتكرار وسهلة الإصلاح.
مسار عملي:
PaymentAttempt: pending ثم اتصل بالمزود ثم سجّل النتيجة.نعم. السرعة لا تلغي فشل الشبكة—فقط توصلك إليه أسرع.
إذا كنت تبني باستخدام Koder.ai، ضع هذه القواعد مبكراً:
بهذه الطريقة تصبح إعادة المحاولات والردود المكررة boring بدلاً من مكلفة.