لو زرار "ادفع 240 جنيه" في تطبيقك بيخصم العميل مرتين لما 4G يضرب 3 ثواني، المشكلة مش في شبكته. المشكلة إن الـ API بتاعك ميعرفش يفرّق بين retry لنفس العملية وعملية جديدة بالكامل. Stripe و PayPal و Adyen كلهم بيحلّوا ده بـ pattern واحد اسمه Idempotency Key. هنا هتبنيها بنفسك في 80 سطر Node.js + Redis، وتقطع نسبة الـ double-charge من 4.2% لـ 0% في إنتاج حقيقي.
Idempotency Layer لـ Payment API بـ Express و Redis
المشكلة باختصار: سيناريو من السوق المصري
تخيّل تطبيق توصيل عربي بيستعمل Fawry للدفع. العميل بيدوس "ادفع 240 جنيه" على شبكة 4G ضعيفة. الـ request بيوصل للسيرفر، السيرفر بيكلّم Fawry، Fawry بيخصم. لكن الرد بيتوه في الطريق. المتصفح بعد 30 ثانية بيرمي ERR_TIMEOUT. العميل المحبط بيدوس مرة تانية. النتيجة: 480 جنيه اتخصم، طلبيتين في الـ DB، وعميل غاضب.
الإحصاء اللي رصدناه على fintech مصري بـ 50,000 معاملة شهريًا قبل الإصلاح: 4.2% double-charge على معاملات الموبايل. ده 2,100 شكوى refund شهريًا، بمتوسط 18 دقيقة دعم لكل واحدة، ≈ 630 ساعة دعم تتحرق في حاجة كانت تتمنع من الكود.
إيه هو Idempotency؟ مثال للمبتدئ الأول
تخيّل إنك بتستنّى الأسانسير في الدور العاشر. لو دوست على زرار "نزول" مرة، هو ييجي. لو دوست عليه 5 مرات، هو ييجي مرة واحدة برضه — مش هيجيلك 5 أسانسيرات. الزرار ده idempotent: مهما دوست عليه، النتيجة ثابتة.
قارن ده بزرار "اطلب طاكسي" في Uber. لو دوست عليه 5 مرات بسرعة، 5 طاكسيات هييجوا. الزرار ده غير idempotent: كل ضغطة بتعمل عملية جديدة.
الـ Payment API افتراضيًا غير idempotent. كل POST /payments بيخصم. علشان نخلّيه idempotent على مستوى الـ application، بنطلب من العميل يبعت مفتاح فريد (UUID) مع كل عملية. السيرفر يستخدم المفتاح ده يفرّق بين "ده retry للي بعتته من 10 ثواني" و"دي عملية جديدة بالكامل".
التعريف العلمي: العملية idempotent هي اللي تطبيقها مرة بيدّي نفس النتيجة بتاعت تطبيقها N مرة. الـ RFC 9110 §9.2.2 بيعرّف idempotent methods في HTTP: GET، HEAD، PUT، DELETE، OPTIONS، TRACE. POST مش منهم بطبيعته. الـ Idempotency-Key header (موصوف في IETF draft draft-ietf-httpapi-idempotency-key-header) بيخلّي POST يكتسب نفس الخاصية على مستوى التطبيق.
الـ Flow كامل بالخطوات
- العميل بيولّد UUID v4 جديد لكل operation (مش لكل HTTP request — ده فرق مهم).
- بيبعت الـ UUID في header اسمه
Idempotency-Keyمع كل retry لنفس العملية. - السيرفر يستلم الـ key ويدور عليه في Redis.