अच्छा सिस्टम डिज़ाइन
(seangoedecke.com)- अच्छा सिस्टम डिज़ाइन वह है जो जटिल न लगे और लंबे समय तक बिना किसी खास समस्या के चलता रहे
- सिस्टम डिज़ाइन में state को संभालना सबसे कठिन हिस्सा है, और जहाँ तक संभव हो state स्टोर करने वाले components की संख्या कम रखना महत्वपूर्ण है
- डेटाबेस आम तौर पर वह जगह है जहाँ state संग्रहीत होता है, इसलिए schema design और indexing, तथा bottleneck हटाने पर केंद्रित दृष्टिकोण की आवश्यकता होती है
- caching, event processing, background jobs आदि को performance और maintenance के लिए सावधानी से लागू करना चाहिए, और इनके अति-उपयोग से बचना बेहतर है
- जटिल डिज़ाइन की बजाय पर्याप्त रूप से सत्यापित सरल components और methodologies का उचित उपयोग टिकाऊ और स्थिर सिस्टम निर्माण की कुंजी है
सिस्टम डिज़ाइन की परिभाषा और समग्र दृष्टिकोण
- यदि software design कोड को जोड़ने की प्रक्रिया है, तो system design विभिन्न services को संयोजित करने की प्रक्रिया है
- सिस्टम डिज़ाइन के मुख्य components हैं app server, database, cache, queue, event bus, proxy आदि
- अच्छा डिज़ाइन ऐसी प्रतिक्रियाएँ पैदा करता है जैसे "कोई खास समस्या नहीं है", "सोच से आसान निकला", "इस हिस्से पर अलग से ध्यान देने की ज़रूरत नहीं है"
- इसके उलट, जटिल और बहुत ध्यान खींचने वाला डिज़ाइन मूल समस्या को छिपा सकता है, या overengineering का संकेत हो सकता है
- जटिल सिस्टम को शुरुआत से ही लागू करने के बजाय, न्यूनतम काम करने वाली सरल संरचना से धीरे-धीरे विकसित करना अधिक लाभदायक है
state और stateless के बीच अंतर
- software design में सबसे पेचीदा हिस्सा है state management
- जो services जानकारी स्टोर किए बिना तुरंत परिणाम लौटा देती हैं (जैसे GitHub का PDF rendering), वे stateless होती हैं
- दूसरी ओर, जो service डेटाबेस में write करती है, वह state को manage करती है
- सिस्टम में state store करने वाले components को यथासंभव कम रखना अच्छा है। इससे सिस्टम की complexity और failure की संभावना कम होती है
- ऐसी संरचना की सिफारिश की जाती है जहाँ state management केवल एक service संभाले, और बाकी services API call या event trigger जैसी stateless भूमिकाओं पर केंद्रित रहें
डेटाबेस डिज़ाइन और bottleneck बिंदु
schema और index डिज़ाइन
- डेटा स्टोर करने के लिए मानव-पठनीय schema design की आवश्यकता होती है
- अत्यधिक flexible schema (उदाहरण: सब कुछ JSON column में स्टोर करना) application code और performance दोनों पर बोझ डाल सकता है
- जिन columns पर query बार-बार चलेगी, उनके आधार पर उपयुक्त index सेट करना चाहिए। हर चीज़ पर index लगाने से उल्टा अनावश्यक overhead बढ़ता है
bottleneck हल करने के तरीके
- डेटाबेस access अक्सर एक भारी bottleneck बन जाता है
- जहाँ तक संभव हो, जटिल डेटा को application में नहीं बल्कि डेटाबेस के भीतर JOIN जैसे तरीकों से process करना performance के लिहाज़ से बेहतर है
- ORM इस्तेमाल करते समय loop के अंदर query चलाने वाली गलती से सावधान रहना चाहिए
- ज़रूरत पड़ने पर query को विभाजित करके डेटाबेस के load या query complexity को नियंत्रित करना भी एक तरीका है
- read queries को read-replica में distribute करके मुख्य (Write) node का load कम करने की रणनीति प्रभावी है
- जब बड़ी संख्या में queries एक साथ आती हैं, तो transaction और write operations डेटाबेस को आसानी से overload कर सकते हैं, इसलिए query throttling पर विचार करना चाहिए
धीमे और तेज़ कामों का विभाजन
- जिन कार्यों से user interact करता है, उनमें कुछ सौ milliseconds के भीतर response आवश्यक है
- अधिक समय लेने वाले कार्य (जैसे बड़े PDF conversion) में सिर्फ न्यूनतम काम तुरंत front-end पर उपलब्ध कराना और बाकी को background में भेजना एक प्रभावी pattern है
- background jobs आम तौर पर queue (जैसे Redis) और job runner के साथ मिलकर काम करती हैं
- बहुत दूर भविष्य के लिए scheduled jobs को Redis की बजाय अलग DB table में manage करना और scheduler के माध्यम से चलाना अधिक व्यावहारिक है
caching
- caching एक जैसे या महंगे operations को बार-बार दोहराने की स्थिति में cost कम करने और performance बढ़ाने में मदद करती है
- आम तौर पर cache के बारे में अभी-अभी सीखने वाला junior engineer हर चीज़ cache करना चाहता है, जबकि अनुभवी engineer cache लागू करने में अधिक सावधानी बरतते हैं
- cache नया state जोड़ता है, इसलिए synchronization issues, errors, stale data जैसे जोखिम मौजूद रहते हैं
- पहले query indexing जैसी performance optimization आज़माने के बाद ही caching लागू करना बेहतर है
- बड़े पैमाने के cache के लिए Redis/Memcached की बजाय S3/Azure Blob Storage जैसे document storage में समय-समय पर स्टोर करने का तरीका भी उपयोगी हो सकता है
event processing
- अधिकांश कंपनियों के पास event hub (जैसे Kafka) होता है, और विभिन्न services events के आधार पर distributed processing करती हैं
- events का अति-उपयोग करने की बजाय, सरल request–response API design logging और troubleshooting के लिहाज़ से अधिक उपयोगी होता है
- event-driven processing तब उपयुक्त है जब sender को receiver के behavior की चिंता न करनी पड़े, या high-volume, latency-tolerant scenarios हों
डेटा पहुँचाने के तरीके: push और pull
- डेटा ट्रांसफर के लिए दो तरीके हैं: Pull (request के बाद response) और Push (बदलाव होते ही स्वतः डिलीवरी)
- Pull तरीका सरल है, लेकिन इसमें repeated requests और overload की समस्या हो सकती है
- Push तरीके में सर्वर डेटा बदलते ही client को तुरंत भेज देता है, इसलिए यह अधिक कुशल है और latest data बनाए रखने में लाभदायक है
- बड़ी संख्या में clients को संभालने के लिए, दोनों तरीकों के अनुसार infrastructure (event queue, कई cache servers आदि) का विस्तार आवश्यक होता है
hot paths पर ध्यान
- hot paths से आशय सिस्टम के भीतर उन रास्तों से है जहाँ सबसे महत्वपूर्ण और सबसे अधिक डेटा प्रवाहित होता है
- hot paths में विकल्प कम होते हैं, और डिज़ाइन विफल होने पर पूरी service में गंभीर समस्या पैदा हो सकती है, इसलिए सावधानीपूर्वक डिज़ाइन अनिवार्य है
- कई विकल्पों वाले छोटे features की तुलना में, hot paths पर ध्यान देकर design और testing में संसाधन लगाना अधिक प्रभावी है
logging, metrics, tracing
- failure होने पर कारण की पहचान के लिए, abnormal paths (unhappy path) के लिए विस्तृत logs सक्रिय रूप से रिकॉर्ड करने चाहिए
- सिस्टम resources (CPU/मेमोरी), queue size, request/job time जैसे बुनियादी observability metrics का संग्रह आवश्यक है
- केवल औसत मान देखने के बजाय p95, p99 latency जैसे distribution metrics भी ज़रूर देखने चाहिए। ऊपर के कुछ धीमे requests ही मुख्य users की समस्या हो सकते हैं
kill switch, retry, failure recovery
- kill switch (सिस्टम को अस्थायी रूप से रोकना) और retry का रणनीतिक उपयोग महत्वपूर्ण है
- बिना सोचे-समझे retry करना सिर्फ दूसरी services पर बोझ डालता है; प्रभावी होने के लिए पहले से circuit breaker जैसे तंत्रों द्वारा requests को नियंत्रित करना चाहिए
- Idempotency Key लागू करके एक ही request के दोबारा process होने पर duplicate work को रोका जा सकता है
- कुछ failure स्थितियों में fail open या fail closed में से चुनाव करना पड़ता है। उदाहरण के लिए, Rate Limiting के लिए fail open (अनुमति देना) user impact के लिहाज़ से कम नुकसानदेह हो सकता है। वहीं authentication के लिए fail closed अनिवार्य है
समापन
- service separation, containers, VM adoption, tracing जैसे कुछ विषय छोड़े गए हैं, लेकिन अच्छी तरह सत्यापित components का सही जगह उपयोग ही लंबे समय में सबसे स्थिर सिस्टम निर्माण की ओर ले जाता है
- तकनीकी रूप से बहुत विशेष डिज़ाइन वास्तव में बहुत दुर्लभ होते हैं, और उबाऊ हद तक सरल डिज़ाइन ही व्यवहारिक काम में सबसे अधिक इस्तेमाल होते हैं
- मूल रूप से अच्छा सिस्टम डिज़ाइन वह प्रक्रिया है जिसमें बिना दिखावे के, पर्याप्त रूप से सिद्ध methodologies को सुरक्षित ढंग से संयोजित किया जाता है
1 टिप्पणियां
Hacker News राय
मुझे अक्सर इस हिस्से में खुद को अकेला महसूस होता है। इंजीनियर जटिल सिस्टम देखकर उसमें कई दिलचस्प चीज़ें पाते हैं और सोचते हैं, “यही तो असली सिस्टम डिज़ाइन है!”, लेकिन असल में जटिल सिस्टम अक्सर अच्छे डिज़ाइन की कमी का नतीजा होते हैं। अगर आप नौकरी ढूंढ रहे हैं, तो इंटरव्यू के दौरान इस सच को पूरी तरह भूल जाना चाहिए। मैंने भी सिस्टम डिज़ाइन इंटरव्यू में यही बात ईमानदारी से कहकर गलती की है। एक काल्पनिक startup app इंटरव्यू में मैंने ऐसे जवाब दिए थे जैसे “इस QPS पर backpressure की चिंता करने की ज़रूरत नहीं”, “cron job की जगह queue इस्तेमाल करने की ज़रूरत नहीं, हाँ trade-off ज़रूर हैं”, “SQL vs NoSQL? टीम जिसे सबसे अच्छी तरह जानती हो वही इस्तेमाल करो”, लेकिन इंटरव्यूअर ऐसे जवाब नहीं चाहते। आपको whiteboard पूरा भर देना चाहिए और Kubernetes इतना डालना चाहिए कि Kubernetes ही Kubernetes को मैनेज कर रहा हो — तभी उन्हें वह signal मिलता है जो वे चाहते हैं
मैंने सैकड़ों सिस्टम डिज़ाइन इंटरव्यू लिए हैं और कई लोगों को ट्रेन किया है। तुम्हारे बताए जवाब weak signal देते हैं (queue वाला जवाब अपवाद है)। इंटरव्यूअर सच में यह जानना चाहते हैं कि तुमने ये फैसले क्यों लिए, किन बातों पर विचार किया, और तुम्हारी सोचने की प्रक्रिया क्या है। अगर तुम जवाब को विस्तार से नहीं समझाओगे, तो इंटरव्यूअर की नज़र में “यहाँ से ज़्यादा जानकारी नहीं मिल रही” जैसा लगेगा। इसलिए उम्मीदवार को वह जानकारी सक्रिय रूप से देनी चाहिए जो इंटरव्यूअर जानना चाहता है। और एक अच्छा इंटरव्यूअर भी अगर तुमसे जवाब खींचकर निकलवा रहा है, तो वह नोट कर सकता है कि “तर्क ठीक है, लेकिन communication अप्रभावी है।” communication skill भी मूल्यांकन का हिस्सा है। आख़िर में, SQL/NoSQL वाले जवाब से मैं सहमत नहीं हूँ। टीम का अनुभव महत्वपूर्ण है, लेकिन तकनीकों के बीच अंतर स्पष्ट होते हैं और परिस्थिति के अनुसार performance में बड़ा फ़र्क पड़ सकता है। ऐसा जवाब यह प्रभाव छोड़ता है कि तुम्हें अलग-अलग स्थितियों का अनुभव कम है
जैसा कहा जाता है, “इंटरव्यू दो-तरफ़ा होता है”, और मुझे तुम्हारे जवाब बहुत तर्कसंगत लगते हैं। अगर मैं इंटरव्यूअर होता, तो शायद तुम्हें ऊँचा स्कोर देता। दूसरी ओर, अगर कोई कंपनी तुम्हें ऐसे जवाबों पर reject करती है, तो संभव है कि समस्या कंपनी में ही हो। लेकिन व्यावहारिक दुनिया में अक्सर जल्दी कहीं जगह बनानी होती है, इसलिए संतुलन रखना और सामने वाला जो सुनना चाहता है उसके मुताबिक जवाब ढालना भी ज़रूरी है
यह सलाह अच्छी नहीं है। सरल और सुरुचिपूर्ण डिज़ाइन की शुरुआत संभावित समस्याओं को नज़रअंदाज़ करने से नहीं होती। follow-up सवाल कोई तकनीकी trivia उगलवाने का समय नहीं, बल्कि चर्चा करने का संकेत होते हैं। तुम्हारे जवाब समझदारी नहीं दिखाते, बल्कि यह आभास देते हैं कि तुम अभी अपरिपक्व हो। इसमें इंटरव्यूअर की गलती नहीं है
बगल वाली टिप्पणी में “इंटरव्यू दो-तरफ़ा है” वाली बात सही है, लेकिन एक अच्छा इंटरव्यूअर ईमानदारी से कहेगा, “यह भी अच्छा जवाब है, लेकिन मैं अभी इसी थीम पर तुम्हारा ज्ञान जाँचना चाहता हूँ।” अगर वह खुद लगातार इधर-उधर की बातें करता रहे, तो वह भी चिंता का संकेत है
मुझे लगता है यह बिल्कुल वही उदाहरण है जो दिखाता है कि LinkedIn-driven development क्यों मौजूद है। CV में ढेर सारी तकनीकें गिनाना, सिर्फ़ एक Postgres और modular monolith का अच्छे से इस्तेमाल समझाने से कहीं ज़्यादा प्रभावशाली दिखता है — यही हक़ीक़त है
मुझे यह सचमुच बहुत अच्छा लेख लगा। लेकिन ऐसी best practices की सीमाओं का भी ज़िक्र होना चाहिए। उदाहरण के लिए, “5 अलग-अलग services को एक table में लिखने मत दो; 4 services API call करें या event भेजें, और सिर्फ़ 1 service table में लिखे” — यह सलाह है, लेकिन वास्तविक दुनिया इतनी साफ़-सुथरी नहीं होती। अगर पाँचों DB तक पहुँच रही हैं, तो आप पहले से ही एक distributed system बना रहे हैं, और DB में वैसे भी permissions, transactions, और custom queries जैसी चीज़ें होती हैं, इसलिए अलग interface डिज़ाइन करने की ज़रूरत नहीं भी पड़ सकती। दूसरी तरफ़, अगर एक service के ज़रिए high-level interface बनाते हैं, तो अब authentication, transaction, और exception handling भी खुद बनानी पड़ेगी। तब सवाल उठता है कि क्या इससे असल में failure modes और जटिल microservice tax ही नहीं बढ़ रहे? दूसरी ओर, कई services का एक ही DB तक पहुँचना अपने-आप में code smell भी हो सकता है। शायद यह DB कई चीज़ों के जोड़-तोड़ का नतीजा हो, और शायद services भी असल में दो-तीन तक घटाई जा सकती हों
“इससे मिलता क्या है?” के सवाल पर, API साझा DB schema इस्तेमाल करने की तुलना में बदलावों के अनुकूल कहीं अधिक होते हैं। कई सिस्टम्स पर काम करने के अनुभव से कहूँ, तो एक DB को कई services में साझा कराने वाला डिज़ाइन मैं दोबारा नहीं बनाऊँगा। 2000s के शुरुआती दौर में किसी छोटी कंपनी में यह चल गया हो, लेकिन उसके बाद मैंने ज़्यादातर इसे विफलता के रूप में ही देखा है (एक ही service के अंदर read/write path अलग होने वाले मामले अपवाद हैं)
मैं इस बात से सहमत नहीं कि DB ही interface है, इसलिए अलग डिज़ाइन की ज़रूरत नहीं। जब कई clients एक ही DB इस्तेमाल करते हैं, तो access pattern अलग होते हैं और migration की समस्या भी बड़ी हो जाती है। अंततः views, permission management वगैरह जैसी अतिरिक्त डिज़ाइन की ज़रूरत पड़ती है और maintenance का बोझ भी बढ़ता है। आदर्श स्थिति में API कहीं ज़्यादा साफ़ है। वास्तविकता यह है कि features जल्दी ship करने के दबाव में लोग shortcut के रूप में direct DB access की अनुमति दे देते हैं, लेकिन जड़ में समस्या यह है कि बहुत से लोग नई ज़रूरतों या डिज़ाइन के अनुसार पूरी चीज़ को दोबारा डिज़ाइन करने से बचते हैं
जब बदलाव की ज़रूरत पड़े, तो लक्ष्य संबंधित coordination के दायरे को न्यूनतम रखना होना चाहिए। अगर datastore की संरचना बदलनी है, तो उसके पास पहुँचने वाले हर हिस्से को नियंत्रित करना पड़ेगा; इसलिए access path जितने कम होंगे, बदलाव उतना आसान होगा। उदाहरण के लिए, वास्तविक कामकाजी माहौल में एक DB अलग किया गया, तो 40 से ज़्यादा teams को कोड बदलना पड़ा। और यह सिर्फ़ “feature requirement” की वजह से हुआ था। अगर वजह “scalability” होती, तो शायद पूरा product ही टूट जाता
आपने कई services को एक DB से जोड़ने को “code smell” कहा, लेकिन उल्टा भी सोचिए: अगर हर service को अलग physical DB देना पड़े, तो availability N से बढ़कर N की M घात जैसी हो सकती है, और व्यवहार में सिस्टम और अस्थिर हो सकता है — कम से कम अगर DB cluster के स्तर पर बात करें
जब database को query करना हो, तो अक्सर सचमुच सीधे DB को query करना ही सबसे प्रभावी होता है। अगर कई tables का data चाहिए, तो application में अलग-अलग query करके जोड़ने की बजाय join इस्तेमाल करना चाहिए। और मैं views, यहाँ तक कि stored procedures, की भी सक्रिय रूप से सिफारिश करूँगा। views एक data abstraction layer हैं, इसलिए डिज़ाइन में बहुत मदद करते हैं, और SQL code अगर अच्छे से लिखा जाए तो समझने और maintain करने में भी आसान होता है
यही वह वजह है जिससे ORM बहुत सारी समस्याएँ पैदा करते हैं। SSR environment की हर MVC view में सीधे SQL views/custom queries इस्तेमाल करना बड़े web services को प्रभावी और elegant बनाने का तरीका है। भारी काम RDBMS को करने दो, और web server SQL के परिणाम सीधे table में पास कर दे। MSSQL, Oracle जैसे legacy RDBMS में बहुत सारी built-in optimizations होती हैं। दूसरी ओर ORM एक single object model थोप देते हैं, इसलिए उनमें लचीलापन लगभग नहीं होता
stored procedures उपयोगी लग सकते हैं, लेकिन व्यवहार में language (जैसे T-SQL) की सीमाओं के कारण पूरी टीम के लिए किसी आधुनिक, परिचित भाषा में एकरूप विकास करना मुश्किल हो जाता है। मैं एक बड़े T-SQL codebase को maintain कर रहा हूँ, और version control या diagnostic tools भी खास नहीं हैं। नए लोगों का application code फिर भी पढ़ने लायक होता है, लेकिन T-SQL तो दुःस्वप्न है
मैं सहमत नहीं हूँ। आधुनिक scalability-केंद्रित architecture में joins को DB के सामने वाले backend में करना बेहतर होता है। DB को simple index lookups तक सीमित रखो, और joins backend में कराओ — इससे DB की scalability बेहतर रहती है और गति भी बढ़ सकती है। क्योंकि server instances बढ़ाना DB बढ़ाने से आसान है। अगर joins इतने बड़े data पर ही संभव हों कि वे DB में करना अनिवार्य हो जाएँ, तब architecture बदलना चाहिए। अगर join को frontend तक ले जाया जा सके, तो result caching का भी फ़ायदा मिलता है
क्या वाकई? मान लो 10,000 customers हैं और 1,000,000 orders हैं। अगर customer के 20 fields और order के 5 fields वाली tables को पूरी तरह join करके भेजोगे, तो 25,000,000 fields ट्रांसफर होंगे। अगर दो queries से अलग-अलग लाकर फिर join करो, तो orders के 5,000,000 fields और customers के 200,000 fields ही होंगे। bandwidth और performance के हिसाब से यह कहीं बेहतर है
यह नियम शुरुआत के लिए ठीक है, लेकिन यह भी अच्छी तरह समझना चाहिए कि अपवाद कब ज़रूरी हैं। जिस app पर मैं काम करता था उसमें join के कारण records ज्यामितीय रूप से बढ़ जाते थे। इसलिए queries को अलग किया गया, और network overhead से कहीं ज़्यादा लाभ result processing/filtering में मिला, जिससे वह बहुत तेज़ हो गया। बाद में सब data को JSONB में store करने वाले ढाँचे में बदला, और वह और भी बेहतर निकला
अच्छा सिस्टम डिज़ाइन कहकर भी problem domain की बिल्कुल चर्चा न होना मुझे खटकता है। सिस्टम डिज़ाइन में सबसे महत्वपूर्ण और कठिन चीज़ वह interface है जो सिस्टम अपने users को देता है। आख़िरकार software system एक तरह का सौदा ही है: “हम तुम्हें यह functionality देंगे, लेकिन बदले में तुम्हें यह structure/model समझना होगा।” interface डिज़ाइन में गलती सबसे महँगी पड़ती है, और अगर आप अपना अधिकतर समय interface पर चर्चा किए बिना बिताते हैं, तो आप सचमुच सबसे महत्वपूर्ण बात खो रहे हैं। सिस्टम के बाकी हिस्से बाद में users को छुए बिना भी बदले जा सकते हैं
“अच्छा डिज़ाइन खुद को दिखाता नहीं, और बुरा डिज़ाइन उल्टा ज़्यादा प्रभावशाली दिखता है” — इस बात से मुझे ज़मीन से जुड़ा हुआ गहरा सहमति महसूस हुई। लगता है इंजीनियरों का मूल्यांकन “जटिलता” के आधार पर होने लगा है, जिससे overengineering को बढ़ावा मिलता है। KISS principle को लंबे समय से वह मान्यता नहीं मिली जिसकी वह हकदार है
कभी-कभी मैं codebase के उन हिस्सों को पीछे मुड़कर देखता हूँ जिन पर उस समय बिना ज़्यादा सोचे आगे बढ़ गया था, और बाद में एहसास होता है कि यही अच्छे डिज़ाइन के निशान थे
दुर्भाग्य से यह सच है। ज़्यादातर लोग जटिल solutions से ज़्यादा आकर्षित होते हैं, और अगर आप सरल जवाब दें तो अयोग्य दिखने का ख़तरा भी रहता है। लेकिन व्यवहार में आसान, manage करने योग्य सरल संरचना ही पूरे project की सफलता में बड़ा योगदान देती है। हाँ, कुछ समस्याएँ स्वभाव से जटिल होती हैं, लेकिन ज़्यादातर चीज़ें तो बस सामान्य web apps ही होती हैं
schema डिज़ाइन में सबसे महत्वपूर्ण चीज़ flexibility है। क्योंकि data जमा हो जाने के बाद schema बदलना बहुत मुश्किल हो जाता है। लेकिन अगर schema को ज़रूरत से ज़्यादा flexible बना दें (सारा data JSON में ठूँस दें या EAV structure बना दें!), तो application code अंतहीन रूप से जटिल हो जाता है और अजीब performance समस्याएँ भी पैदा होती हैं। इसलिए आम तौर पर मैं ऐसे schema को पसंद करता हूँ जिसे सिर्फ़ tables देखकर ही सहज रूप से समझा जा सके कि उसका उपयोग क्या है। EAV, JSON columns/tables बार-बार देखकर सचमुच coding छोड़ देने का मन होने लगता है। निश्चित रूप से EAV के उपयोगी cases मौजूद हैं, लेकिन ज़्यादातर हालात में यह मैदान में सिर्फ़ अराजकता लाता है। N+1 समस्या, dynamic query generation, audit data को उसी DB में store करके उसे business logic का हिस्सा बना देना, जटिल Oracle environments, और क्या DB में रखना है तथा क्या app में — इसका गलत विभाजन; ऐसे हर variable डेवलपर की life quality को बहुत बुरी तरह गिरा देता है
इससे जुड़ी Bill Karwin की किताब “SQL Antipatterns” EAV pattern के ख़तरों और सीमाओं को अच्छी तरह समझाती है। फिर भी, कभी-कभी जब schema बनाना मुश्किल हो (जैसे Postgres में JSONB column), तो इसे अस्थायी उपाय की तरह इस्तेमाल किया जा सकता है, लेकिन यह best practice नहीं बन सकता। जहाँ normalization संभव हो, वहाँ हमेशा normalization चुनना बेहतर है
“audit data को उसी DB में रखने से वह आखिरकार business logic का हिस्सा बन जाता है, जो परेशानी पैदा करता है” — इस बात पर सोच रहा हूँ, तो “सही तरीका” क्या है? अलग DB? पूरी तरह स्वतंत्र storage?
“5 services को एक ही table में लिखने मत दो; 4 सिर्फ़ API call करें या event भेजें और 1 ही सीधे DB में लिखे” — इस सलाह पर, सबसे अच्छा तो यह है कि शुरुआत से ही ऐसी स्थिति न बने जहाँ 5 services को एक ही table में लिखना पड़े। अगर ऐसा हो रहा है, तो संभव है कि services के बीच logic वास्तव में काफ़ी overlap कर रहा हो। तब यह भी सोचना चाहिए कि क्या ये 5 services सचमुच अलग-अलग होनी चाहिए, या इन्हें एक में जोड़ा जा सकता है। व्यावहारिक रूप से, अलग data tables देना या refactoring करना ही समस्या का बेहतर समाधान हो सकता है
stateful/stateless का अंतर infra और development responsibility बाँटने की एक मूल कुंजी है। जब container में चीज़ें stateless चलती हैं, तो गड़बड़ होने की गुंजाइश कम होती है; fail हो जाएँ तो बस दोबारा deploy कर दो। जब तक आप DB में ऐसी गलती न कर दें जो dataset को बिगाड़ दे, तब तक ज़्यादातर चीज़ें जल्दी recover हो सकती हैं। अलग-अलग अनुभव, समय और मेहनत वाले लोग इस स्तर तक ठीक रहते हैं। लेकिन database, file storage जैसे state वाले हिस्से बिल्कुल अलग होते हैं। एक ही गलती पूरे business को ख़तरे में डाल सकती है, इसलिए इन्हें गहरे व्यावहारिक अनुभव वाले dedicated लोगों को संभालना चाहिए। DB अगर ठीक चल भी रहा हो, लेकिन backup न हो, तो वह पहले से ही बड़ा risk है। वास्तव में ऐसे क्षेत्र वे समस्याएँ हैं जिन्हें कुछ मिनट में deploy करके ठीक नहीं किया जा सकता
“bool की जगह timestamp से दिखाओ” वाली सलाह मुझे कुछ ज़्यादा व्यापक लगती है। उदाहरण के लिए is_on → true, on_at → 1023030 समझ में आता है, लेकिन is_a_bear → true, a_bear_at → 12312231231 काफ़ी अजीब लगता है। ज़्यादातर भालू किसी समय ‘भालू’ नहीं बनते… यानी यह बात सिर्फ़ कुछ विशेष परिस्थितियों में लागू होती है
मेरे हिसाब से लगभग हर मामले में boolean की जगह timestamp या integer बेहतर है। खासकर वे fields जिनमें सिर्फ़ दो स्थितियाँ होती हैं, वे अक्सर आगे चलकर “type classification” में बदल जाती हैं। उदाहरण के लिए, अगर अभी सिर्फ़ bear हो, तब भी future expansion के लिए enum type बेहतर हो सकता है। और state fields भी सिर्फ़ active/inactive तक सीमित नहीं रहते; वे suspended, deleted, paused जैसी स्थितियों तक बढ़ते हैं, जिससे booleans बढ़ते-बढ़ते चीज़ों को और उलझा देते हैं। integer बेहतर है
अगर कथन को शाब्दिक रूप से लें, तो मतलब यह हुआ कि DB में boolean इस्तेमाल करना ही smell है — और इस हिस्से से मैं सहमत हूँ। हालाँकि bool → timestamp बदल देना joins में सुविधा के कारण अक्सर काम आ सकता है, लेकिन यह कोई “complete solution” नहीं है। अगर real-time changes महत्वपूर्ण हों, तो सही चीज़ शुरू से audit table होती है। soft delete भी मुझे वैसा ही आधा-अधूरा समाधान लगता है। असली इरादा delete को रोकना होता है, लेकिन backup-recovery उससे बेहतर सुरक्षा दे सकता है
boolean type का data size छोटा होता है, इसलिए कुछ workloads (जैसे analytics के लिए बड़े data sets) में यह अधिक कुशल हो सकता है। और कभी-कभी तार्किक रूप से boolean store करना ही सही होता है। उदाहरण के लिए, process result (success/failure) को दिखाने के लिए boolean व्यावहारिक है
क्या सचमुच सिर्फ़ boolean को ही timestamp में बदलना चाहिए? isDarkTheme, paginationItems जैसी चीज़ों के लिए भी बदलाव का समय जानना उपयोगी हो सकता है। यह लगभग poor-man changelog जैसा लगता है
ऐसे मामले में Bear जैसे enum value इस्तेमाल करना बेहतर होगा
अगर आप अच्छे सिस्टम डिज़ाइन को थोड़े अधिक अमूर्त दृष्टिकोण से समझाने वाली किताब ढूँढ रहे हैं, तो John Gall की Systemantics ज़ोरदार सिफारिश करूँगा। मुझे लगता है कि एक engineer के रूप में इसे ज़रूर पढ़ना चाहिए