03 अक्टू॰ 2025·8 मिनट

बाहरी बनाम आंतरिक डेटा — Pat Helland के ऐप सबक

Pat Helland के "बाहरी बनाम आंतरिक डेटा" सिद्धांत से सीखें कि स्पष्ट सीमाएँ कैसे तय करें, idempotent कॉल्स कैसे डिजाइन करें, और नेटवर्क फेल होते समय स्टेट को कैसे reconcile करें।

बाहरी बनाम आंतरिक डेटा — Pat Helland के ऐप सबक

“बाहर बनाम अंदर” साधारण भाषा में क्या मतलब है

जब आप एक ऐप बनाते हैं, तो आसानी से यह सोचना शुरू कर देते हैं कि अनुरोध व्यवस्थित रूप से, एक-एक कर, सही क्रम में पहुंचते हैं। असल नेटवर्क वैसा नहीं होता। यूज़र ने स्क्रीन फ्रीज़ होने पर "Pay" दो बार दबा दिया। मोबाइल कनेक्शन बटन दबाने के ठीक बाद कट गया। एक वेबहुक देर से आया, या दो बार आया। कभी-कभी वह बिल्कुल नहीं आता।

Pat Helland का विचार डेटा on the outside vs inside इस सभ्भर समस्या को सोचने का साफ़ तरीका देता है।

"बाहर" कैसा दिखता है

"बाहर" वह सब है जिसे आपका सिस्टम नियंत्रित नहीं करता। यह वह जगह है जहाँ आप अन्य लोगों और सिस्टम्स से बात करते हैं, और जहाँ डिलीवरी अनिश्चित होती है: ब्राउज़र और मोबाइल ऐप से HTTP अनुरोध, कतारों से संदेश, थर्ड‑पार्टी वेबहुक्स (पेमेंट, ईमेल, शिपिंग), और क्लाइंट, प्रॉक्सी, या बैकग्राउंड जॉब्स द्वारा ट्रिगर किए गए retries।

बाहर पर मानें कि संदेश देर से आ सकते हैं, डुप्लिकेट हो सकते हैं, या गलत क्रम में आ सकते हैं। भले ही कुछ "आम तौर पर विश्वसनीय" दिखे, उस दिन के लिए डिजाइन करें जब वह भरोसेमंद न रहे।

"अंदर" का मतलब क्या है

"अंदर" वह है जिसे आपका सिस्टम भरोसेमंद बना सकता है। यह वह टिकाऊ स्टेट है जिसे आप स्टोर करते हैं, वे नियम जो आप लागू करते हैं, और वे तथ्य जिन्हें आप बाद में साबित कर सकते हैं:

  • डेटाबेस रिकॉर्ड और उनका इतिहास
  • बिज़नेस नियम (उदाहरण: "एक ऑर्डर केवल एक बार ही भुगतान किया जा सकता है")
  • एक स्त्रोत-ऑफ‑ट्रूथ स्थिति के लिए (pending, paid, canceled)

अंदर वह जगह है जहाँ आप इनवरिएंट्स की रक्षा करते हैं। अगर आप "प्रति ऑर्डर एक भुगतान" का वादा करते हैं, तो वह वादा अंदर लागू होना चाहिए, क्योंकि बाहर पर भरोसा नहीं किया जा सकता।

मनस्थितिपरिवर्तन सरल है: परफेक्ट डिलीवरी या परफेक्ट टाइमिंग मानकर मत चलिए। हर बाहरी इंटरैक्शन को एक अविश्वसनीय सुझाव की तरह ट्रीट करें जो दोहराया जा सकता है, और अंदर को सुरक्षित रूप से प्रतिक्रिया देने के काबिल बनाइये।

यह छोटे टीमों और सरल ऐप्स के लिए भी मायने रखता है। पहला नेटवर्क गड़बड़ जो डुप्लिकेट चार्ज या अटका हुआ ऑर्डर बनाता है, वह सैद्धांतिक से निकलकर रिफंड, सपोर्ट टिकट और भरोसे का नुकसान बन जाता है।

एक ठोस उदाहरण: एक यूज़र "Place order" दबाता है, ऐप एक अनुरोध भेजता है, और कनेक्शन कट जाता है। यूज़र फिर से कोशिश करता है। अगर आपके अंदर यह पहचानने का तरीका नहीं है कि "यह वही कोशिश है", तो आप दो ऑर्डर बना सकते हैं, इन्वेंटरी दो बार रिज़र्व कर सकते हैं, या दो कन्फर्मेशन्स ईमेल कर सकते हैं।

Pat Helland से मुख्य सबक

Helland का बिंदु सीधा है: बाहरी दुनिया अनिश्चित है, लेकिन आपके सिस्टम के अंदर चीज़ें सुसंगत रहनी चाहिए। नेटवर्क पैकेट्स खो देता है, फ़ोन सिग्नल खो देते हैं, क्लॉक्स drift करते हैं, और यूज़र्स refresh दबाते हैं। आपका ऐप इन चीज़ों को कंट्रोल नहीं कर सकता। जो वह नियंत्रित कर सकता है वह है कि जब डेटा एक साफ़ बॉर्डर पार कर जाए तो आप उसे "सत्य" के रूप में क्या स्वीकार करते हैं।

एक रोज़मर्रा के पल में समय और अनिश्चितता

किसी को मोबाइल से कॉफ़ी ऑर्डर करते हुए कल्पना कीजिए जब बिल्डिंग में Wi‑Fi खराब है। वे "Pay" दबाते हैं। स्पिनर चलता है। नेटवर्क कट जाता है। वे फिर से दबाते हैं।

शायद पहला अनुरोध आपके सर्वर तक पहुँचा, पर रिस्पॉन्स वापस नहीं आया। या शायद कोई भी अनुरोध नहीं पहुँचा। यूज़र की नज़रों में दोनों संभावनाएँ एक जैसी दिखती हैं।

यही समय और अनिश्चितता है: आप अभी तक नहीं जानते कि क्या हुआ, और बाद में आपको पता चल सकता है। आपका सिस्टम इंतज़ार के दौरान समझदारी से व्यव्हार करना चाहिए।

Retries, duplicates, और reordering

एक बार आप यह स्वीकार कर लेते हैं कि बाहर अविश्वसनीय है, तो कुछ "अजीब" व्यवहार सामान्य दिखने लगते हैं:

  • Retries डुप्लिकेट बनाते हैं (दो "Pay" अनुरोध)।
  • संदेश गलत क्रम में पहुँचते हैं ("cancel" पहले आ जाए, फिर "pay")।
  • एक अनुरोध प्रोसेस हो चुका है, पर क्लाइंट ने कभी रिस्पॉन्स नहीं देखा।

बाहरी डेटा एक दावा है, तथ्य नहीं। "मैंने भुगतान किया" एक बयान है जो एक अविश्वसनीय चैनल के ऊपर भेजा गया। वह तथ्य तब बनता है जब आप उसे अंदर अपनी प्रणाली में टिकाऊ, सुसंगत तरीके से रिकॉर्ड कर लेते हैं।

यह आपको तीन व्यावहारिक आदतों की ओर ले जाता है: स्पष्ट सीमाएँ परिभाषित करें, retries को idempotency से सुरक्षित बनाएं, और जब वास्तविकता मेल न खाए तो reconciliation की योजना बनाएं।

स्पष्ट सीमाएँ: आपका सिस्टम क्या मालिक है और क्या नहीं

"बाहर बनाम अंदर" विचार एक व्यावहारिक प्रश्न से शुरू होता है: आपकी सिस्टम की सच्चाई कहाँ से शुरू और कहाँ खत्म होती है?

बॉर्डर के अंदर आप मजबूत गारंटी दे सकते हैं क्योंकि आप डेटा और नियमों को नियंत्रित करते हैं। बॉर्डर के बाहर आप सर्वश्रेष्ठ‑प्रयास करते हैं और मानते हैं कि संदेश खो सकते हैं, डुप्लिकेट हो सकते हैं, देर से आ सकते हैं, या गलत क्रम में पहुँच सकते हैं।

वास्तविक ऐप्स में यह बॉर्डर अक्सर इन जगहों पर दिखता है:

  • एक API endpoint जो आपकी डेटाबेस में रिकॉर्ड लिखता है
  • एक queue consumer जो इवेंट को एक स्टोर किए गए परिवर्तन में बदलता है
  • एक callback handler जो प्रोवाइडर ने क्या हुआ रिकॉर्ड करता है
  • एक sender जो आपके अपने स्टेट को कमिट करने के बाद किसी दूसरे सिस्टम को नोटिफाई करता है

एक बार यह रेखा खींचने के बाद, तय करें कि किन इनवरिएंट्स को अंदर गैर-परक्राम्य (non-negotiable) माना जाएगा। उदाहरण:

  • एक order ID आपकी डेटाबेस में यूनिक है।
  • एक बैलेंस कभी निगेटिव नहीं होता।
  • एक स्टेट केवल आगे बढ़ता है (created -> paid -> shipped)।
  • हर एक्सटर्नल रिक्वेस्ट जिसे आप स्वीकार करते हैं उसका एक स्टोर किया गया ऑडिट ट्रेल होता है।

बॉर्डर को यह भी स्पष्ट भाषा चाहिए कि "हम कहाँ हैं"। बहुत सी विफलताएँ "हमने आपको सुना" और "हमने इसे पूरा कर लिया" के बीच के गैप में रहती हैं। एक सहायक पैटर्न तीन अर्थ अलग करने का है:

  • Received: संदेश आपके एज पर आया (लाज़मी नहीं कि वह अभी सेव हुआ है)
  • Accepted: आपने उसे सेव कर लिया और बाद में सुरक्षित रूप से काम retry कर सकते हैं
  • Processed: इच्छित काम पूरा हुआ और आपने परिणाम रिकॉर्ड कर लिया

जब टीमें इसे स्किप कर देती हैं, तो वे ऐसे बग बनाते हैं जो केवल लोड के दौरान या आंशिक आउटेज के समय होते हैं। एक सिस्टम "paid" का अर्थ पैमेंट कैप्चर समझता है; दूसरा इसे पेमेंट प्रयास शुरू होने के रूप में लेता है। वह mismatch डुप्लिकेट, अटके ऑर्डर और सपोर्ट टिकट बनाता है जिन्हें कोई दोहरा नहीं पाता।

Idempotency: retries को सुरक्षित बनाना

Idempotency का मतलब है: अगर वही अनुरोध दो बार भेजा गया तो सिस्टम उसे एक ही अनुरोध के रूप में ट्रीट करे और वही आउटकम लौटाए।

Retries सामान्य हैं। टाइमआउट होते हैं। क्लाइंट खुद को रिपीट करते हैं। अगर बाहर बार‑बार कर सकता है, तो अंदर को उसे स्थिर स्टेट परिवर्तन में बदलना होगा।

एक सरल उदाहरण: एक मोबाइल ऐप "pay $20" भेजता है और कनेक्शन कट जाता है। ऐप फिर retry करता है। बिना idempotency के ग्राहक से दो बार चार्ज हो सकता है। idempotency के साथ, दूसरा अनुरोध पहले चार्ज का ही परिणाम लौटाता है।

idempotency लागू करने के सामान्य तरीके

अधिकांश टीमें इनमें से एक पैटर्न का उपयोग करती हैं (कभी-कभी मिश्रित):

  • Idempotency key: क्लाइंट हर उद्देश्य के लिए एक यूनिक की भेजता है (उदाहरण: Idempotency-Key: ...)। सर्वर उस की और अंतिम रिस्पॉन्स को रिकॉर्ड करता है।
  • De-duplication table: (client_id, key) या (order_id, operation) से की गई एक पंक्ति स्टोर करें और दूसरे साइड‑इफेक्ट को मना कर दें।
  • Natural keys: एक बिज़नेस आइडेंटिफायर का उपयोग करें जो पहले से यूनिक है, ताकि "create payment" केवल एक बार मौजूद हो सके।

जब एक डुप्लिकेट आता है, तो सबसे अच्छा व्यवहार आम तौर पर "409 conflict" या एक सामान्य एरर नहीं होता। यह वही परिणाम लौटाना है जो आपने पहली बार लौटाया था, उसी रीसोर्स ID और स्टेटस सहित। वही क्लाइंट और बैकग्राउंड जॉब्स के लिए retries को सुरक्षित बनाता है।

रिकॉर्ड कहाँ रखें (और कितनी देर)

Idempotency रिकॉर्ड आपकी बॉर्डर के अंदर टिकाऊ स्टोरेज में होना चाहिए, मेमोरी में नहीं। अगर आपकी API रिस्टार्ट है और भूल जाती है, तो सुरक्षा गारंटी चली जाएगी।

रिकॉर्ड को उतना समय रखें जितना रीअलिस्टिक retries और देर से डिलीवरी कवर करें। विंडो बिज़नेस रिस्क पर निर्भर है: कम‑जोखिम वाले create के लिए मिनट से घंटे, पेमेंट/ईमेल/शिपमेंट जैसे महंगे मामलों के लिए दिन, और अगर पार्टनर विस्तारित अवधि के लिए retry कर सकते हैं तो और लंबा।

"डिस्ट्रिब्यूटेड ट्रांज़ैक्शन" के जाल से कैसे बचें

मोबाइल retries को पूर्वानुमेय बनाएं
एक Flutter क्लाइंट बनाएं जो स्थिर अनुरोध IDs के साथ सुरक्षित रूप से retries करे।

डिस्ट्रिब्यूटेड ट्रांज़ैक्शन सुखदायक लगते हैं: सर्विसेज़, कतारों और डेटाबेस में एक बड़ा कमिट। व्यवहार में वे अक्सर अप्राप्य, धीमे, या बहुत नाजुक होते हैं। एक बार नेटवर्क हॉप शामिल हुआ, आप यह नहीं मान सकते कि सब कुछ साथ में कमिट होगा।

एक आम जाल वह workflow बनाना है जो तभी काम करे जब हर स्टेप अभी सफल हो: save order, charge card, reserve inventory, send confirmation। अगर स्टेप 3 टाइमआउट हो जाए, तो क्या वह फेल हुआ या सफल? यदि आप retry करते हैं, तो क्या आप डबल‑चार्ज या डबल‑रिज़र्व कर देंगे?

दो व्यावहारिक दृष्टिकोण इससे बचते हैं:

  • Outbox/inbox: अपने स्टेट परिवर्तन के साथ ही उसी ट्रांज़ैक्शन में एक durable intent (आउटबॉक्स रो) लिखें, फिर एक वर्कर संदेश भेजे। रिसीविंग साइड पर, एक inbox रखें जो संदेश ID से की‑पुंजी पर सुरक्षित हो ताकि एक ही संदेश फिर से आने पर भी सुरक्षा रहे।
  • Saga-style steps with compensations: workflow को छोटे स्टेप्स में तोड़ें जो स्वतंत्र रूप से पूरे हो सकें। अगर बाद का स्टेप फेल हो, तो इतिहास रोल‑बैक करने की कोशिश करने की बजाय एक compensating action चलाएँ (उदाहरण: इन्वेंटरी रिलीज़ करना या बिना भुगतान वाले ऑर्डर को रद्द करना)।

प्रति workflow एक स्टाइल चुनें और उसी के साथ बने रहें। कभी‑कभी "हम आउटबॉक्स करते हैं" और कभी‑कभी "हम synchronous सफलता मानते हैं" मिलाने से ऐसे एज‑केस बनते हैं जिन्हें टेस्ट करना मुश्किल होता है।

एक सरल नियम मददगार है: अगर आप सीमाओं के पार एटॉमिक कमिट नहीं कर सकते, तो retries, duplicates, और delays के लिए डिजाइन करें।

समायोजन (Reconciliation): असहमतियों से कैसे उबरें

Reconciliation एक बुनियादी सत्य स्वीकारना है: जब आपका ऐप नेटवर्क के ऊपर अन्य सिस्टम्स से बात करता है, तो कभी‑कभी आप यह टाल नहीं पाएंगे कि क्या हुआ इस पर असहमति हो जाएगी। अनुरोध टाइमआउट होते हैं, callback देर से आते हैं, और लोग क्रियाओं को फिर से करते हैं। Reconciliation वह तरीका है जिससे आप mismatches का पता लगाते हैं और उन्हें समय के साथ ठीक करते हैं।

बाहरी सिस्टम्स को स्वतंत्र सत्य के स्रोत के रूप में ट्रीट करें। आपका ऐप अपनी आंतरिक रिकॉर्ड रखता है, पर उसे यह तुलना करने का तरीका चाहिए कि साझेदारों, प्रोवाइडरों और उपयोगकर्ताओं ने वास्तव में क्या किया।

सामान्य reconciliation तंत्र

अधिकांश टीमें कुछ नीरस उपकरणों का उपयोग करती हैं (नीरस अच्छा है): एक वर्कर जो pending क्रियाओं को फिर से कोशिश करता और बाहरी स्थिति फिर से जांचता है, असंगतियों के लिए शेड्यूल्ड स्कैन, और सपोर्ट के लिए एक छोटा एडमिन रिपेयर एक्शन ताकि retry, cancel, या mark as reviewed किया जा सके।

क्या तुलना करनी चाहिए और क्या रिकॉर्ड करना चाहिए

रिकंसाइलेशन तभी काम करती है जब आप जानते हों कि क्या तुलना करनी है: आंतरिक लेजर बनाम प्रोवाइडर लेजर (payments), ऑर्डर स्टेट बनाम शिपमेंट स्टेट (fulfillment), सब्सक्रिप्शन स्टेट बनाम बिलिंग स्टेट।

स्टेट्स को मरम्मत‑योग्य बनाएं। सीधे "created" से "completed" पर न कूदें, होल्डिंग स्टेट्स जैसे pending, on hold, या needs review का उपयोग करें। इससे यह कहना सुरक्षित होता है "हमें अभी यकीन नहीं है", और reconciliation के लिए एक स्पष्ट जगह मिलती है।

महत्वपूर्ण परिवर्तनों पर एक छोटा ऑडिट ट्रेल कैप्चर करें:

  • आपने कब अनुरोध भेजा और आख़िरी बार कब सुनने को मिला
  • correlation IDs जो आपकी रिकॉर्ड को बाहरी इवेंट/संदर्भ से जोड़ते हैं
  • आख़िरी ज्ञात बाहरी स्थिति (और वह कहाँ से आई)
  • मैन्युअल ओवरराइड के लिए एक कारण फ़ील्ड (कौन, क्या, क्यों)

उदाहरण: अगर आपका ऐप शिपमेंट लेबल माँगता है और नेटवर्क कट जाता है, तो आंतरिक रूप से आपके पास "no label" रह सकता है जबकि कैरियर ने वास्तव में एक बना दिया हो। एक recon वर्कर correlation ID से खोज कर सकता है, लेबल मौजूद पाए और ऑर्डर को आगे बढ़ा सकता है (या यदि विवरण मेल नहीं खाते तो रिव्यू के लिए मार्क कर सकता है)।

चरण दर चरण: ऐसा workflow डिज़ाइन करना जो नेटवर्क फेलियर जित सके

डुप्लिकेट के बिना वेबहुक संभालें
डुप्लिकेट से बचने के लिए वेबहुक हैंडलर बनाएं, स्टोर किए गए इवेंट IDs और ऑडिट ट्रेल के साथ।

एक बार आप स्वीकार कर लें कि नेटवर्क फेल होगा, लक्ष्य बदल जाता है। आप हर स्टेप को एक ही बार में सफल बनाने की कोशिश नहीं कर रहे; आप हर स्टेप को फिर से चलाने के लिए सुरक्षित और मरम्मत करने में आसान बनाना चाहते हैं।

एक व्यावहारिक वर्कफ़्लो

  1. एक वाक्य की बॉर्डर स्टेटमेंट लिखें। स्पष्ट हों कि आपका सिस्टम क्या मालिक है (source of truth), क्या वह नकल करता है, और क्या वह केवल दूसरों से मांगता है।

  2. हैप्पी पाथ से पहले विफलता मोड सूचीबद्ध करें। कम से कम: टाइमआउट (आप नहीं जानते कि यह काम कर गया), डुप्लिकेट अनुरोध, आंशिक सफलता (एक स्टेप हुआ, अगला नहीं हुआ), और आउट‑ऑफ‑ऑर्डर इवेंट्स।

  3. प्रत्येक इनपुट के लिए एक idempotency रणनीति चुनें। सिंक्रोनस APIs के लिए यह अक्सर idempotency key और स्टोर किए गए परिणाम होते हैं। संदेशों/इवेंट्स के लिए यह आम तौर पर एक यूनिक मैसेज ID और "क्या मैंने इसे प्रोसेस किया?" रिकॉर्ड होता है।

  4. इरादा (intent) को पर्सिस्ट करें, फिर कार्य करें। पहले कुछ टिकाऊ स्टोर करें जैसे "PaymentAttempt: pending" या "ShipmentRequest: queued", फिर बाहरी कॉल करें, फिर परिणाम स्टोर करें। एक स्थिर संदर्भ ID लौटाएँ ताकि retries उसी intent की ओर इशारा करें न कि नया बनाने के लिए।

  5. reconciliation और एक मरम्मत पथ बनाएं, और उन्हें दिखाई देने योग्य बनाएं। Reconciliation एक जॉब हो सकती है जो "बहुत देर से pending" रिकॉर्ड्स को स्कैन कर बाहरी सिस्टम्स को फिर से चेक करे। मरम्मत पथ एक सुरक्षित एडमिन एक्शन हो सकता है जैसे "retry", "cancel", या "mark resolved", साथ में एक ऑडिट नोट। मूलभूत ऑब्ज़रवबिलिटी जोड़ें: correlation IDs, स्पष्ट स्टेट फ़ील्ड्स, और कुछ काउंट्स (pending, retries, failures)।

उदाहरण: यदि चेकआउट का टाइमआउट ठीक उसी समय हुआ जब आप पेमेंट प्रोवाइडर को कॉल कर रहे थे, तो अनुमान न लगाएँ। प्रयास स्टोर करें, attempt ID लौटाएँ, और उपयोगकर्ता को उसी idempotency key के साथ retry करने दें। बाद में reconciliation पुष्टि कर सकती है कि प्रोवाइडर ने चार्ज किया या नहीं और प्रयास को डबल‑चार्ज किए बिना अपडेट कर सकती है।

उदाहरण परिदृश्य: retries और देर से callbacks वाला एक ऑर्डर फ्लो

एक ग्राहक "Place order" दबाता है। आपकी सेवा प्रोवाइडर को पेमेंट अनुरोध भेजती है, पर नेटवर्क फ़्लैकी है। प्रोवाइडर की अपनी सच्चाई है, और आपकी डेटाबेस की भी। वे तब तक ड्रीफ्ट करेंगे जब तक आप डिज़ाइन करके उन्हें सिंक नहीं रखते।

बाहर पर क्या होता है (इवेंट्स जिन्हें आप नियंत्रित नहीं करते)

आपकी नज़र में बाहर संदेशों का एक स्ट्रीम है जो देर से, दोहराया हुआ, या अनुपस्थित हो सकता है:

  • "Submit order" आपकी API पर आता है।
  • आपका पेमेंट अनुरोध प्रोवाइडर को जाता है।
  • प्रोवाइडर एक वेबहुक भेजता है जो कहता है "authorized"।
  • प्रोवाइडर वेबहुक को फिर से भेजता है और वही callback फिर आता है।
  • आपका क्लाइंट टाइमआउट हो जाता है और "Place order" को फिर से भेजता है।

इनमें से किसी भी स्टेप की गारंटी "एक बार बिल्कुल" नहीं होती। वे केवल "शायद" की गारंटी देते हैं।

अंदर आप क्या रखते हैं (रिकॉर्ड्स जिन्हें आप नियंत्रित करते हैं)

अपनी बॉर्डर के अंदर टिकाऊ तथ्य और बाहरी इवेंट्स को उन तथ्यों से जोड़ने के लिए न्यूनतम जानकारी स्टोर करें।

जब ग्राहक पहली बार ऑर्डर करता है, तो order रिकॉर्ड एक स्पष्ट स्टेट जैसे pending_payment में बनाइए। साथ ही एक payment_attempt रिकॉर्ड बनाइए जिसमें एक यूनिक प्रोवाइडर संदर्भ और ग्राहक क्रिया से जुड़ा idempotency_key हो।

अगर क्लाइंट टाइमआउट और फिर retry करता है, तो आपकी API को दूसरा ऑर्डर नहीं बनाना चाहिए। उसे idempotency_key देख कर वही order_id और वर्तमान स्टेट लौटाना चाहिए। यही एक चुनाव नेटवर्क फेलियर्स पर डुप्लिकेट से बचाता है।

अब वेबहुक दो बार आता है। पहला callback payment_attempt को authorized पर अपडेट करता है और ऑर्डर को paid में ले जाता है। दूसरा callback वही हैंडलर हिट करता है, पर आप पहचान लेते हैं कि आपने उस प्रोवाइडर इवेंट को पहले ही प्रोसेस कर लिया था (प्रोवाइडर इवेंट ID स्टोर करके, या वर्तमान स्टेट चेक करके) और कुछ नहीं करते। आप फिर भी 200 OK लौट सकते हैं, क्योंकि परिणाम पहले से ही सच है।

अंत में, reconcilation गंदे मामलों को हैंडल करती है। अगर किसी विलंब के बाद ऑर्डर अभी भी pending_payment है, तो एक बैकग्राउंड जॉब स्टोर किए गए संदर्भ का उपयोग करते हुए प्रोवाइडर से क्वेरी करता है। अगर प्रोवाइडर कहता है "authorized" पर आपने वेबहुक मिस कर दिया था, तो आप अपने रिकॉर्ड अपडेट कर देंगे। अगर प्रोवाइडर कहता है "failed" पर आपने उसे paid मार्क कर दिया था, तो आप उसे review के लिए flag कर सकते हैं या compensating action जैसे refund ट्रिगर कर सकते हैं।

डुप्लिकेट और अटके स्टेट्स के कारण बनने वाली आम गलतियाँ

कोड लिखने से पहले स्टेट्स डिजाइन करें
कोड जनरेट करने से पहले Planning Mode में सीमाएँ, स्टेट और विफलता के रास्ते मैप करें।

अधिकांश डुप्लिकेट रिकॉर्ड और "अटके" वर्कफ़्लो इस बात से आते हैं कि आप बाहर हुई घटनाओं (एक अनुरोध आया, एक संदेश मिला) को उस चीज़ के साथ मिला देते हैं जो आपने अंदर टिकाऊ रूप से कमिट किया है।

एक क्लासिक विफलता: क्लाइंट "place order" भेजता है, आपका सर्वर काम शुरू करता है, नेटवर्क कट जाता है, और क्लाइंट फिर से कोशिश करता है। अगर आप प्रत्येक retry को पूरी तरह नया सत्य मानते हैं, तो आपको डबल चार्ज, डुप्लिकेट ऑर्डर, या कई ईमेल मिलेंगे।

आम कारण:

  • इनकमिंग रिक्वेस्ट पर बहुत जल्दी भरोसा करना: डेटाबेस कमिट होने से पहले ईमेल भेजना या "order created" लॉग करना असुरक्षित है।
  • रिट्राइज जो नई पंक्तियाँ बनाते हैं: हर प्रयास पर नया order ID जेनरेट करना बजाय retries को एक ही आउटपुट से मैप करने के।
  • "एक बार बिल्कुल" डिलीवरी मान लेना: कतारें और callback उस वादे को नहीं देतीं। डुप्लिकेट, देरी, और reorder होते हैं।
  • कोई स्थिर पहचानकर्ता नहीं: यदि आप जवाब नहीं दे सकते "क्या मैंने इस intent को पहले देखा है?", तो आप डुप्लिकेट रोक नहीं सकते।
  • सिर्फ सफलता/असफलता, मध्य स्टेट नहीं: बिना pending/awaiting स्टेट के, टाइमआउट रहस्य बन जाते हैं और यूज़र्स फिर से क्लिक करते हैं।

एक मुद्दा सब कुछ और खराब कर देता है: कोई ऑडिट ट्रेल नहीं। अगर आप फ़ील्ड ओवरराइट करते हैं और केवल नवीनतम स्टेट रखते हैं, तो आप वह सबूत खो देते हैं जिसकी आपको बाद में reconciliation के लिए जरूरत है।

एक अच्छा सैनीटी चेक है: "अगर मैं इस हैंडलर को दो बार चलाऊँ, क्या मुझे वही परिणाम मिलता है?" अगर उत्तर नहीं है, तो डुप्लिकेट एक दुर्लभ एज‑केस नहीं; यह गारंटीकृत समस्या है।

त्वरित चेकलिस्ट और व्यावहारिक अगले कदम

अगर आप एक चीज़ याद रखें: आपका ऐप तब भी सही रहना चाहिए जब संदेश देर से आएँ, दो बार आएँ, या बिल्कुल न आएँ।

इस चेकलिस्ट का उपयोग कमजोर बिंदुओं का पता लगाने के लिए करें इससे पहले कि वे डुप्लिकेट रिकॉर्ड, गायब अपडेट, या अटके वर्कफ़्लो बन जाएँ:

  • Source of truth स्पष्ट है: प्रत्येक वर्कफ़्लो के लिए आप एक ऐसी जगह बता सकते हैं जो "सत्य" है (अक्सर आपकी डेटाबेस)।
  • हर लिखाई को सुरक्षित रूप से retry किया जा सकता है: प्रत्येक कमांड/API कॉल के पास idempotency key (या एक प्राकृतिक यूनिक की) है।
  • स्थिर IDs और correlation IDs एंड‑टू‑एंड मौजूद हैं: आप एक बिज़नेस एक्शन को लॉग्स, टेबल्स और callbacks में ट्रेस कर सकते हैं।
  • Reconciliation स्वचालित रूप से चलता है: आप नियमित रूप से "हम क्या मानते हैं" बनाम "क्या हुआ" की तुलना करते हैं और मरम्मत करते हैं या स्पष्ट अलर्ट उठाते हैं।
  • Rollback स्टेट को दूषित नहीं करता: स्टेट परिवर्तन ऑडिटेबल हैं और वर्ज़न्स के पार संगत हैं।

अगर आप इनमें से किसी का तुरंत जवाब नहीं दे सकते, तो यह अक्सर बताता है कि कोई बॉर्डर धुंधला है या कोई स्टेट ट्रांज़िशन गायब है।

प्रायोगिक अगले कदम:

  1. पहले बॉर्डर और स्टेट्स का स्केच बनाएं। प्रत्येक वर्कफ़्लो के लिए एक छोटा स्टेट सेट परिभाषित करें (उदाहरण: Created, PaymentPending, Paid, FulfillmentPending, Completed, Failed)।

  2. सबसे अधिक जोखिम वाले लिखावों पर idempotency जोड़ें। सबसे पहले high-risk writes: create order, capture payment, issue refund। PostgreSQL में idempotency कीज़ को unique constraint के साथ स्टोर करना एक सुरक्षित शुरुआत है।

  3. reconciliation को एक सामान्य फीचर मानें। एक जॉब शेड्यूल करें जो "बहुत देर से pending" रिकॉर्ड्स को खोजे, बाहरी सिस्टम्स को फिर से चेक करे, और लोकल स्टेट की मरम्मत करे।

  4. सुरक्षित रूप से iterate करें। ट्रांज़िशन और retry नियम समायोजित करें, फिर उसी अनुरोध को जानबूझकर दोबारा भेजकर और उसी इवेंट को फिर से प्रोसेस करके टेस्ट करें।

यदि आप जल्दी बना रहे हैं किसी चैट‑ड्रिवन प्लेटफ़ॉर्म जैसे Koder.ai (koder.ai) पर, तब भी इन नियमों को अपने जनरेट किए हुए सर्विसेज़ में शुरुआत में शामिल करना उपयोगी है: गति ऑटोमेशन से आती है, पर विश्वसनीयता स्पष्ट सीमाओं, idempotent हैंडलर्स, और reconciliation से आती है।

अक्सर पूछे जाने वाले प्रश्न

What does “data on the outside vs inside” mean in simple terms?

"Outside" वह सब कुछ है जो आपका सिस्टम नियंत्रित नहीं करता: ब्राउज़र, मोबाइल नेटवर्क, कतारें, थर्ड‑पार्टी वेबहुक्स, retries और टाइमआउट। मानकर चलें कि संदेश देर से आ सकते हैं, डुप्लिकेट हो सकते हैं, खो सकते हैं, या गलत क्रम में आ सकते हैं。

"Inside" वह है जो आप नियंत्रण में रखते हैं: आपका संग्रहीत स्टेट, आपके नियम, और वे तथ्य जिन्हें आप बाद में प्रमाणित कर सकते हैं (आम तौर पर आपकी डेटाबेस में)।

Why can’t I trust incoming requests or webhooks to happen exactly once?

क्योंकि नेटवर्क आपको सच नहीं बताता।

क्लाइंट का टाइमआउट यह नहीं दर्शाता कि सर्वर ने अनुरोध प्रोसेस किया या नहीं। एक वेबहुक दो बार आना यह नहीं दर्शाता कि प्रोवाइडर ने क्रिया दो बार की। अगर आप हर संदेश को "नया सत्य" मानते हैं, तो आप डुप्लिकेट ऑर्डर, डबल चार्ज और अटके वर्कफ़्लो बना देंगे।

Where should I draw the “boundary” in a typical app?

एक स्पष्ट बॉन्ड्री वह बिंदु है जहाँ एक अविश्वसनीय संदेश एक टिकाऊ तथ्य बन जाता है।

आम बॉन्ड्री के उदाहरण:

  • एक API endpoint जो आपकी डेटाबेस में कमिट करता है
  • एक queue consumer जो इवेंट को स्टेट परिवर्तन के रूप में स्टोर करता है
  • एक webhook handler जो प्रोवाइडर का दावा रिकॉर्ड करता है

एक बार डेटा बॉर्डर पार कर जाए, आप अंदर इनवरिएंट्स लागू करते हैं (जैसे "ऑर्डर केवल एक बार भुगतान हो सकता है").

How do I stop double charges when users retry “Pay”?

इडेम्पोटेंसी का मतलब: अगर वही अनुरोध दो बार भेजा गया तो सिस्टम उसे एक ही अनुरोध की तरह ट्रीट करे और वही परिणाम लौटाए।

व्यावहारिक पैटर्न:

  • क्लाइंट हर क्रिया के लिए एक idempotency key भेजे
  • सर्वर उस key और अंतिम परिणाम को टिकाऊ स्टोरेज में रखे
  • डुप्लिकेट पर वही resource ID/status पहले की तरह लौटाएँ
Where do I store idempotency records, and how long should I keep them?

इसे केवल मेमोरी में न रखें। अपनी बॉर्डर के अंदर स्टोर करें (उदाहरण: PostgreSQL) ताकि सर्विस रेस्टार्ट पर सुरक्षा न भुल जाए।

रिटेंशन का नियम:

  • कम जोखिम वाली क्रियाएँ: मिनट से घंटे
  • महंगी क्रियाएँ (payments, refunds, shipments, emails): दिनों या उससे अधिक

इसे इतना रखें कि वास्तविक retries और देरी के दौरान कवर हो सके।

What states should I add to avoid “we’re not sure” bugs?

अनिश्चितता स्वीकार करने वाले स्टेट्स का प्रयोग करें।

साधारण सेट:

  • pending_* (हमने intent स्वीकार कर लिया है पर परिणाम नहीं जानते)
  • succeeded / failed (हमने अंतिम परिणाम रिकॉर्ड कर लिया)
  • needs_review (मismatch मिला है और मानव या विशेष जॉब की जरूरत है)

यह टाइमआउट के दौरान अनुमान लगाने से रोकता है और reconciliation आसान बनाता है।

Why are distributed transactions usually a trap for app workflows?

क्योंकि आप नेटवर्क के पार कई सिस्टमों में एक साथ एटॉमिक कमिट नहीं कर सकते।

यदि आप "save order → charge card → reserve inventory" को सिंक्रोनसली करते हैं और किसी स्टेप का टाइमआउट हो जाता है तो यह स्पष्ट नहीं रहेगा कि वह स्टेप सफल हुआ या नहीं। रिट्राई करने पर डुप्लिकेट हो सकते हैं; न करने पर कार्य अधूरा रह सकता है।

आउटबॉक्स/इनबॉक्स या सागा-शैली compensation चुनें; दोनों ही partial success के लिए डिजाइन हैं।

What is the outbox/inbox pattern, and when should I use it?

आउटबॉक्स/इनबॉक्स पैटर्न नेटवर्क की अशुद्धियों का नाटक किए बिना क्रॉस-सिस्टम मैसेजिंग को विश्वसनीय बनाता है।

  • Outbox: अपनी स्टेट परिवर्तन के साथ ही उसी ट्रांज़ैक्शन में एक आउटबॉक्स रो लिखें जो भेजे जाने वाले संदेश का प्रतिनिधित्व करे।
  • एक वर्कर आउटबॉक्स पढ़कर संदेश भेजता है।
  • Inbox (रिसीवर साइड): प्रोसेस किए गए मैसेज IDs स्टोर करें ताकि re-deliveries साइड-इफेक्ट्स न बनाएं।
What is reconciliation, and what’s a simple way to implement it?

रिकंसाइलेशन यह मानकर चलता है कि आपके और किसी बाहरी सिस्टम के बीच कभी-कभी असहमति होगी।

अच्छे डिफ़ॉल्ट्स:

  • एक शेड्यूल्ड जॉब जो "बहुत समय से pending" आइटम्स को फिर से चेक करे
  • तुलना चरण (आपका स्टेट बनाम प्रोवाइडर स्टेट)
  • एक मरम्मत कार्रवाई: retry, cancel, refund, या needs_review मार्क करना

यह भुगतान, fulfillment, सब्सक्रिप्शन या किसी भी वेबहुक वाले काम के लिए अनिवार्य है।

Does this still matter if I’m building quickly with a platform like Koder.ai?

हाँ। तेज़ी से बनाना नेटवर्क फेलियर को नहीं हटाता—यह आपको बस जल्दी उस समस्या तक पहुँचा देता है।

यदि आप Koder.ai से सर्विस जनरेट कर रहे हैं, तो इन डिफ़ॉल्ट्स को शुरू में शामिल करें:

  • स्पष्ट बॉर्डर (कब intent टिकाऊ बनता है)
  • "create/capture/refund" जैसी क्रियाओं के लिए idempotent हैंडलर्स
  • बाहरी संदर्भों के साथ correlation IDs स्टोर करें
  • pending रिकॉर्ड्स के लिए reconciliation जॉब

इस तरह retries और डुप्लिकेट कॉलबैक महंगे नहीं बल्कि सामान्य बन जाते हैं।

Related posts

कर्मचारी ऑफबोर्डिंग ऐप: एक्सेस की कमियों को सुरक्षित तरीके से बंद करें

ऐसा कर्मचारी ऑफबोर्डिंग ऐप बनाएं जो उपकरण वापसी के काम सौंपे, उनकी स्थिति दर्ज करे और HR, मैनेजर तथा IT के अनुमोदन जुटाए।

स्टाफ के इस्तेमाल से पहले बिज़नेस ऐप के लिए वास्तविक जैसा टेस्ट डेटा

जानें कि बिज़नेस ऐप के लिए वास्तविक जैसे टेस्ट डेटा कैसे बनाएँ, स्टाफ की अनुमतियों का मॉडल कैसे तैयार करें, मुश्किल स्थितियों को कैसे परखें और लॉन्च से पहले गलत इनपुट कैसे पकड़ें।

शिफ्ट कर्मचारियों के लिए ऐप: साझा डिवाइस डिज़ाइन करने के सुझाव

साझा डिवाइस इस्तेमाल करने वाले शिफ्ट कर्मचारियों के लिए छोटे टास्क वर्कफ़्लो, भूमिका-आधारित एक्सेस, भरोसेमंद हैंडओवर और स्पष्ट स्टेटस अपडेट वाले ऐप डिज़ाइन करना सीखें।