ज़्यादातर बैकएंड रीराइट एक जैसी जगह से शुरू होते हैं। बिज़नेस नियम कंट्रोलर में रहते हैं। कंट्रोलर ओआरएम से बात करते हैं। ओआरएम टेस्ट में घुस आता है। छह महीने बाद भुगतान नियम बदलने का मतलब होता है एचटीटीपी हैंडलर, एसक्यूएल और एक कतार उपभोक्ता छूना जिसे किसी ने याद नहीं रखा। क्लीन आर्किटेक्चर आदतें हैं जो इस बिगाड़ को धीमा करती हैं। यह धर्म नहीं है, और मुफ़्त भी नहीं।
यह लेख उन्हीं हिस्सों पर है जो असली उत्पाद दबाव में टिकते हैं: परतें, निर्भरता नियम, यूज़ केस, और सीधी बात कि कब यह सब ज़रूरत से ज़्यादा है।
असल में कौन सी समस्या हल कर रहे हो
संभालने लायक बैकएंड वह है जहाँ:
१. बिज़नेस नियम बदल सकें बिना ट्रांसपोर्ट और स्टोरेज दोबारा लिखे। २. स्टोरेज या एचटीटीपी बदल सकें बिना बिज़नेस नियम दोबारा लिखे। ३. फ़ैसलों का परीक्षण बिना डेटाबेस या पूरे वेब फ़्रेमवर्क के हो सके। ४. नए एंडपॉइंट नीति दोहराएँ न कि वैलिडेशन और साइड इफ़ेक्ट कॉपी करें।
अगर सेवा एक टेबल पर पतली सीआरयूडी है, तो शायद आप पहले से ठीक हैं। अगर सेवा पैसे, इन्वेंटरी, अनुपालन, कई चरणों वाले वर्कफ़्लो, या एक ही नियम तक कई रास्ते (एपीआई, जॉब, एडमिन टूल) संभालती है, तो संरचना मायने रखने लगती है।
साधारण भाषा में परतें
अंकल बॉब के चित्र में कई घेरे हैं। बैकएंड में चार विचारों से सालों चला जा सकता है:
| परत | क्या रखती है | उदाहरण |
|---|---|---|
| डोमेन / एंटिटी | शुद्ध बिज़नेस अर्थ | Order, Money, InvoiceStatus, जैसे नियम "रद्द ऑर्डर नहीं भेज सकते" |
| यूज़ केस / एप्लिकेशन | एक उपयोगकर्ता या सिस्टम दिखने वाली क्रिया | PlaceOrder, RefundPayment, ExpireSubscription |
| इंटरफ़ेस एडाप्टर | अंदर-बाहर अनुवाद | कंट्रोलर, प्रेजेंटर, रिपॉज़िटरी इम्प्लिमेंटेशन, मैसेज मैपर |
| फ़्रेमवर्क और ड्राइवर | वे विवरण जिन्हें बदलना हो | एक्सप्रेस/फ़ास्टएपीआई, पोस्टग्रेस, रेडिस, स्ट्राइप एसडीके, फ़ाइल सिस्टम |
सोचो नीति अंदर, विवरण बाहर। डोमेन को फ़ास्टएपीआई का पता नहीं। यूज़ केस एसक्यूएलअल्केमी सेशन ऑब्जेक्ट इम्पोर्ट नहीं करता। कंट्रोलर में रिफ़ंड का गणित नहीं बैठता।
फ़ोल्डर लेआउट जो साफ़ मैप होता है (नाम टीम पर निर्भर):
src/
domain/ # entities, value objects, domain errors
application/ # use cases, ports (interfaces)
adapters/
http/ # routes, request/response DTOs
persistence/ # repository implementations
messaging/ # consumers, publishers
main/ # composition root: wire dependencies
ठीक ये नाम ज़रूरी नहीं। ज़रूरी है जगह जहाँ नियम और वायरिंग एक फ़ाइल न बाँटें।
निर्भरता नियम
सोर्स कोड निर्भरताएँ अंदर की ओर इशारा करती हैं। बाहरी परतें भीतरी को जानती हैं। भीतरी परतें बाहरी को कभी इम्पोर्ट नहीं करतीं।
कोड रिव्यू में दिखने वाले नतीजे:
- डोमेन कोड में कोई फ़्रेमवर्क इम्पोर्ट नहीं, कोई ओआरएम मॉडल नहीं, कोई एचटीटीपी स्टेटस कोड नहीं।
- यूज़ केस पोर्ट (इंटरफ़ेस या अमूर्त टाइप) पर निर्भर करते हैं, पोस्टग्रेस या स्ट्राइप क्लास पर नहीं।
- एडाप्टर वे पोर्ट लागू करते हैं और यूज़ केस बुलाते हैं।
main(या आपका डीआई कंटेनर) ही पूरा ग्राफ़ बनाता है।
HTTP controller --> PlaceOrder use case --> OrderRepository (port)
^ ^
| |
domain types PostgresOrderRepository
गलत दिशा (आम असफलता):
PlaceOrder imports Session from ORM
PlaceOrder calls response.json(...)
Entity methods take Request objects
जैसे ही यूज़ केस वेब परत पर निर्भर हो, हर नया प्रवेश बिंदु (सीएलआई, वर्कर, ग्राफ़क्यूएल) को एचटीटीपी नकली बनाना पड़ता है। आर्किटेक्चर चुपचाप यहीं मरता है।
पोर्ट और एडाप्टर, जादू नहीं
पोर्ट छोटा इंटरफ़ेस है जिसकी एप्लिकेशन को ज़रूरत है:
// application/ports/order-repository.ts
export interface OrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}
export interface PaymentGateway {
charge(input: ChargeInput): Promise<ChargeResult>;
}
यूज़ केस डोमेन ऑब्जेक्ट और पोर्ट का ऑर्केस्ट्रेशन करता है:
// application/place-order.ts
export class PlaceOrder {
constructor(
private readonly orders: OrderRepository,
private readonly payments: PaymentGateway,
private readonly clock: Clock,
) {}
async execute(cmd: PlaceOrderCommand): Promise<PlaceOrderResult> {
const order = Order.create(cmd, this.clock.now());
const charge = await this.payments.charge({
amount: order.total,
customerId: cmd.customerId,
});
if (!charge.ok) {
throw new PaymentFailed(charge.reason);
}
order.markPaid(charge.id, this.clock.now());
await this.orders.save(order);
return { orderId: order.id, paymentId: charge.id };
}
}
एडाप्टर असली तकनीक से पोर्ट लागू करता है:
// adapters/persistence/postgres-order-repository.ts
export class PostgresOrderRepository implements OrderRepository {
constructor(private readonly db: Pool) {}
async findById(id: string): Promise<Order | null> {
const row = await this.db.query(/* ... */);
return row ? mapRowToOrder(row) : null;
}
async save(order: Order): Promise<void> {
await this.db.query(/* insert or update */);
}
}
एचटीटीपी हैंडलर पतला रहता है: इनपुट पार्स करो, placeOrder.execute बुलाओ, परिणाम या डोमेन त्रुटि को स्टेटस कोड में मैप करो। मैपिंग किनारे पर रहती है ताकि यूज़ केस नकली (फ़ेक) के साथ टेस्ट हो।
ऐप की रीढ़: यूज़ केस
क्लीन आर्किटेक्चर से अगर एक ही बात लेनी हो तो यूज़ केस लो।
यूज़ केस वह है:
- जिसका नाम बिज़नेस क्या करता है पर हो (
CancelSubscription, न किUpdateSubscriptionRow) - एक मुख्य ट्रांज़ैक्शन सीमा (या स्पष्ट विफलता बिंदुओं वाला बहु-चरण वर्कफ़्लो)
- जहाँ अक्सर नीति वाली अनुमति जाँच रहती है, न कि फ़्रेमवर्क मिडलवेयर की छोटी बातें
- जिस पर यूनिट टेस्ट सबसे सख्त लगते हैं
कंट्रोलर उबाऊ हो जाते हैं। यह गुण है। उबाऊ कंट्रोलर मतलब दिलचस्प बग नामित क्रियाओं में रहते हैं जिन्हें व्हाइटबोर्ड पर गिना जा सकता है।
कमांड बनाम क्वेरी
पूरा सीक्यूआरएस ज़रूरी नहीं। हल्का विभाजन मदद करता है:
- कमांड स्थिति बदलते हैं:
PlaceOrder,RefundPayment - क्वेरी स्थिति पढ़ती हैं:
GetOrder,ListInvoicesForCustomer
क्वेरी भारी डोमेन फ़ैक्टरी छोड़ सकती हैं और रीड मॉडल या साधारण एसक्यूएल प्रोजेक्शन पर जा सकती हैं। हर पढ़ाई को समृद्ध एग्रीगेट से गुज़ारना आम ओवरकिल है।
वैलिडेशन कहाँ जाए
| जाँच का प्रकार | कहाँ |
|---|---|
| आकार / टाइप / ज़रूरी फ़ील्ड | किनारे का डीटीओ या स्कीमा (ज़ोड, पाइडैंटिक, बीन वैलिडेशन) |
| बिज़नेस इनवेरिएंट | डोमेन एंटिटी या डोमेन सर्विस |
| प्रक्रिया नियम (कौन कर सकता है, किस स्थिति में) | यूज़ केस |
| इन्फ़्रा सीमा (पेलोड आकार) | फ़्रेमवर्क / गेटवे |
"ऑर्डर कुल धनात्मक होना चाहिए" सिर्फ़ कंट्रोलर में न छोड़ो। "जेएसओएन फ़ील्ड स्ट्रिंग हो" एंटिटी में न ठूसो।
टेस्ट: असली फ़ायदा
क्लीन आर्किटेक्चर किराया टेस्ट में चुकाता है।
- डोमेन टेस्ट: शुद्ध फ़ंक्शन और एंटिटी, मॉक की ज़रूरत नहीं।
- यूज़ केस टेस्ट: पोर्ट के लिए मेमोरी फ़ेक। तेज़। निश्चित।
- एडाप्टर टेस्ट: कम; रिपॉज़िटरी के लिए सीआई में असली पोस्टग्रेस, या गेटवे के लिए कॉन्ट्रैक्ट टेस्ट।
- एचटीटीपी टेस्ट: धुआँ और मैपिंग, पूरा नियम सेट दोबारा नहीं।
यूज़ केस टेस्ट का आकार:
test("rejects cancel when already shipped", async () => {
const orders = new InMemoryOrderRepository([
Order.rehydrate({ id: "o1", status: "shipped" }),
]);
const uc = new CancelOrder(orders, fixedClock);
await expect(uc.execute({ orderId: "o1", reason: "changed mind" }))
.rejects.toBeInstanceOf(OrderNotCancellable);
});
अगर हर मतलब का टेस्ट वेब फ़्रेमवर्क और डेटाबेस चालू करता है, तो आर्किटेक्चर अभी सुरक्षा नहीं दे रहा। सस्ते टेस्ट संभव बनाने वाला वही निर्भरता नियम है।
साझा कर्नेल, मॉड्यूल और कई सेवाएँ
क्लीन आर्किटेक्चर एक डिप्लॉयएबल के अंदर है। यह सेवा सीमाओं की जगह नहीं लेता।
- प्रति सेवा एक बाउंडेड कॉन्टेक्स्ट (या बड़ा मॉड्यूल) यूज़ केस सुसंगत रखता है।
- कई माइक्रोसर्विस पर साझा "ईश्वर डोमेन" लाइब्रेरी अक्सर टाइप का वितरित मोनोलिथ बन जाती हैं।
- एक-दूसरे की एंटिटी इम्पोर्ट करने से बेहतर शेयर्ड नथिंग या संस्करणित इवेंट।
मॉड्यूलर मोनोलिथ में पैकेज को सेवा जैसे मानो: दूसरे मॉड्यूल को उसकी एप्लिकेशन एपीआई से बुलाओ, उसकी टेबल में हाथ न डालो।
कब यह ओवरकिल है
सरल डिज़ाइन चुनो जब इनमें से ज़्यादातर सच हों:
१. एक डेवलपर, छोटी उम्र, या प्रोटोटाइप जो अगली तिमाही मर सकता है। २. सीआरयूडी आकार का काम: सूची, पढ़ो, कॉलम अपडेट, लगभग कोई बहु-चरण नियम नहीं। ३. एक डेटाबेस, एक एचटीटीपी एपीआई, उन्हीं नियमों का दूसरा उपभोक्ता नहीं। ४. टीम फ़ोल्डर संरचना से उत्पाद से ज़्यादा लड़ रही हो। ५. ऐसी चीज़ों के इंटरफ़ेस बना रहे हो जिन्हें कभी नहीं बदलोगे और टेस्ट में फ़ेक भी नहीं करोगे।
बहुत दूर चले जाने के लक्षण:
- एक जगह इस्तेमाल एक पोस्टग्रेस टेबल के पाँच इंटरफ़ेस
- मैपर जो एक जैसे डीटीओ के मैपर मैप करते हैं
- यूज़ केस फ़ाइलें जो बिना डोमेन लॉजिक के सिर्फ़ रिपॉज़िटरी बुलाती हैं
- नए सदस्य एक फ़ील्ड भेजने से पहले "आर्किटेक्चर" सीखने में एक हफ़्ता लगाते हैं
ईमानदार बीच का रास्ता जो कई टीमें अपनाती हैं:
| स्थिति | लेआउट |
|---|---|
| आंतरिक टूल, पतली एपीआई | रूट + सर्विस फ़ंक्शन + एसक्यूएल |
| बढ़ती उत्पाद सेवा | कंट्रोलर, एप्लिकेशन सर्विस, रिपॉज़िटरी, जहाँ नियम इकट्ठे हों वहाँ डोमेन |
| पैसे / अनुपालन / बहु-प्रवेश वर्कफ़्लो | पूरे पोर्ट, यूज़ केस, डोमेन एंटिटी |
खड़े शुरू करो। जब दूसरा प्रवेश बिंदु आए, या नियम एचटीटीपी से टेस्ट करना मुश्किल हो, तब यूज़ केस निकालो। टूडू ऐप के पहले दिन बीस खाली परतें न खड़ी करो।
कोड रिव्यू तक टिकने वाले व्यावहारिक नियम
१. डोमेन और यूज़ केस में फ़्रेमवर्क इम्पोर्ट नहीं। ज़रूरत हो तो सीआई में ग्रेप।
२. महत्वपूर्ण स्थिति बदलने वाली प्रत्येक बिज़नेस क्रिया के लिए एक यूज़ केस क्लास (या फ़ंक्शन)।
३. पोर्ट छोटे रहें, एप्लिकेशन पक्ष के मालिक, एडाप्टर लागू करें।
४. कंपोज़िशन रूट सब वायर करे। कंट्रोलर रिक्वेस्ट हैंडलर की गहराई में इन्फ़्रा new न करें जहाँ बचा जा सके।
५. त्रुटियाँ डोमेन-नामित हों (InsufficientStock, SubscriptionInactive)। एचटीटीपी पर एक बार किनारे मैप करो।
६. पढ़ाई लिखने से सरल हो सकती है। हर जीईटी को समृद्ध एग्रीगेट से न गुज़ारो।
७. बेदर्दी से नाम बदलो। पंद्रह काम करने वाला OrderService यूज़ केस परत नहीं; कबाड़ का दराज है।
८. निर्भरता नियम रीडमी में पाँच पंक्तियों में लिखो। लंबे आर्किटेक्चर विकी सड़ जाते हैं।
न्यूनतम कंपोज़िशन रूट रेखाचित्र:
// main/server.ts
const pool = new Pool(env.DATABASE_URL);
const orders = new PostgresOrderRepository(pool);
const payments = new StripePaymentGateway(env.STRIPE_KEY);
const placeOrder = new PlaceOrder(orders, payments, systemClock);
app.post("/orders", httpPlaceOrder(placeOrder));
वायरिंग बदसूरत हो सकती है। बिज़नेस कोड नहीं होना चाहिए।
आम स्टैक में कैसा दिखता है
बिना "क्लीन" नाम वाले फ़्रेमवर्क अपनाए भी वही विचार लागू हो सकते हैं:
- नोड / टाइपस्क्रिप्ट: क्लास या फ़ंक्शन के रूप में यूज़ केस; इंटरफ़ेस के रूप में पोर्ट; प्रिज़्मा/टाइपओआरएम एडाप्टर में; एचटीटीपी किनारे पर ज़ोड।
- पायथन: कॉलएबल या क्लास यूज़ केस; पोर्ट के लिए
Protocol; अगर मॉडल गंदा हो तो एसक्यूएलअल्केमी मॉडल डोमेन एंटिटी न बनें; रिक्वेस्ट डीटीओ के लिए पाइडैंटिक। - जावा / कोटलिन: पैकेज-बाय-फ़ीचर या हेक्सागोनल मॉड्यूल; स्प्रिंग किनारे पर (
@RestController,@Repositoryइम्प्ल); शुद्ध यूनिट टेस्ट चाहो तो बिना स्प्रिंग एनोटेशन के डोमेन जार। - गो: इंटरफ़ेस उपभोक्ता के पास (यूज़ केस पैकेज); ठोस पोस्टग्रेस स्ट्रक्ट
internal/postgresमें;cmdवायर करे।
ब्लॉग की ब्रांड से ज़्यादा मायने रखता है कौन किस पर निर्भर है।
छोटी निर्णय चेकलिस्ट
दूसरी परत जोड़ने से पहले पूछो:
- क्या यह नियम एक से अधिक ट्रांसपोर्ट से पहुँचेगा?
- क्या नियम कंट्रोलर में रहे तो टेस्ट करना कठिन है?
- क्या हम यह वैलिडेशन वर्कर में दोहराने वाले हैं?
- क्या जूनियर इंजीनियर व्यवहार बिज़नेस नाम से ढूँढ लेता है?
हाँ हो तो यूज़ केस और पोर्ट निकालो। नहीं हो तो कोड उबाऊ छोड़ो और टिकट बंद करो।
क्लीन आर्किटेक्चर बैकएंड को तब संभालता रखता है जब बिज़नेस नियमों को फ़्रेमवर्क चर्न से बचाए और उन्हें सस्ते में टेस्ट करने दे। यह रस्म बन जाता है जब हर फ़ाइल वह इंटरफ़ेस हो जो दूसरी इम्प्लिमेंटेशन का इंतज़ार कर रही हो जो कभी नहीं आती। जहाँ दर्द असली हो वहाँ निर्भरता नियम लगाओ, किनारे पतले रखो, और यूज़ केस को वही नाम दो जो उत्पाद पहले से बोलता है।
