لو endpoint /charge بتاعك بياخد طلب، يخصم من الكارت، وبعدها الإنترنت يتقطع قبل ما الـ response يوصل للعميل، الـ frontend هيعيد المحاولة تلقائيًا. النتيجة: العميل اتخصم منه مرتين على نفس العملية. الحل اسمه Idempotency Key، وبتطبّقه في 70 سطر.
Idempotency Keys: ازاي تخلي عملية حساسة تتنفّذ مرة واحدة بس مهما اتعادت
الموضوع ده مش رفاهية. Stripe و PayPal و AWS كلهم بيفرضوا Idempotency Key على عمليات الكتابة. لو ما عندكش الطبقة دي في API بتاعك، بيتحصل لك مشكلة دفعات مكررة في وقت ضغط على شبكة العميل، مش في الاختبارات. ركز معايا في الجزء ده — ده اللي بيحصل فعلاً في الإنتاج.
المشكلة باختصار
أي client حديث (browser، mobile، حتى Postman في الـ retry policy) بيعيد المحاولة لما الـ request يفشل. الفشل ممكن يحصل في 3 أماكن:
- الـ request ما وصلش للسيرفر أصلاً.
- السيرفر استلم ونفّذ، لكن الـ response ضاع في الطريق.
- السيرفر استلم بس مات قبل الرد.
الحالة التانية هي الأخطر. السيرفر شحن الكارت فعلاً، الـ DB اتحدّثت، بس الـ client شايف timeout فبيبعت الطلب تاني. من غير حماية، بتشحن مرتين.
المثال للمبتدئ: ايصال البنك
تخيل إنك في ATM، طلبت تسحب 1000 جنيه. الجهاز اتعلّق وعرض "حصل خطأ، حاول مجددًا". ضغطت Withdraw تاني، طلع لك الـ 1000 وكشف الحساب نزل 2000. ده بالظبط اللي بيحصل في APIs بدون Idempotency.
الحل اللي البنوك بتعمله: كل عملية ليها reference number فريد. لو عيّدت العملية بنفس الرقم، الجهاز بيقولك "العملية دي اتنفّذت قبل كده، خد ايصالها" بدل ما يخصم تاني. الـ reference number ده هو الـ Idempotency Key.
التعريف العلمي الدقيق
Idempotent function في الرياضيات هي دالة f بتحقق f(f(x)) = f(x). تطبيق نفس العملية مرة أو ألف مرة بنفس الـ input بيرجّع نفس الـ output ومش بيغيّر state النظام أكتر من مرة.
في HTTP، الـ RFC 9110 §9.2.2 بيعرّف GET و PUT و DELETE كـ idempotent بطبيعتها. POST مش idempotent بشكل افتراضي — بس ممكن نخلّيه idempotent بإضافة header اسمه Idempotency-Key (موصوف في IETF draft draft-ietf-httpapi-idempotency-key-header).
الفكرة: السيرفر يخزّن نتيجة أول مرة شاف فيها الـ key، وأي طلب بنفس الـ key بعد كده بيرجّع نفس النتيجة المحفوظة بدون إعادة تنفيذ.
الحل: Idempotency Key Pattern في 4 خطوات
- الـ client بيولّد UUID v4 عشوائي قبل ما يبعت الطلب ويحطّه في header
Idempotency-Key. - السيرفر بيجرّب يحجز الـ key في Redis بـ
SETNX(set-if-not-exists) مع TTL = 24 ساعة.