कारोबार के लिए Event-Driven आर्किटेक्चर: आपका ऑर्डर ही स्टॉक और बही-खाते को ख़ुद क्यों बताए
नाइटली sync और उलझी कॉल्स से स्टॉक और हिसाब बेमेल हो जाते हैं। event, transactional outbox और idempotent listener कैसे एक ऑर्डर से स्टॉक, लेजर और लॉयल्टी भरोसे के साथ अपडेट करते हैं, उदाहरण सहित देखिए।
Author
Anichur Rahaman
2 महीने पहले11 min read3 views
सेल वाले वीकेंड में छह outlet वाले एक रिटेल कारोबार को 1,900 order मिले। सोमवार सुबह फ़ाइनेंस हेड रिपोर्टें खोलता है तो stock शीट कहती है कि 14 जैकेट बचे हैं, जबकि अगले शहर के outlet ने आख़िरी जैकेट शनिवार को ही बेच दी। बही-खाते का हाल और बुरा है: रेवेन्यू रात की एक batch से पोस्ट होता है, वह batch order 1,412 पर टाइमआउट हो गई, और 488 order खातों से ग़ायब हैं। मंगलवार की रिकॉन्सिलिएशन से पहले किसी को पता भी नहीं चलेगा।
इस कहानी में किसी ने ग़लती नहीं की। system ही हर विभाग से कहता है कि order की ख़बर ख़ुद ढूँढ लो: पोलिंग करके, एक्सपोर्ट करके, या ग़लत वक़्त पर आई कॉल से। यह लेख उलटी बात कहता है: जब कुछ घटे, तो उसे एक बार तथ्य के रूप में दर्ज करें, और कारोबार के बाक़ी हिस्से उस तथ्य पर अपने-आप प्रतिक्रिया दें। यही event-driven आर्किटेक्चर है।
system डिज़ाइन का यही हिस्सा मेरे काम का बड़ा हिस्सा रहा है, इसलिए राय थोड़ी तीखी मिलेगी। मक़सद यह है कि अंत तक आप असली event-driven system और मार्केटिंग के दावे में फ़र्क़ कर सकें, और जान लें कि माँगना क्या है।
नाइटली sync और उलझी हुई कॉल्स क्यों फ़ेल होती हैं
ज़्यादातर बिज़नेस software सीधी कॉल से शुरू होते हैं। checkout ख़त्म हुआ, तो checkout का कोड stock के कोड को बुलाता है, फिर अकाउंटिंग को, फिर email को। डेमो में चल जाता है, पर तीन जगह टूटता है, और तीनों पहले से अंदाज़ा लगाई जा सकती हैं।
सबसे धीमा स्टेप ग्राहक को रोके रखता है। email सर्विस आठ सेकंड ले, तो ख़रीदार आठ सेकंड इंतज़ार करता है, या email बंद होने से order ही फ़ेल हो जाता है।
हर नई ज़रूरत पुराने कोड को बदलती है। लॉयल्टी पॉइंट जोड़ने के लिए कंपनी की सबसे जोखिम भरी फ़ाइल, यानी checkout, खोलकर एक और कॉल डालनी पड़ती है।
अधूरे काम का कोई मालिक नहीं होता। stock घट गया, फिर अकाउंटिंग फ़ेल हो गई। कहीं दर्ज नहीं कि दूसरा स्टेप अभी बाक़ी है।
आम जुगाड़ है नाइटली sync: रात 2 बजे order एक्सपोर्ट करो, कहीं और इंपोर्ट करो। इससे गड़बड़ी उन घंटों में खिसक जाती है जब कोई देख नहीं रहा, और हर आँकड़ा 24 घंटे तक पुराना हो जाता है। फिर टीमें अंतर पकड़ने के लिए रिकॉन्सिलिएशन स्प्रेडशीट बनाती हैं, और आख़िर में वही स्प्रेडशीट असली system बन जाती है।
Command और event: वे दो शब्द जो मायने रखते हैं
ज़्यादातर काम दो शब्द करते हैं, और इन्हें गड्डमड्ड कर देना डिज़ाइन की पहली आम ग़लती है।
Command एक अनुरोध है: "यह order ले लो", "यह payment refund करो"। यह किसी एक मालिक को संबोधित होता है, वर्तमान काल में कहा जाता है, और इसे ठुकराया जा सकता है। Event एक घटा हुआ तथ्य है: OrderPlaced, PaymentReceived, ParcelDelivered। इसका नाम भूतकाल में होता है, क्योंकि यह हो चुका है, और इसे कोई नकार नहीं सकता। जो हिस्सा इस तथ्य का मालिक है वह producer है। जिसे भी इसकी परवाह है वह listener है (इसे consumer या subscriber भी कहते हैं)।
Producer को पता नहीं होता कि कौन सुन रहा है। पूरा फ़ायदा इसी एक बात में है। लॉयल्टी पॉइंट जोड़ने का मतलब है ParcelDelivered के लिए एक नया listener लिखना। checkout को छुआ नहीं जाता, इसलिए वह टूट भी नहीं सकता।
एक order, तीन event, पाँच listener
एक उदाहरण से बात साफ़ होती है। मान लीजिए (काल्पनिक उदाहरण) एक ग्राहक SKU JKT-M की दो जैकेट ख़रीदता है, 40.00 प्रति जैकेट (यूनिट कॉस्ट 22.00), साथ में 5.00 shipping और 8.00 टैक्स। कुल 93.00, कार्ड से भुगतान। अगले तीन दिनों में यह order तीन event बनाता है।
एक घटना, पाँच स्वतंत्र प्रतिक्रियाएँ। checkout इनमें से किसी को कॉल नहीं करता।
हर event के साथ एक छोटा payload चलता है, इतना कि listener को producer से कुछ पूछने वापस न जाना पड़े।
stock_movements: type sale, qty -2, रिज़र्वेशन मुक्त। ऑन-हैंड 46
ParcelDelivered
ledger
डेबिट Customer deposits 93.00; क्रेडिट Sales 80.00, Shipping income 5.00, Tax payable 8.00। डेबिट Cost of goods sold 44.00; क्रेडिट Inventory 44.00
ParcelDelivered
लॉयल्टी
loyalty_entries: customer 77, +80 पॉइंट (माल की हर एक करेंसी यूनिट पर 1 पॉइंट), ref order 1042
ParcelDelivered
notification
notifications: डिलीवरी का संदेश और रिव्यू का न्योता, status queued
हिसाब जाँच लीजिए: डिलीवरी journal में डेबिट 93.00 + 44.00 = 137.00 है और क्रेडिट 80.00 + 5.00 + 8.00 + 44.00 = 137.00। यह इसलिए संतुलित है कि listener को संतुलित एंट्री पोस्ट करने के लिए ही लिखा गया है, इसलिए नहीं कि महीने के अंत में किसी ने मिलाकर देखा। यहाँ रेवेन्यू डिलीवरी पर माना गया है; आपकी अकाउंटिंग नीति अलग हो सकती है, और वह फ़ैसला सिर्फ़ ledger listener का है।
Outbox: event कभी खोता क्यों नहीं
Producer की तरफ़ एक जाल है। order लेने का मतलब है दो लिखावटें: database में order सेव करना, और queue में OrderPlaced भेजना। ये दो अलग system हैं, इसलिए एक ट्रांज़ैक्शन दोनों को नहीं ढक सकता। database में कमिट हो गया और भेजने से पहले प्रोसेस मर गया, तो order है पर event नहीं। stock को कुछ पता ही नहीं चला। उलटा, पहले भेज दिया और database रोलबैक हो गया, तो बाक़ी सबने ऐसे order पर प्रतिक्रिया दे दी जो कभी सेव ही नहीं हुआ। इसे dual-write problem कहते हैं।
इसका हल है transactional outbox, जिसे Chris Richardson ने अपने microservices pattern catalogue में समझाया है। order और उसका event एक ही database में, एक ही ट्रांज़ैक्शन में लिखे जाते हैं; event जाता है outbox टेबल में। या तो दोनों रो मौजूद होती हैं, या एक भी नहीं। फिर एक अलग relay प्रोसेस बिना भेजी outbox रो पढ़ता है, उन्हें queue में भेजता है और "sent" का निशान लगा देता है।
एक ट्रांज़ैक्शन, दो रो। दूसरी रो को relay queue तक पहुँचाता है, जितनी बार भी ज़रूरत पड़े।
Relay भी भेजने और "sent" निशान लगाने के बीच क्रैश हो सकता है। रीस्टार्ट पर वह event दोबारा भेज देता है। यानी outbox एक सटीक चीज़ देता है: event कभी खोता नहीं, पर एक से ज़्यादा बार पहुँच सकता है। अगली बात यहीं से निकलती है।
"Exactly once" असल में at least once और idempotent listener है
vendor exactly-once डिलीवरी का वादा करना पसंद करते हैं। network से जुड़ी अलग-अलग मशीनों के बीच आम तौर पर यह मुमकिन नहीं: जिस भेजने वाले को पावती नहीं मिली, उसे नहीं पता कि संदेश पहुँचा या नहीं, इसलिए उसे चुनना पड़ता है कि दोबारा भेजे या खो जाने का जोखिम ले। भरोसेमंद system दोबारा भेजना चुनते हैं। payment प्रोवाइडर यह खुलकर कहते हैं; मिसाल के तौर पर Stripe की डॉक्यूमेंटेशन कहती है कि एक ही webhook event के एक से ज़्यादा बार आने की उम्मीद रखिए और event ID से डुप्लीकेट छाँट दीजिए।
इसलिए काम की गारंटी है at least once डिलीवरी और idempotent हैंडलिंग। Listener idempotent तब होता है जब एक ही event को दो बार प्रोसेस करने का असर एक बार प्रोसेस करने जैसा ही हो। आम तरीक़ा छोटा है: processed_events नाम की टेबल, जिसमें (listener, event_id) पर unique key हो।
हर event के साथ listener क्या करता है, वह event भी जो वह पहले देख चुका है।
दो बारीकियाँ तय करती हैं कि यह चलेगा या नहीं। पहली, "बदलाव लागू करना" और "event ID दर्ज करना" दोनों एक ही database ट्रांज़ैक्शन में कमिट होने चाहिए। पहले लागू किया, फिर दर्ज किया, तो बीच में क्रैश का मतलब है रिट्राई पर डबल पोस्टिंग। दूसरी, unique key को जाँच atomic बनानी चाहिए, ताकि एक ही event की दो कॉपियाँ एक साथ आएँ तो दोनों पास न हो सकें।
उदाहरण पर लौटें: अगर ParcelDelivered दो बार आए, तो दूसरी बार ledger listener देखता है कि evt_5003 पहले से दर्ज है और उसे छोड़ देता है। journal दो बार पोस्ट नहीं होता, और 80 लॉयल्टी पॉइंट 160 नहीं बनते।
क्रम, रिट्राई और मुफ़्त में मिलने वाला इतिहास
क्रम
अलग-अलग order के event किसी भी क्रम में प्रोसेस हों तो चलता है। पर एक ही order के event के लिए क्रम मायने रखता है: PaymentReceived से पहले RefundIssued प्रोसेस होने का कोई मतलब नहीं। दो बचाव साथ चलते हैं। event को order ID के हिसाब से रूट कीजिए, ताकि एक order के सारे event एक ही लेन से क्रम में गुज़रें, और हर event को order-वार सीक्वेंस नंबर दीजिए, ताकि listener बहुत पुराने event को पहचानकर उसे अलग रख दे या छोड़ दे।
रिट्राई और dead-letter queue
जब कोई listener फ़ेल होता है, क्योंकि database व्यस्त था या courier का API बंद था, तो event बढ़ते अंतराल के साथ दोबारा कोशिश के लिए लौट जाता है, जैसे 10 सेकंड, 1 मिनट, 5 मिनट, 30 मिनट, 2 घंटे। बैकऑफ़ ज़रूरी है: तुरंत रिट्राई करने से छोटी-सी रुकावट अपने ही पैदा किए ओवरलोड में बदल जाती है। तय संख्या में कोशिशों के बाद, मान लें पाँच, event dead-letter queue में चला जाता है और किसी को alert मिलता है। Dead letter दिखती है और वापस लाई जा सकती है। इसकी तुलना फ़ेल हुए नाइटली एक्सपोर्ट से कीजिए, जो बस ग़ायब होता है।
इतिहास
कारोबार का हर बदलाव जब एक event है, जिसमें समय, ID और payload है, तो audit ट्रेल अलग से बनाने की ज़रूरत नहीं पड़ती। "इस ग्राहक के 80 पॉइंट क्यों हैं?" का जवाब तैयार है: ParcelDelivered evt_5003, जिसे Loyalty ने तय समय पर प्रोसेस किया। event को एक तय अवधि तक रखिए, और जब किसी listener में बग हो, तो उसे ठीक करके प्रभावित event उसमें दोबारा चला (replay) दीजिए। Idempotency की वजह से replay सुरक्षित है।
कब इस्तेमाल न करें
Event-driven डिज़ाइन में चलते-फिरते हिस्से बढ़ते हैं: queue, relay, worker, monitoring, और eventual consistency के हिसाब से सोचने की आदत। ब्रोशर साइट, एक user के टूल, या सादे CRUD admin panel में, जहाँ एक टेबल अपडेट होती है और एक screen उसे पढ़ती है, सीधी फ़ंक्शन कॉल सरल और बेहतर है। जिस स्टेप का जवाब अभी चाहिए, जैसे "यह कार्ड वैध है?", उस पर भी यही लागू होता है। वह जवाब वाला command है, event नहीं।
मेरी कसौटी: जब किसी एक काम के तीन या ज़्यादा स्वतंत्र नतीजे अलग-अलग टीमों या module के हाथ में हों (stock, पैसा, संदेश, डिलीवरी), तब event अपनी क़ीमत वसूलने लगते हैं।
क़ीमत के बारे में भी ईमानदार रहिए। Listener order के कुछ पल बाद चलते हैं, इसलिए checkout के फ़ौरन बाद stock पढ़ने वाली screen थोड़ी देर पुराना आँकड़ा दिखा सकती है। अच्छे product इसे checkout ट्रांज़ैक्शन के भीतर ही रिज़र्वेशन करके सँभालते हैं, और अंदाज़ा लगाने की जगह "प्रोसेसिंग" दिखाते हैं।
वहाँ तक कैसे पहुँचें, और कैसे जानें कि चल रहा है
स्टेप
उन घटनाओं की सूची बनाइए जिनके बारे में आपका कारोबार पहले से बात करता है: order placed, payment received, parcel delivered, return approved, stock counted। हर एक का नाम भूतकाल में रखिए।
हर payload में इतने फ़ील्ड रखिए कि listener को कभी वापस पूछना न पड़े, और हर event को एक यूनिक ID और टाइमस्टैम्प दीजिए।
जिस बदलाव को event बयान करता है, उसी ट्रांज़ैक्शन में event को outbox टेबल में लिखिए।
एक relay चलाइए जो outbox रो को queue में भेजे और "sent" का निशान लगाए।
हर listener को idempotent बनाइए, और processed-events रिकॉर्ड उसकी अपनी लिखावटों वाले ट्रांज़ैक्शन में ही रखिए।
पहले असली order से पहले बैकऑफ़ के साथ रिट्राई, dead-letter queue और उस पर alert लगाइए।
कंज़्यूमर एक-एक करके शिफ़्ट कीजिए: पहले stock, फिर ledger, फिर संदेश और लॉयल्टी। नाइटली जॉब सबसे आख़िर में बंद कीजिए, एक हफ़्ते तक आँकड़े मिलने के बाद।
vendor से पूछने के सवाल
क्या event उसी ट्रांज़ैक्शन में लिखा जाता है जिसमें बिज़नेस बदलाव होता है, या बाद में भेजा जाता है?
एक ही event दो बार आए तो क्या होता है? डुप्लीकेट हटाने वाली key दिखाने को कहिए।
फ़ेल हुए event कहाँ जाते हैं, alert किसे मिलता है, और क्या मैं उन्हें दोबारा चला सकता हूँ?
क्या मैं एक order का event इतिहास समय और हैंडलर के साथ देख सकता हूँ?
इन दावों को असली system पर परखने का एक तरीक़ा: StoreConsole जैसा self-hosted platform अपने inventory, अकाउंटिंग और लॉयल्टी module को एक ही order event पर अलग-अलग listener की तरह चलाता है।
क्या मापें
Outbox lag: सबसे पुरानी बिना भेजी रो की उम्र। कुछ सेकंड हों तो हालत ठीक है।
Queue depth और हैंडलिंग टाइम, हर listener का अलग।
Dead-letter की गिनती: शून्य के आसपास रहनी चाहिए, चुपचाप बढ़ती नहीं।
Duplicate-skip दर: शून्य से ऊपर का आँकड़ा साबित करता है कि डुप्लीकेट छँटाई काम कर रही है।
रोज़ का drift चेक: stock ledger की यूनिट बनाम order से निकली यूनिट, और ledger का रेवेन्यू बनाम order का रेवेन्यू। दोनों अंतर शून्य होने चाहिए।
वही सोमवार, नए सिरे से
1,900 order वाले फ़ाइनेंस हेड के पास लौटते हैं। हर order ने बिक्री वाले ट्रांज़ैक्शन में ही अपना event लिखा, इसलिए 1,900 outbox रो हैं और एक भी खोई नहीं। order 1,412 पर ledger listener टाइमआउट में फँसा। 1,412 से 1,900 तक के order queue में इंतज़ार करते रहे, बैकऑफ़ के साथ रिट्राई हुए और कुछ ही मिनटों में पोस्ट हो गए। दो order ग़लत टैक्स कोड की वजह से पाँच बार फ़ेल होकर dead-letter queue में पहुँचे, और alert शनिवार दोपहर को ही उठ गया, मंगलवार को नहीं।
पास के outlet की शनिवार की बिक्री ने उसी पल stock रिज़र्व कर लिया था, इसलिए 14 जैकेट सचमुच 14 ही थीं। सोमवार का drift चेक दोनों कॉलम में शून्य दिखाता है। रिकॉन्सिलिएशन स्प्रेडशीट खोलनी ही नहीं पड़ी।
मुख्य बातें
Command अनुरोध करता है और ठुकराया जा सकता है; event एक घट चुका तथ्य है। Producer event की घोषणा करता है, उसे पता नहीं कि कौन सुन रहा है।
Event को बिज़नेस बदलाव वाले ट्रांज़ैक्शन में ही लिखिए (outbox), ताकि वह कभी न खोए।
Exactly-once डिलीवरी यथार्थवादी वादा नहीं है। at-least-once डिलीवरी और idempotent listener बनाइए।
प्रोसेस हो चुके event की ID listener की लिखावटों वाले ट्रांज़ैक्शन में ही दर्ज कीजिए।
बैकऑफ़ के साथ रिट्राई कीजिए, फिर dead-letter और alert। दिखने वाली नाकामी चुपचाप पड़े गड्ढे से बेहतर है।
वहाँ इस्तेमाल कीजिए जहाँ एक काम के तीन या ज़्यादा स्वतंत्र नतीजे हों। सादे CRUD में छोड़ दीजिए।
अनिचुर रहमान सॉफ़्टवेयर आर्किटेक्ट और StoreConsole के संस्थापक हैं। वे बढ़ते कारोबारों के लिए कॉमर्स और ERP सिस्टम डिज़ाइन करते हैं — ख़ास ध्यान event-driven आर्किटेक्चर, डेटा की शुद्धता और अपने सर्वर पर चलने वाले सिस्टम पर।