PostgreSQL में आउटबॉक्स पैटर्न — भरोसेमंद API एकीकरण के लिए
PostgreSQL में आउटबॉक्स पैटर्न सीखें: इवेंट्स को सुरक्षित रूप से स्टोर करें और थर्ड-पार्टी APIs तक रे-ट्राई, ऑर्डरिंग और डिडुप्लिकेशन के साथ पहुंचाएँ।

क्यों एकीकरण असफल होते हैं जबकि आपकी एप ठीक काम करती है
यह आम है कि आपके ऐप में कोई कार्रवाई “सफल” दिखे जबकि पीछे का एकीकरण चुपचाप फेल हो गया हो। आपका डेटाबेस लिखना तेज़ और भरोसेमंद है। तीसरे पक्ष के API को कॉल इतना भरोसेमंद नहीं है। इससे दो अलग दुनिया बन जाती हैं: आपके सिस्टम के अनुसार बदलाव हुआ, लेकिन बाहरी सिस्टम ने कभी सुना ही नहीं।
एक सामान्य उदाहरण: ग्राहक ने ऑर्डर दिया, आपका ऐप उसे PostgreSQL में सेव कर देता है, और फिर शिपिंग प्रदाता को सूचित करने की कोशिश करता है। अगर प्रदाता 20 सेकंड के लिए टाइमआउट हो जाए और आपका अनुरोध हार मान ले, तो ऑर्डर सच है, लेकिन शिपमेंट कभी बनाया नहीं गया।
यूजर इसे भ्रमित करने वाला और असंगत व्यवहार अनुभव करते हैं। मिसिंग इवेंट्स ऐसा दिखते हैं कि “कहीं कुछ हुआ ही नहीं।” डुप्लिकेट इवेंट्स ऐसा दिखते हैं कि “मुझे दो बार चार्ज क्यों किया गया?” सपोर्ट टिम्स के लिए भी मुशकिल होती है क्योंकि यह बताना कठिन होता है कि समस्या आपके ऐप में थी, नेटवर्क में थी, या पार्टनर में।
रिप्रयट्स मदद करते हैं, लेकिन सिर्फ retries ही सहीपन की गारंटी नहीं देते। अगर आप टाइमआउट के बाद रीट्राय करते हैं, तो आप वही इवेंट दो बार भेज सकते हैं क्योंकि आपको नहीं पता कि पार्टनर ने पहला अनुरोध प्राप्त किया था या नहीं। अगर आप गलत क्रम में retries करते हैं, तो आप "Order shipped" भेज सकते हैं, उससे पहले कि "Order paid" पहुँचे।
ये समस्याएँ आमतौर पर सामान्य concurrency से आती हैं: कई वर्कर समानांतर में प्रोसेस कर रहे होते हैं, कई ऐप सर्वर एक साथ लिख रहे होते हैं, और “best effort” queues में लोड के साथ टाइमिंग बदलती है। फेल्यर के तरीके पूर्वानुमेय हैं: APIs डाउन या धीमे हो जाते हैं, नेटवर्क अनुरोध ड्रॉप कर देता है, प्रोसेस क्रैश हो जाते हैं, और retries duplicates बनाते हैं जब कुछ idempotency लागू न हो।
आउटबॉक्स पैटर्न इसलिए मौजूद है क्योंकि ये फेल्यर्स सामान्य हैं।
सीधे शब्दों में आउटबॉक्स पैटर्न क्या है
आउटबॉक्स पैटर्न सरल है: जब आपका ऐप कोई महत्वपूर्ण बदलाव करता है (जैसे ऑर्डर बनाना), तो वह उसी ट्रांज़ैक्शन में एक छोटा “भेजने के लिए इवेंट” रिकॉर्ड भी डेटाबेस तालिका में लिखता है। अगर डेटाबेस कमिट सफल होता है, तो आप जानते हैं कि बिजनेस डेटा और इवेंट रिकॉर्ड साथ मौजूद हैं।
इसके बाद, एक अलग वर्कर आउटबॉक्स तालिका पढ़कर उन इवेंट्स को तीसरे पक्ष के APIs को डिलीवर करता है। अगर कोई API धीमा है, डाउन है, या टाइमआउट कर रहा है, तो आपका मुख्य यूजर रिक्वेस्ट तब भी सफल रहता है क्योंकि वह बाहरी कॉल की प्रतीक्षा नहीं कर रहा होता।
यह उन अजीब स्थितियों से बचाता है जो तब मिलती हैं जब आप रिक्वेस्ट हैंडलर के अंदर API कॉल करते हैं:
- ऑर्डर सेव है, लेकिन API कॉल फेल हो गया।
- API कॉल सफल हुआ, लेकिन आपका ऐप ऑर्डर सेव करने से पहले क्रैश कर गया।
- यूजर रीट्राय करता है, और आप वही चीज़ दो बार भेज देते हैं।
आउटबॉक्स पैटर्न मुख्य रूप से खोए हुए इवेंट्स, आंशिक फेल्यर्स (डेटाबेस ठीक, बाहरी API नहीं), आकस्मिक डबल सेंड्स, और सुरक्षित retries (बिना अटकलबाज़ी के बाद फिर कोशिश करना) में मदद करता है।
यह सब कुछ ठीक नहीं करता। अगर आपका पेलोड गलत है, आपके बिजनेस नियम गलत हैं, या तीसरे पक्ष का API डेटा को ठुकरा देता है, तो आपको अभी भी वैलिडेशन, अच्छा एरर हैंडलिंग और फेल हुए इवेंट्स को जांचने व ठीक करने का तरीका चाहिए।
PostgreSQL में एक आउटबॉक्स तालिका डिज़ाइन करना
एक अच्छी आउटबॉक्स तालिका जानबूझकर साधारण होनी चाहिए। इसे लिखना आसान होना चाहिए, पढ़ना आसान होना चाहिए, और ग़लत इस्तेमाल करना मुश्किल होना चाहिए।
यहाँ एक व्यावहारिक बेसलाइन स्कीमा है जिसे आप अनुकूलित कर सकते हैं:
create table outbox_events (
id bigserial primary key,
aggregate_id text not null,
event_type text not null,
payload jsonb not null,
status text not null default 'pending',
created_at timestamptz not null default now(),
available_at timestamptz not null default now(),
attempts int not null default 0,
locked_at timestamptz,
locked_by text,
meta jsonb not null default '{}'::jsonb
);
आईडी चुनना
bigserial (या bigint) का उपयोग करने से ऑर्डरिंग सरल और इंडेक्स तेज़ रहते हैं। UUIDs सिस्टम-व्यापी अनन्यता के लिए अच्छे हैं, लेकिन वे निर्माण क्रम में सॉर्ट नहीं करते, जिससे पोलिंग कम पूर्वानुमेय हो सकती है और इंडेक्स भारी हो सकते हैं।
एक सामान्य समझौता यह है: ऑर्डरिंग के लिए id को bigint रखें, और अगर आपको सेवाओं के बीच साझा करने के लिए स्थिर पहचान चाहिए तो एक अलग event_uuid जोड़ें।
जिन इंडेक्सों की जरूरत होती है
आपका वर्कर हर दिन एक ही पैटर्न से क्वेरी करेगा। अधिकांश सिस्टम्स को चाहिए:
(status, available_at, id)जैसा इंडेक्स ताकि अगला pending इवेंट क्रम में जल्दी मिल सके।(locked_at)पर इंडेक्स अगर आप stale locks को समाप्त करने का प्लान करते हैं।(aggregate_id, id)जैसा इंडेक्स अगर आप कभी-कभी पर-aggregate क्रम में डिलिवर करते हैं।
पेलोड्स को स्थिर रखें
पेलोड्स को छोटा और प्रत्याशित रखें। रिसीवर को जो चाहिए वही स्टोर करें, अपनी पूरी पंक्ति नहीं। एक स्पष्ट वर्शन (उदा., meta में) जोड़ें ताकि आप फ़ील्ड्स को सुरक्षित रूप से आगे बढ़ा सकें।
रूटिंग और डिबगिंग संदर्भ जैसे tenant ID, correlation ID, trace ID, और एक dedup key के लिए meta का उपयोग करें। यह अतिरिक्त संदर्भ बाद में बहुत काम आता है जब सपोर्ट पूछे "इस ऑर्डर के साथ क्या हुआ?"।
बिजनेस लिखाई के साथ इवेंट्स को सुरक्षित रूप से कैसे स्टोर करें
सबसे महत्वपूर्ण नियम सरल है: बिजनेस डेटा और आउटबॉक्स इवेंट एक ही डेटाबेस ट्रांज़ैक्शन में लिखें। अगर ट्रांज़ैक्शन कमिट होता है, तो दोनों मौजूद हैं। अगर यह रोलबैक होता है, तो न तो डेटा और न ही इवेंट मौजूद होगा।
उदाहरण: ग्राहक ने ऑर्डर दिया। एक ही ट्रांज़ैक्शन में आप ऑर्डर रो, ऑर्डर आइटम्स, और एक आउटबॉक्स रो जैसे order.created डालते हैं। अगर कोई भी कदम फेल होता है, तो आप नहीं चाहेंगे कि एक “created” इवेंट दुनिया में निकल जाए।
एक इवेंट या कई?
जब तक संभव हो, प्रत्येक बिजनेस कार्रवाई के लिए एक इवेंट से शुरू करें। यह सोचने में आसान और प्रोसेस करने में सस्ता है। केवल तब कई इवेंट्स बनाएं जब अलग consumers सचमुच अलग टाइमिंग या पेलोड चाहते हों (उदा., fulfillment के लिए order.created और billing के लिए payment.requested)। एक क्लिक के लिए कई इवेंट्स जनरेट करने से retries, ordering सिरदर्द, और duplicate हैंडलिंग बढ़ जाती है।
कौन सा पेलोड स्टोर करें?
आम तौर पर आपको चुनना होता है:
- Snapshot: कार्रवाई के समय की प्रमुख फील्ड्स (order total, currency, customer ID) स्टोर करें। इससे बाद में अतिरिक्त रीड नहीं करनी पड़ती और संदेश स्थिर रहता है।
- Reference ID: केवल order ID स्टोर करें और वर्कर बाद में विवरण लोड करे। इससे आउटबॉक्स छोटा रहता है, लेकिन अतिरिक्त रीड्स लगती हैं और ऑर्डर एडिट होने पर बदल सकता है।
एक व्यावहारिक मध्यम मार्ग पहचानकर्ता और कुछ महत्वपूर्ण वैल्यू का छोटा स्नैपशॉट है। यह रिसीवर्स को तेज़ी से काम करने देता है और डिबग में मदद करता है।
ट्रांज़ैक्शन बॉउण्ड्री को तंग रखें। तीसरे पक्ष के APIs को उसी ट्रांज़ैक्शन के अंदर कॉल न करें।
तीसरे पक्ष के APIs को इवेंट डिलिवर करना: वर्कर लूप
एक बार इवेंट्स आपके आउटबॉक्स में आ जाएं, आपको एक वर्कर चाहिए जो उन्हें पढ़े और तीसरे पक्ष के API को कॉल करे। यही हिस्सा पैटर्न को एक विश्वसनीय एकीकरण बनाता है।
पोलिंग आमतौर पर सबसे सरल विकल्प है। LISTEN/NOTIFY लैटेंसी घटा सकता है, लेकिन यह चलने वाली चीज़ें बढ़ाता है और तब भी फॉलबैक चाहिए जब नोटिफिकेशन्स मिस हों या वर्कर रिस्टार्ट हो। अधिकांश टीमों के लिए, छोटे बैच के साथ स्थिर पोलिंग चलाना आसान और डिबग करने में बेहतर होता है।
रोज़ सुरक्षित रूप से क्लेम करना
वर्कर को रोज़ क्लेम करना चाहिए ताकि दो वर्कर्स एक ही इवेंट को एक साथ प्रोसेस न करें। PostgreSQL में आम तरीका है कि आप row locks और SKIP LOCKED का उपयोग करके एक बैच चुनें, फिर उन्हें इन-प्रोग्रेस के रूप में मार्क करें।
एक व्यावहारिक स्टेटस फ्लो यही है:
pending: भेजने के लिए तैयारprocessing: किसी वर्कर द्वारा लॉक किया गया (uselocked_byऔरlocked_at)sent: सफलतापूर्वक डिलिवर हुआfailed: अधिकतम प्रयासों के बाद रोका गया (या मैनुअल समीक्षा के लिए अलग रखा गया)
डेटाबेस के प्रति दयालु रहने के लिए बैच्स छोटे रखें। 10 से 100 रो का बैच, हर 1 से 5 सेकंड में चलाना एक सामान्य शुरुआती बिंदु है।
जब कॉल सफल हो, तो रो को sent मार्क करें। जब यह फेल हो, तो attempts बढ़ाएँ, available_at को भविष्य में सेट करें (backoff), लॉक साफ करें, और इसे pending में वापस ले आएं।
ऐसे लॉग्स जो मदद करें (बिना सीक्रेट लीक किए)
अच्छे लॉग्स फेल्यर्स को कार्रवाई योग्य बनाते हैं। आउटबॉक्स id, इवेंट टाइप, डेस्टिनेशन नाम, प्रयास गणना, टाइमिंग, और HTTP स्टेटस या एरर क्लास को लॉग करें। रिक्वेस्ट बॉडीज़, ऑथ हेडर्स, और फ़ुल रिस्पॉन्स से बचें। अगर आपको कॉरिलेशन चाहिए, तो रॉ पेलोड के बजाय एक सुरक्षित रिक्वेस्ट ID या हैश स्टोर करें।
वास्तविक सिस्टम्स में काम करने वाले ऑर्डरिंग नियम
कई टीमें शुरु में सोचती हैं “इवेंट्स को उसी क्रम में भेजें जिस क्रम में हमने बनाए थे।” मुश्किल यह है कि “वही क्रम” शायद ग्लोबल नहीं होता। अगर आप एक ग्लोबल क्यू फोर्स करते हैं, तो एक धीमा ग्राहक या फ्लेकी API सबको रोक सकता है।
एक व्यावहारिक नियम है: ऑर्डरिंग को समूह-स्तर पर बनाए रखें, पूरे सिस्टम पर नहीं। एक ग्रुपिंग की चुनें जो बाहरी दुनिया के नजरिए से आपके डेटा से मेल खाती हो, जैसे customer_id, account_id, या aggregate_id जैसे order_id। फिर हर समूह के अंदर ऑर्डर का गारंटी दें जबकि कई समूहों को समानांतर में डिलिवर करने दें।
ऑर्डर न तोड़ते हुए पैरेलल वर्कर्स
कई वर्कर्स चलाएँ, पर सुनिश्चित करें कि दो वर्कर्स एक ही समूह को एक साथ प्रोसेस न करें। सामान्य तरीका यह है कि हमेशा किसी दिए गए aggregate_id के लिए सबसे पहले unsent इवेंट को डिलिवर करें और अलग-अलग aggregates पर पैरेलल चलने दें।
क्लेमिंग के नियम सरल रखें:
- केवल किसी समूह के लिए सबसे पहले pending इवेंट को डिलिवर करें।
- समूहों के बीच पैरेलल होना चाहिए, समूह के अंदर नहीं।
- एक इवेंट क्लेम करें, भेजें, स्टेटस अपडेट करें, फिर आगे बढ़ें।
जब एक इवेंट बाकी को ब्लॉक करे
एक दिन एक “जहरीला” इवेंट घंटों तक फेल हो जाएगा (खराब पेलोड, revoked token, provider outage)। अगर आप कड़ाई से पर-ग्रुप ऑर्डर लागू करते हैं, तो उस समूह की बाद की घटनाएँ प्रतीक्षा करेंगी, लेकिन दूसरे समूह जारी रहें।
एक कामचलाऊ समझौता यह है कि प्रति इवेंट retries को कैप करें। उसके बाद उसे failed मार्क करें और केवल उसी समूह को रोकें जब तक कोई रूट कारण ठीक न करे। इससे एक टूटे हुए ग्राहक से सब रोके नहीं रहेंगे।
retries बिना स्थिति और बुरी किए
Retries वह जगह हैं जहाँ एक अच्छा आउटबॉक्स सेटअप भरोसेमंद या शोरगुल भरा बन सकता है। लक्ष्य सरल है: तब फिर कोशिश करें जब सफल होने की संभावना हो, और जब सफल नहीं होगा तब जल्दी बंद कर दें।
exponential backoff और एक हार्ड कैप का उपयोग करें। उदाहरण के लिए: 1 मिनट, 2 मिनट, 4 मिनट, 8 मिनट, फिर रोक दें (या अधिकतम विलंब जैसे 15 मिनट के साथ जारी रखें)। हमेशा अधिकतम प्रयास सेट करें ताकि एक खराब इवेंट सिस्टम को अनन्त समय तक जाम न कर सके।
हर फेल्यर को retry नहीं करना चाहिए। नियम स्पष्ट रखें:
- Retry करें: नेटवर्क टाइमआउट, कनेक्शन रिसेट्स, DNS हिचकी, और HTTP 429 या 5xx प्रतिक्रियाएँ।
- Retry मत करें: HTTP 400 (bad request), 401/403 (auth समस्याएँ), 404 (गलत एंडपॉइंट), या वैलिडेशन एरर जिन्हें आप भेजने से पहले पहचान सकते हैं।
रिट्री स्टेट आउटबॉक्स रो पर स्टोर करें। attempts बढ़ाएँ, अगले प्रयास के लिए available_at सेट करें, और एक छोटा सुरक्षित एरर सारांश रिकॉर्ड करें (स्टेटस कोड, एरर क्लास, ट्रिम्ड मैसेज)। एरर फ़ील्ड्स में पूरा पेलोड या संवेदनशील डेटा स्टोर न करें।
रेट लिमिट्स के लिए विशेष हैंडलिंग चाहिए। अगर आपको HTTP 429 मिलता है, तो जहाँ Retry-After मौजूद हो उसे सम्मान दें। अन्यथा, retry स्टॉर्म से बचने के लिए अधिक आक्रामक बैकऑफ अपनाएँ।
डुप्लिकेशन और idempotency के मूल सिद्धांत
यदि आप विश्वसनीय API इंटीग्रेशन बनाते हैं, तो मान लें कि वही इवेंट दो बार भेजा जा सकता है। एक वर्कर HTTP कॉल के बाद क्रैश हो सकता है पर रिकॉर्ड नहीं कर पाया कि सफलता हुई। एक टाइमआउट सफलता छिपा सकता है। एक retry धीमी पहली कोशिश के साथ ओवरलैप कर सकता है। आउटबॉक्स पैटर्न मिस्ड इवेंट्स घटाता है, पर अपने आप duplicates नहीं रोकता।
सबसे सुरक्षित तरीका idempotency है: बार-बार डिलिवरी से वही नतीजा आएगा जैसा एक बार देने से आता। तीसरे पक्ष के API को कॉल करते समय एक idempotency key शामिल करें जो उस इवेंट और उस डेस्टिनेशन के लिए स्थिर रहे। कई APIs हेडर सपोर्ट करते हैं; अगर नहीं, तो की को रिक्वेस्ट बॉडी में रखें।
एक साधारण की है डेस्टिनेशन प्लस इवेंट ID। उदाहरण के लिए इवेंट ID evt_123 के लिए हमेशा कुछ ऐसा उपयोग करें: destA:evt_123।
आपकी तरफ, डुप्लिकेट भेजने से रोकने के लिए एक outbound delivery log रखें और (destination, event_id) जैसा यूनिक नियम लागू करें। यहां तक कि अगर दो वर्कर रेस करें, तो भी केवल एक ही “हम इसे भेज रहे हैं” रिकॉर्ड बना पाएगा।
वेबहुक भी डुप्लिकेट कर सकते हैं
अगर आप वेबहुक कॉलबैक्स प्राप्त करते हैं (जैसे “delivery confirmed” या “status updated”), तो उन्हें भी उसी तरह संभालें। प्रदाता retries करते हैं, और आप एक ही पेलोड कई बार देख सकते हैं। प्रोसेस्ड वेबहुक IDs स्टोर करें, या प्रदाता के मैसेज ID से स्थिर हैश निकालकर रिपीट्स को रिजेक्ट करें।
डेटा कितनी देर रखें
आउटबॉक्स रो तब तक रखें जब तक आपने सफलता रिकॉर्ड न कर ली हो (या आप एक अंतिम फेल्यर स्वीकार कर लें)। डिलीवरी लॉग्स को लंबे समय तक रखें, क्योंकि वे आपके ऑडिट ट्रेल हैं जब कोई पूछे, “क्या हमने इसे भेजा था?”
एक सामान्य दृष्टिकोण:
- आउटबॉक्स रो: सफलता के बाद एक छोटा safety विंडो देने के बाद delete या archive करें (कुछ दिनों)।
- डिलीवरी लॉग्स: compliance और सपोर्ट ज़रूरतों के आधार पर कुछ हफ्ते या महीने रखें।
- Idempotency कीज़: कम से कम उतने समय तक रखें जितने retries हो सकते हैं (और वेबहुक डुप्लिकेट्स के लिए और भी लंबा)।
चरण-दर-चरण: आउटबॉक्स पैटर्न लागू करना
निर्णय लें कि आप क्या प्रकाशित करेंगे। इवेंट्स को छोटा, फोकस्ड, और बाद में रीप्ले करने में आसान रखें। एक अच्छा नियम है: हर इवेंट में एक बिजनेस फैक्ट हो, और रिसीवर के काम के लिए पर्याप्त डेटा हो।
नींव बनाना
स्पष्ट इवेंट नाम चुनें (उदा., order.created, order.paid) और अपने पेलोड स्कीमा का वर्शन रखें (जैसे v1, v2)। वर्शनिंग से आप बाद में फील्ड जोड़ सकते हैं बिना पुराने कंज्यूमर्स को तोड़े।
अपनी PostgreSQL आउटबॉक्स तालिका बनाएं और वर्कर की सबसे ज़रूरी क्वेरीज के लिए इंडेक्स जोड़ें, खासकर (status, available_at, id)।
अपनी राइट फ्लो अपडेट करें ताकि बिजनेस चेंज और आउटबॉक्स इन्सर्ट एक ही डेटाबेस ट्रांज़ैक्शन में हों। यही मुख्य गारंटी है।
डिलिवरी और कंट्रोल जोड़ना
एक साधारण इम्प्लिमेंटेशन प्लान:
- इवेंट टाइप और पेलोड वर्जन पर फैसला करें जिन्हें आप दीर्घकालिक संभाल सकें।
- आउटबॉक्स तालिका और इंडेक्स बनाएं।
- मुख्य डेटा बदलाव के साथ एक आउटबॉक्स रो डालें।
- एक वर्कर बनाएं जो रो क्लेम करे, थर्ड-पार्टी API को भेजे, फिर स्टेटस अपडेट करे।
- बैकऑफ के साथ retry शेड्यूलिंग जोड़ें और कोशिशों के ख़त्म होने पर
failedस्टेट रखें।
मूल मीट्रिक्स जोड़ें ताकि आप जल्दी समस्या देख सकें: लॅग (सबसे पुराना unsent इवेंट उम्र), सेंड रेट, और फेल्यर रेट।
एक सरल उदाहरण: ऑर्डर इवेंट्स भेजना
ग्राहक आपका ऐप में ऑर्डर करता है। आपके सिस्टम से बाहर दो चीज़ें होनी चाहिए: बिलिंग प्रदाता को चार्ज करना, और शिपिंग प्रदाता को शिपमेंट बनाना।
आउटबॉक्स पैटर्न के साथ, आप चेकआउट रिक्वेस्ट के अंदर उन APIs को कॉल नहीं करते। इसके बजाय, आप ऑर्डर और एक आउटबॉक्स इवेंट एक ही PostgreSQL ट्रांज़ैक्शन में सेव करते हैं, ताकि आप कभी "ऑर्डर सेव हुआ, पर नोटिफिकेशन नहीं गया" (या उल्टा) जैसी स्थिति में न फंसें।
एक सामान्य आउटबॉक्स रो में aggregate_id (ऑर्डर ID), event_type जैसे order.created, और टोटल, आइटम्स, और डेस्टिनेशन विवरण के साथ एक JSONB पेलोड शामिल हो सकता है।
फिर एक वर्कर pending रो उठाता है और बाहरी सर्विसेज को कॉल करता है (चाहे परिभाषित क्रम में या अलग इवेंट्स जैसे payment.requested और shipment.requested छोड़कर)। अगर कोई प्रोवाइडर डाउन है, तो वर्कर प्रयास रिकॉर्ड करता है, अगले ट्राई के लिए available_at आगे कर देता है, और आगे बढ़ जाता है। ऑर्डर अभी भी मौजूद है, और इवेंट बाद में रीट्राई होगा बिना नए चेकआउट्स को ब्लॉक किए।
ऑर्डरिंग आमतौर पर “प्रति ऑर्डर” या “प्रति ग्राहक” होती है। यह सुनिश्चित करें कि एक ही aggregate_id वाले इवेंट्स एक-एक करके प्रोसेस हों ताकि order.paid कभी order.created से पहले न पहुँचे।
डुप्लिकेटेशन आपको दो बार चार्ज करने या दो शिपमेंट बनाने से बचाता है। जब तीसरा पक्ष समर्थन करे तो idempotency की भेजें, और एक डेस्टिनेशन डिलीवरी रिकॉर्ड रखें ताकि टाइमआउट के बाद रीट्राय एक दूसरी कार्रवाई न कर दे।
शिप करने से पहले त्वरित जाँच
किसी एकीकरण पर भरोसा करने से पहले, किनारों पर टेस्ट करें: क्रैशेस, retries, duplicates, और कई वर्कर्स।
आम विफलताओं को पकड़ने वाली जाँचें:
- पुष्टि करें कि आउटबॉक्स रो उसी ट्रांज़ैक्शन में बनाए जा रहे हैं जैसे बिजनेस परिवर्तन।
- सत्यापित करें कि सेंडर कई इंस्टेंस में सुरक्षित रूप से चल सकता है। दो वर्कर्स एक ही समय पर एक ही इवेंट नहीं भेजना चाहिए।
- अगर ऑर्डरिंग मायने रखती है, तो नियम एक वाक्य में परिभाषित करें और इसे एक स्थिर की से लागू करें।
- प्रत्येक डेस्टिनेशन के लिए तय करें कि आप डुप्लिकेट कैसे रोकेंगे और आप यह कैसे साबित करेंगे कि “हमने इसे भेजा।”
- निकास परिभाषित करें: N प्रयासों के बाद इवेंट को
failedपर ले जाएँ, अंतिम एरर सारांश रखें, और एक सरल reprocess क्रिया प्रदान करें।
वास्तविकता जांच: Stripe जैसी सेवा एक अनुरोध स्वीकार कर सकती है पर आपका वर्कर सफलता रिकॉर्ड करने से पहले क्रैश कर सकता है। बिना idempotency के, रीट्राय डबल एक्शन कर सकता है। idempotency और सहेजे हुए डिलीवरी रिकॉर्ड के साथ, रीट्राय सुरक्षित बन जाता है।
अगले कदम: बिना ऐप बाधित किए रोलआउट
रोलआउट वह जगह है जहाँ आउटबॉक्स प्रोजेक्ट आम तौर पर सफल या अटके रहते हैं। पहले छोटा रखें ताकि आप वास्तविक व्यवहार देखें बिना अपनी पूरी इंटीग्रेशन लेयर को जोखिम में डाले।
एक इंटीग्रेशन और एक इवेंट प्रकार से शुरू करें। उदाहरण के लिए, केवल order.created को एक वेंडर API को भेजें जबकि बाकी जैसा है वैसा ही रहे। इससे आपको थ्रूपुट, लैटेंसी, और फेल्यर रेट का साफ़ बेसलाइन मिलेगी।
समस्याओं को जल्दी दिखाई दें। आउटबॉक्स लॅग (कितने इवेंट्स इंतज़ार कर रहे हैं, और सबसे पुराना कितना पुराना है) और फेल्यर रेट (कितने stuck retries में हैं) के लिए डैशबोर्ड और अलर्ट जोड़ें; अगर आप 10 सेकंड में जवाब दे सकें "क्या हम अभी पीछे हैं?", तो आप यूज़र्स से पहले समस्याओं को पकड़ लेंगे।
पहले हादसे से पहले एक सुरक्षित reprocess योजना रखें। तय करें कि “reprocess” का क्या अर्थ है: वही पेलोड फिर से भेजना, वर्तमान डेटा से पेलोड फिर से बनाना, या इसे मैन्युअल समीक्षा के लिए भेजना। दस्तावेज़ करें कि कौन से मामले फिर से भेजना सुरक्षित है और किसे मानव जाँच की ज़रूरत है।
अगर आप इसे no-code प्लेटफ़ॉर्म जैसे AppMaster (appmaster.io) से बना रहे हैं, तो वही संरचना लागू होती है: अपना बिजनेस डेटा और एक आउटबॉक्स रो PostgreSQL में एक साथ लिखें, फिर एक अलग बैकएंड प्रोसेस चलाएँ जो डिलिवर करे, retries संभाले, और इवेंट्स को sent या failed मार्क करे।
सामान्य प्रश्न
Use the outbox pattern when a user action updates your database and must trigger work in another system. It’s most useful when timeouts, flaky networks, or third-party outages can create “saved in our app, missing in theirs” situations.
Writing the business row and the outbox row in the same database transaction gives you one clear guarantee: either both exist or neither exists. That prevents partial failures like “API call succeeded but the order wasn’t saved” or “order saved but the API call never happened.”
A good default is id, aggregate_id, event_type, payload, status, created_at, available_at, attempts, plus lock fields like locked_at and locked_by. This keeps sending, retry scheduling, and safe concurrency simple without overcomplicating the table.
A common baseline is an index on (status, available_at, id) so workers can quickly fetch the next batch of sendable events in order. Add other indexes only when you truly query by those fields, because extra indexes slow down inserts.
Polling is the simplest and most predictable approach for most teams. Start with small batches and a short interval, then tune based on load and lag; you can add optimizations later, but a simple loop is easier to debug when things go wrong.
Claim rows using row-level locks so two workers can’t process the same event at the same time, typically with SKIP LOCKED. Then mark the row as processing with a lock timestamp and worker ID, send it, and finally mark it sent or return it to pending with a future available_at.
Use exponential backoff with a hard cap on attempts, and retry only failures that are likely temporary. Timeouts, network errors, and HTTP 429/5xx are good retry candidates; validation errors and most 4xx responses should be treated as final until you fix data or configuration.
Assume duplicates can still happen, especially if a worker crashes after the HTTP call but before recording success. Use an idempotency key that is stable per destination and per event, and keep a delivery record (with a unique constraint) so even racing workers can’t create two sends.
Default to preserving order within a group, not globally. Use a grouping key like aggregate_id (order ID) or customer_id, process only one event at a time per group, and allow parallelism across different groups so one slow customer doesn’t block everyone.
Mark it as failed after a maximum number of attempts, keep a short safe error summary, and stop processing later events for that same group until someone fixes the root cause. This contains the blast radius and prevents endless retry noise while keeping other groups moving.


