এই পর্বে যা শিখবেন
- MVC, MVP, MVVM-এর পার্থক্য আর React এদের মধ্যে কোথায় বসে
- একটি জট পাকানো handler ভেঙে controller, service আর repository বানানো
- Layer অনুযায়ী folder বনাম feature অনুযায়ী folder — কখন কোনটি
- Fat controller, god component আর circular dependency চেনা ও সারানো
- Monolith, modular monolith, microservices, BFF আর API gateway-এর সৎ হিসাব
- Express, Django, Laravel আর Next.js-এ এই ধারণাগুলো কোথায় পড়ে
১ · Separation of concerns — রেস্তোরাঁয় কেন একজনই সব করেন না
বাজারঘর নামের কাল্পনিক অনলাইন দোকানটি আমরা এই কোর্স জুড়ে বানাব। ধরুন প্রথম সপ্তাহেই একটি মাত্র ফাংশনে “কার্টে যোগ করুন” লিখে ফেললেন। সেই ফাংশন request পড়ে, stock দেখে, দাম হিসাব করে, database-এ লেখে, আবার HTML-ও বানায়। প্রথম দিন সব ঠিকঠাক চলে।
তিন মাস পরে তিনটি বদল এলো। মালিক বললেন, ঈদে ছাড় দিতে হবে। ডিজাইনার বললেন, পাতার চেহারা বদলাবে। আপনি ঠিক করলেন, database বদলাবেন। তিনটি বদলের জন্যই ওই একই ফাংশন খুলতে হচ্ছে। একটি বদল করতে গিয়ে আরেকটি ভাঙছে। এটাই architecture-এর আসল সমস্যা। Architecture মানে কোড কোথায় থাকবে, কে কাকে চিনবে, আর কোন বদল কতটুকু কোড ছোঁবে — এই সিদ্ধান্তগুলো।
একটি রেস্তোরাঁর কথা ভাবুন। ওয়েটার অর্ডার নেন, বাবুর্চি রান্না করেন, ভাঁড়ারি গুদাম সামলান। ওয়েটার জানেন না মসলা কোন তাকে আছে। বাবুর্চি জানেন না কোন টেবিলে কে বসেছেন। তাই নতুন ওয়েটার এলে রান্না বদলায় না, আর নতুন রেসিপি এলে টেবিল সাজানোর নিয়ম বদলায় না। কোডেও আমরা ঠিক এই ভাগটুকুই চাই।
Separation of concerns: আলাদা দায়িত্ব আলাদা জায়গায়। পরীক্ষা করার সহজ প্রশ্ন: “এই কোড বদলানোর কারণ কয়টি?” কারণ একাধিক হলে কোডটি ভাগ করার সুযোগ আছে। ভালো ভাগে একসাথে বদলায় এমন কোড কাছাকাছি থাকে (cohesion বেশি), আর এক অংশের ওপর আরেক অংশের নির্ভরতা কম থাকে (coupling কম)।
“Separation of concerns” কথাটি সাধারণত Edsger Dijkstra-র ১৯৭৪ সালের একটি লেখার সাথে যুক্ত করা হয়। ১৯৭২ সালে David Parnas তাঁর প্রবন্ধে দেখান, module ভাগ করা উচিত “কোন সিদ্ধান্ত বদলাতে পারে” সেটি লুকানোর জন্য। আজকের প্রায় সব pattern এই দুটি চিন্তারই নানা রূপ। Coupling আর cohesion-এর তত্ত্ব Coupling ও cohesion পর্বে আছে, এখানে আমরা শুধু ব্যবহার করব।
একটি সতর্কতা শুরুতেই। Pattern কোনো ধর্ম নয়। পঞ্চাশ লাইনের script-এ চার স্তর বানালে সেটি উল্টো বোঝা। Pattern একটি দাম, যা দিয়ে ভবিষ্যতের সহজ বদল কেনা হয়। তাই প্রতিটি pattern-এর সাথে আমরা দামটিও দেখব।
২ · MVC — desktop থেকে web-এ এসে অর্থ বদলাল
MVC (Model-View-Controller) সবচেয়ে পুরোনো আর সবচেয়ে বেশি ভুল বোঝা pattern. তিনটি ভাগ এমন: model হলো data আর তার নিয়ম, view হলো যা দেখানো হয়, controller হলো ব্যবহারকারীর কাজ গ্রহণ করে বাকি দুজনকে চালানোর অংশ।
নরওয়ের প্রকৌশলী Trygve Reenskaug ১৯৭৮–৭৯ সালে Xerox PARC-এ অতিথি গবেষক থাকার সময় Smalltalk-এ কাজ করতে গিয়ে ধারণাটি গড়ে তোলেন। ১৯৭৯ সালের মে আর ডিসেম্বরের দুটি report আজও অনলাইনে আছে। কোন Smalltalk সংস্করণে প্রথম চলেছিল, সেই খুঁটিনাটি নিয়ে উৎসগুলো এক কথা বলে না। পরে Smalltalk-80-এর মাধ্যমে নকশাটি ছড়ায়। বিস্তারিত ইতিহাস আর observer-এর মেকানিজম MVC, MVP আর MVVM পর্বে আছে।
Desktop-এর মূল MVC-তে view আর model একই কম্পিউটারে, একই সময়ে বেঁচে থাকে। Model বদলালে সে নিজে সব view-কে খবর দেয় (observer), আর view নিজেকে আঁকে। ওয়েবে এটি অসম্ভব। Browser আর server আলাদা, HTTP-তে server নিজে থেকে পাতা বদলাতে পারে না। তাই ১৯৯০-এর দশকের শেষে Java-র জগৎ থেকে একটি রূপান্তরিত নকশা আসে, আর Rails (২০০৪) সেটিকে বিখ্যাত করে।
| প্রশ্ন | Desktop MVC | Web MVC |
|---|---|---|
| কত সময় বাঁচে | অ্যাপ খোলা যতক্ষণ | একটি request যতক্ষণ |
| Model বদলের খবর | observer দিয়ে view-কে জানায় | জানায় না; পরের request-এ নতুন পাতা |
| Controller-এর কাজ | keyboard আর mouse-এর ঘটনা ধরা | HTTP request ধরে model ও view জোড়া |
| View-এর ফল | পর্দায় আঁকা | HTML বা JSON লেখা |
নিচের ডায়াগ্রামে বাজারঘরের একটি পাতার request ধাপে ধাপে দেখুন। প্রতিটি ধাপে কার হাতে কী data আছে, সেটি খেয়াল করুন।
- Browser
GETক্রেতা - Router
/products/:idকোন ঠিকানা - Controller
show(1)কাজের সমন্বয় - Model
find(1)data আর নিয়ম - View
HTMLচেহারা
GET /products/1 HTTP/1.1 Host: bazarghar.example
pattern: /products/:id
id: "1"
ডাকবে: ProductController.show
id = Number("1") // 1
ProductModel.find(1)
SELECT id, name, taka, stock FROM products WHERE id = $1 -- $1 = 1 { id: 1, name: "Chal 5kg", taka: 420, stock: 3 }
<h1>Chal 5kg</h1><p>420 taka, in stock</p>
HTTP/1.1 200 OK
Content-Type: text/html
<h1>Chal 5kg</h1><p>420 taka, in stock</p>
- ধাপ ১ — অনুরোধ। ক্রেতা পণ্যের পাতা খুললেন। Browser-এর কাছে শুধু একটি ঠিকানা আছে:
/products/1. - ধাপ ২ — Router. Route-এর তালিকা মিলিয়ে ঠিকানা থেকে
idপাওয়া গেল। এটি এখনো লেখা"1", সংখ্যা নয়। - ধাপ ৩ — Controller. Controller ইনপুট সংখ্যায় বদলে model-কে ডাকল। সে জানে কাকে ডাকতে হবে, কিন্তু data কোথায় থাকে জানে না।
- ধাপ ৪ — Model. Model database থেকে একটি row আনল:
nameহলো Chal 5kg,takaহলো 420,stockহলো 3. Query-তে মান আলাদা করে পাঠানো হয়েছে, তাই SQL injection-এর পথ বন্ধ। - ধাপ ৫ — View. Stock 3, তাই view লিখল “in stock”. View শুধু দেখানোর সিদ্ধান্ত নেয়, stock কমানোর মতো কোনো কাজ করে না।
- ধাপ ৬ — উত্তর। Controller HTML-টি status 200 দিয়ে ফেরত পাঠাল। Request শেষ, এই তিন অংশের স্মৃতিতে আর কিছু থাকে না। পরের ক্লিকে সব নতুন করে শুরু হবে। এটাই desktop MVC-র সাথে বড় পার্থক্য।
এবার একই গঠন নিজে চালিয়ে দেখুন। কোনো framework নেই, শুধু তিনটি অংশ আর একটি ছোট router. কোড বদলে নতুন ঠিকানা বা নতুন পণ্য যোগ করতে পারেন।
// Model: data আর তা খোঁজার নিয়ম। HTTP বা HTML চেনে না।
const ProductModel = {
rows: [
{ id: 1, name: "Chal 5kg", taka: 420, stock: 3 },
{ id: 2, name: "Dal <b>sale</b>", taka: 140, stock: 0 },
],
find(id) { return this.rows.find((r) => r.id === id) ?? null; },
};
// View: data থেকে লেখা। কোনো সিদ্ধান্ত data বদলায় না।
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<");
const ProductView = {
page: (p) => `<h1>${esc(p.name)}</h1><p>${p.taka} taka, ` +
`${p.stock > 0 ? "in stock" : "stock out"}</p>`,
notFound: (id) => `<h1>404</h1><p>No product ${id}</p>`,
};
// Controller: দুজনকে জোড়ে।
function show(id) {
const product = ProductModel.find(id);
if (!product) return { status: 404, html: ProductView.notFound(id) };
return { status: 200, html: ProductView.page(product) };
}
// Router: ঠিকানা থেকে controller.
function handle(url) {
const m = url.match(/^\/products\/(\d+)$/);
return m ? show(Number(m[1])) : { status: 404, html: "<h1>No route</h1>" };
}
for (const url of ["/products/1", "/products/2", "/products/9", "/about"]) {
const r = handle(url);
console.log(url, "->", r.status, r.html);
}আউটপুট:
/products/1 -> 200 <h1>Chal 5kg</h1><p>420 taka, in stock</p>
/products/2 -> 200 <h1>Dal <b>sale</b></h1><p>140 taka, stock out</p>
/products/9 -> 404 <h1>404</h1><p>No product 9</p>
/about -> 404 <h1>No route</h1>
দ্বিতীয় লাইনে পণ্যের নামের <b> ট্যাগ < হয়ে গেছে। View-এর escape করার দায়িত্ব এখানেই, না হলে দোকানির লেখা নাম দিয়ে XSS (পাতায় ক্ষতিকর script ঢোকানোর আক্রমণ) হতে পারে। আসল template engine আর React এটি নিজে থেকেই করে। তাত্ত্বিক ব্যাখ্যা আছে XSS ও CSP পর্বে। আর খেয়াল করুন, “stock out” লেখার সিদ্ধান্ত বদলাতে শুধু view ছুঁতে হবে, “কত দাম” বদলাতে শুধু model.
৩ · MVP, MVVM আর React — UI = f(state)
Desktop আর মোবাইল UI-তে MVC-র পরে দুটি বড় রূপ আসে। দুটিরই লক্ষ্য view-কে এত পাতলা করা, যাতে পর্দা ছাড়াই আচরণ test করা যায়।
MVP — Presenter বলে দেয়
MVP-তে view একদম বোকা। Presenter প্রতিটি বদলের পরে view-কে নির্দেশ দেয়: “এই লেখা বসান, ওই button বন্ধ করুন।” ১৯৯০-এর দশকে Taligent-এর কাজ থেকে নামটি ছড়ায়।
বেশি দেখা যায়: পুরোনো Android, WinFormsMVVM — Binding মিলিয়ে রাখে
MVVM-তে view model হলো পর্দার জন্য তৈরি data. Data binding সেটিকে view-এর সাথে জুড়ে রাখে। John Gossman ২০০৫ সালে WPF-এর জন্য নকশাটি বর্ণনা করেন।
কাছাকাছি: Vue, Angular, WPF, SwiftUIReact — State থেকে UI
React নিজেকে MVC বা MVVM বলে না। Component state পড়ে UI বর্ণনা করে, আর data একদিকেই নামে।
পর্ব ০৭ থেকে হাতে-কলমেদুইমুখী binding সুবিধার, কিন্তু একটি ঝুঁকি আছে। Screen বড় হলে একটি বদল আরেকটি বদল ঘটায়, সেটি আরেকটি। “এই মানটি কে বদলাল?” প্রশ্নের উত্তর খুঁজতে অনেক জায়গা দেখতে হয়। Facebook-এর দল ২০১৪ সালের দিকে Flux নামে একটি নকশা দেখায়, যেখানে data একদিকেই ঘোরে। এই চিন্তা থেকে পরে Redux (২০১৫) আসে। এর সংক্ষিপ্ত রূপ হিসেবে সম্প্রদায়ে একটি সূত্র বলা হয়: UI = f(state). এটি React-এর কোনো দাপ্তরিক সূত্র নয়, তবে চিন্তাটি পরিষ্কার করে: একই state মানে একই UI.
চায়ের দোকানের হিসাব খাতার কথা ভাবুন। খাতাই একমাত্র সত্য। কেউ চা খেলে দোকানি সরাসরি বোর্ডের সংখ্যা মোছেন না। তিনি খাতায় একটি লাইন লেখেন, তারপর খাতা দেখে বোর্ডটি নতুন করে লেখেন। বোর্ড ভুল মনে হলে শুধু খাতা দেখলেই চলে। এখানে খাতা হলো state, লাইন লেখা হলো action, আর বোর্ড হলো UI.
নিচের ডায়াগ্রামে বাজারঘরের “+” button দুইবার চাপা হচ্ছে। প্রতিটি ধাপে ডান দিকের state আর নিচের UI মিলিয়ে দেখুন।
1function reducer(state, a) {2 if (a.type === 'add') return {3 qty: state.qty + 1, total: state.total + a.taka };4 return state;5}6const view = (s) => `Cart: ${s.qty} / Tk ${s.total}`;7plus.onclick = () => dispatch({ type: 'add', taka: 420 });
| action | |
| qty | |
| total | |
| render |
- প্রথম render. State-এ
qty0,total0.viewফাংশন থেকে পর্দায় এলোCart: 0 / Tk 0. Render সংখ্যা ১। - ক্লিক। ক্রেতা “+” চাপলেন। Button পর্দার সংখ্যা ছোঁয় না। সে শুধু জানায় কী ঘটেছে: একটি action, দাম 420. পর্দা এখনো আগের মতো।
- Reducer. Reducer পুরোনো state থেকে একটি নতুন state বানাল: 0 + 1 = 1, 0 + 420 = 420. পর্দা এখনো বদলায়নি, কারণ render হয়নি।
- দ্বিতীয় render. State বদলেছে, তাই
viewআবার চলল। পর্দায়Cart: 1 / Tk 420. কেউ হাতে পাতার DOM (browser-এ পাতার object-এর গাছ) বদলায়নি। Render সংখ্যা ২। - আবার ক্লিক। একই action, একই পথ। ব্যতিক্রম নেই, তাই ভুল খোঁজার জায়গাও কম।
- Reducer আবার। 1 + 1 = 2, 420 + 420 = 840. নতুন state তৈরি হলো, পর্দা তখনো
Tk 420দেখাচ্ছে। - তৃতীয় render. পর্দায়
Cart: 2 / Tk 840. পুরো চক্রটি: ঘটনা → action → reducer → state → UI. তীর একদিকেই ঘোরে।
একই ধারণা এবার চালানো যায় এমন ছোট store-এ। এখানে বাড়তি একটি জিনিস আছে: state না বদলালে render হয় না।
function createStore(reducer, state) {
const listeners = [];
return {
getState: () => state,
subscribe: (fn) => listeners.push(fn),
dispatch(action) {
const next = reducer(state, action);
if (next === state) return; // বদল নেই, render নেই
state = next;
listeners.forEach((fn) => fn());
},
};
}
function reducer(state, a) {
if (a.type === "add") return { qty: state.qty + 1, total: state.total + a.taka };
if (a.type === "clear" && state.qty > 0) return { qty: 0, total: 0 };
return state;
}
const view = (s) => `Cart: ${s.qty} / Tk ${s.total}`; // UI = f(state)
const store = createStore(reducer, { qty: 0, total: 0 });
let renders = 0;
const render = () => console.log(`render ${++renders}:`, view(store.getState()));
store.subscribe(render);
render();
const before = store.getState();
store.dispatch({ type: "add", taka: 420 });
store.dispatch({ type: "add", taka: 420 });
store.dispatch({ type: "unknown" });
store.dispatch({ type: "clear" });
store.dispatch({ type: "clear" });
console.log("old state untouched:", before, "| total renders:", renders);আউটপুট:
render 1: Cart: 0 / Tk 0
render 2: Cart: 1 / Tk 420
render 3: Cart: 2 / Tk 840
render 4: Cart: 0 / Tk 0
old state untouched: { qty: 0, total: 0 } | total renders: 4
পাঁচটি dispatch হলো, কিন্তু render বাড়ল মাত্র তিনটি। unknown আর দ্বিতীয় clear-এ reducer একই state ফেরত দিয়েছে, তাই UI চুপ। পুরোনো state-ও অক্ষত আছে, কারণ reducer কখনো state-এর ভেতরে হাত দেয়নি। React-এর useState আর useReducer-ও একই চিন্তায় চলে, তবে আসল React-এর সময়সূচি অনেক জটিল। সেটি পর্ব ১০ আর পর্ব ১৭-তে দেখবেন।
৪ · Layered architecture — controller, service, repository
আজকের বেশিরভাগ backend HTML বানায় না, JSON ফেরত দেয়। তখন MVC-র “view” প্রায় হারিয়ে যায়, আর “model” একা অনেক ভারী হয়ে পড়ে। তাই কাজের নকশাটি হয়েছে layered architecture: তিনটি স্তর, প্রত্যেকটির একটি মাত্র দায়িত্ব।
তিন স্তর আর নির্ভরতার দিক
Calls go down; lower layers never know the layers above them
| প্রশ্ন বা কাজ | স্তর | কেন |
|---|---|---|
req.body পড়া, status 201 দেওয়া | Controller | HTTP-র ভাষা |
ইনপুটের আকার যাচাই (qty সংখ্যা কি না) | Controller বা middleware | দরজার পাহারা |
| Stock-এর বেশি কেনা যাবে না | Service | ব্যবসার নিয়ম |
| ঈদে ১০% ছাড় | Service | ব্যবসার নিয়ম |
SELECT … WHERE id = $1 | Repository | data আনার কৌশল |
| “এই অর্ডার এই ক্রেতার কি না” যাচাই | Service | Route বদলালেও নিয়ম থাকবে |
Business logic এমন নিয়ম, যা আপনার দোকানের মালিক মুখে বলতে পারেন, কোনো প্রযুক্তির নাম না নিয়েই: “স্টকের বেশি বিক্রি করা যাবে না।” এই নিয়ম এমন জায়গায় রাখুন, যেখান থেকে web, মোবাইল API, cron job আর admin panel সবাই একই কোড ডাকতে পারে। সেটিই service layer, অথবা বড় নকশায় domain object. React component-এ রাখলে সেটি শুধু দেখানোর সুবিধার জন্য চলে; আসল যাচাই server-এ হতেই হবে, কারণ browser-এর কোড ব্যবহারকারী বদলাতে পারেন।
এবার একই “কার্টে যোগ করুন” দুইভাবে লিখে test করে দেখুন। প্রথমটি জট পাকানো, দ্বিতীয়টিতে নিয়ম আলাদা। Service-কে repository বাইরে থেকে দেওয়া হচ্ছে, এই কৌশলের নাম dependency injection.
const assert = require("node:assert/strict");
// জট পাকানো: HTTP, নিয়ম আর data এক ফাংশনে
const db = { stock: { chal: 3 }, price: { chal: 42000 }, carts: {} };
function tangledHandler(req, res) {
const { id, qty } = req.body;
if (qty < 1) return res.status(400).json({ error: "BAD_QTY" });
if (qty > db.stock[id]) return res.status(409).json({ error: "OUT_OF_STOCK" });
db.carts[req.user] = { id, qty };
res.status(201).json({ total: qty * db.price[id] });
}
// test করতে নকল req, নকল res আর আসল db — তিনটিই লাগে
const seen = {};
const fakeRes = { status(c) { seen.code = c; return this; }, json(b) { seen.body = b; } };
tangledHandler({ user: "u1", body: { id: "chal", qty: 2 } }, fakeRes);
console.log("tangled:", seen.code, seen.body, "db.carts:", db.carts);
// স্তরে ভাগ করা: service শুধু নিয়ম জানে, repo বাইরে থেকে আসে
function addItem(repo, user, id, qty) {
if (qty < 1) throw new Error("BAD_QTY");
const p = repo.product(id);
if (qty > p.stock) throw new Error("OUT_OF_STOCK");
repo.save(user, { id, qty });
return { total: qty * p.price };
}
// test-এ HTTP নেই, database নেই: নকল repo-ই যথেষ্ট
const saved = [];
const fakeRepo = { product: () => ({ stock: 3, price: 42000 }), save: (u, c) => saved.push(c) };
assert.equal(addItem(fakeRepo, "u1", "chal", 2).total, 84000);
assert.throws(() => addItem(fakeRepo, "u1", "chal", 4), /OUT_OF_STOCK/);
assert.throws(() => addItem(fakeRepo, "u1", "chal", 0), /BAD_QTY/);
console.log("layered: 3 checks passed, saved:", saved.length);আউটপুট:
tangled: 201 { total: 84000 } db.carts: { u1: { id: 'chal', qty: 2 } }
layered: 3 checks passed, saved: 1
দাম ৪২,০০০ পয়সা, মানে ৪২০ টাকা; দুটি নিলে 84000. টাকা পূর্ণসংখ্যা পয়সায় রাখলে দশমিকের গোলমাল থাকে না। জট পাকানো কোড test করতে নকল req আর নকল res বানাতে হলো, আর global db সত্যিই বদলে গেল। পরের test-এ সেই ময়লা থেকে যাবে। স্তরে ভাগ করা কোডে তিনটি নিয়মই সরাসরি ডাকা গেল, আর saved 1 বলছে ব্যর্থ দুটি চেষ্টায় কিছুই লেখা হয়নি।
বাস্তবে একটি controller ভাঙা এক লাফে হয় না। প্রতিটি ধাপে কোড চালু রেখে ধীরে ভাঙতে হয়। নিচের ধাপগুলো দেখুন:
async function addItem(req, res) {
const { productId, qty } = req.body;
const p = await db.query('SELECT ... $1', [productId]);
if (qty > p.stock) return res.status(409).json(...);
await db.query('INSERT ...', [...]);
res.status(201).json({ total: qty * p.price });
}
const p = await repo.findProduct(productId); if (qty > p.stock) return res.status(409).json(...); await repo.saveItem(userId, productId, qty);
async addItem(userId, productId, qty) {
const p = await repo.findProduct(productId);
if (qty > p.stock) throw new CartError('OUT_OF_STOCK');
await repo.saveItem(userId, productId, qty);
return { total: qty * p.price };
}
async addItem(req, res) {
const { productId, qty } = req.body ?? {};
try {
const cart = await service.addItem(userId, productId, qty);
res.status(201).json(cart);
} catch (e) {
if (!(e instanceof CartError)) throw e;
res.status(STATUS[e.code]).json({ error: e.code });
}
}
await assert.rejects(
createCartService(fakeRepo(3)).addItem('u1', 'p1', 4),
{ code: 'OUT_OF_STOCK' });
- ধাপ ১ — শুরুর অবস্থা। Controller-এর ভেতরে SQL, stock-এর নিয়ম আর response, সব মিশে আছে। আগে যা যা করছে তার একটি বাইরের test লিখে নিন, যাতে ভাঙলে ধরা পড়ে।
- ধাপ ২ — Repository বের করা। সব SQL একটি নতুন ফাইলে গেল। আচরণ একই আছে। Test আবার চালান।
- ধাপ ৩ — Service বের করা। Stock-এর নিয়ম গেল service-এ। সে আর
resচেনে না, তাই ভুল হলে একটি অর্থবহ error ছোড়ে। - ধাপ ৪ — Controller পাতলা। এখন সে তিনটি কাজ করে: ইনপুট পড়া, service ডাকা, error-কে status-এ বদলানো। অচেনা error সে গিলে ফেলে না, ওপরে ছেড়ে দেয়।
- ধাপ ৫ — Test. এবার নিয়মটি HTTP ছাড়াই পরীক্ষা করা যায়। ঈদের ছাড়ের নতুন নিয়ম এলে শুধু service ছুঁতে হবে।
এই নকশা আমরা আসল Express 5.3.0-তে বানিয়ে চালিয়েছি। Service আর test উপরের ধাপের মতো, শুধু বাস্তবের মতো একটু পূর্ণ। সত্যিকারের run (Node 24.15.0, নিজের কম্পিউটারে):
~/bazarghar$ node --test
✔ 2 ta jog korle mot dam 84000 paisa (0.7626ms)
✔ stock-er beshi chaile OUT_OF_STOCK, kichui save hoy na (0.6287ms)
✔ qty 0 hole BAD_QTY (0.1292ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
…
~/bazarghar$ node src/app.js
201 {"items":[{"productId":"chal-5kg","qty":2,"unitPaisa":42000}],"totalPaisa":84000}
409 {"error":"OUT_OF_STOCK"}
404 {"error":"NOT_FOUND"}
400 {"error":"BAD_QTY"}
Stock 3, প্রথম অনুরোধে 2 নেওয়ার পরে আবার 2 চাইলে মোট 4 হয়, তাই 409. একই service দুই জায়গা থেকে চলল: test থেকে আর HTTP থেকে। পূর্ণ গঠন, dependency injection আর transaction-এর হিসাব পর্ব ২৯ আর পর্ব ৪৩-এ আসবে। এখানে একটি সততা জরুরি: এই কোডে দুজন ক্রেতা একসাথে কিনলে race condition (কোন কাজ আগে শেষ হলো তার ওপর ফল নির্ভর করা) হতে পারে। Layer ভাগ করলে নকশা পরিষ্কার হয়, কিন্তু সব bug চলে যায় না।
৫ · Folder — layer অনুযায়ী, নাকি feature অনুযায়ী
স্তর ভাগ হলো, এবার ফাইলগুলো কোথায় রাখবেন? দুটি পরিচিত পথ আছে। ডাকঘরের বাছাইয়ের কথা ভাবুন: চিঠি, পার্সেল আর মানি অর্ডার আলাদা তাকে রাখা যায়, আবার জেলা অনুযায়ীও রাখা যায়।
Layer অনুযায়ী
src/
controllers/
cart.js order.js
services/
cart.js order.js
repositories/
cart.js order.js
ছোট প্রজেক্টে সহজ, নতুনদের কাছে পরিচিত। কিন্তু একটি feature বদলাতে তিনটি folder ঘুরতে হয়।
Feature অনুযায়ী
src/features/
cart/
cart.routes.js
cart.controller.js
cart.service.js
cart.repository.js
cart.service.test.js
order/ …
একসাথে বদলায় এমন কোড একসাথে থাকে। পরে কোনো অংশ আলাদা করা সহজ। ডান পাশের গঠনটিই আমাদের চালানো প্রজেক্টের আসল গঠন।
সাধারণ নিয়ম: প্রায় পাঁচটির বেশি feature হলে বা একাধিক মানুষ কাজ করলে feature folder বেশি সুবিধা দেয়। তবে এটি অভ্যাসের কথা, কোনো মাপা সীমা নয়। Feature-এর ভেতরেও স্তর থাকে; folder শুধু ঠিক করে আগে কী দিয়ে ভাগ হবে। Frontend-এও একই কথা: components/, hooks/ আলাদা না রেখে features/cart/-এ সব রাখা যায়। পর্ব ০৫-এ project-এর পুরো গঠন দেখবেন।
৬ · তিনটি anti-pattern — fat controller, god component, circular dependency
Anti-pattern হলো এমন নকশা, যা শুরুতে সহজ লাগে, পরে দাম চায়। এই তিনটি প্রায় প্রতিটি প্রজেক্টে দেখা যায়।
Fat controller
Fat controller-এ HTTP-র সাথে নিয়ম আর SQL মিশে থাকে। লক্ষণ: এক ফাংশনে শত শত লাইন, একই নিয়ম দুটি route-এ কপি। ওষুধ: আগের ধাপগুলো।
God component
God component হলো React-এর fat controller: একটি component-এ fetch, form, হিসাব আর পুরো পাতার markup. লক্ষণ: এক ফাইলে অনেক useState আর useEffect. ওষুধ: ছোট component আর custom hook (পর্ব ০৯, পর্ব ১৪)।
Circular dependency
Circular dependency মানে A, B-কে import করে, আর B আবার A-কে। লক্ষণ: load order বদলালে অদ্ভুত error. ওষুধ: নিচে দেখুন।
Circular import শুধু দেখতে খারাপ নয়, এটি সত্যিই প্রোগ্রাম থামাতে পারে। Module-এর নিয়মের জন্য ES modules পর্ব দেখুন। আমরা বাজারঘরে ইচ্ছা করে এমন একটি চক্র বানিয়ে চালালাম: order.js ডাকে payment.js-কে, আর payment.js load হওয়ার সময়েই order.js-এর একটি constant পড়ে। সত্যিকারের run (Node 24.15.0, madge 8.0.0):
~/bazarghar$ node src/cycle/main.js
~/bazarghar/src/cycle/payment.js:2
export const GATEWAY_FEE = DELIVERY_PAISA / 100;
^
ReferenceError: Cannot access 'DELIVERY_PAISA' before initialization
at ~/bazarghar/src/cycle/payment.js:2:28
…
~/bazarghar$ npx madge --circular src/cycle/main.js
Processed 3 files (370ms)
✖ Found 1 circular dependency!
1) order.js > payment.js
madge হলো import-এর গ্রাফ বানানোর একটি npm টুল। সমাধানও সরল: যে জিনিস দুজনেরই দরকার, সেটি তৃতীয় একটি ফাইলে নিন। DELIVERY_PAISA নতুন pricing.js-তে যাওয়ার পরে চক্র ভাঙল, প্রোগ্রাম 90000 ছাপল, আর একই টুল জানাল No circular dependency found!. এই টুল ভেতরে কী করে? গ্রাফে depth-first search চালায়। ছোট করে নিজেই দেখুন:
const modules = {
routes: ["cart.controller"],
"cart.controller": ["cart.service"],
"cart.service": ["cart.repo", "order.service"],
"order.service": ["payment.service"],
"payment.service": ["cart.service", "db"],
"cart.repo": ["db"],
db: [],
};
function findCycle(graph) {
const mark = {}; // "open" = এখনো পথে আছে, "done" = শেষ
const path = [];
function visit(name) {
if (mark[name] === "done") return null;
if (mark[name] === "open") return [...path.slice(path.indexOf(name)), name];
mark[name] = "open";
path.push(name);
for (const dep of graph[name]) {
const found = visit(dep);
if (found) return found;
}
path.pop();
mark[name] = "done";
return null;
}
for (const name of Object.keys(graph)) {
const found = visit(name);
if (found) return found;
}
return null;
}
console.log("cycle:", findCycle(modules).join(" -> "));
modules["payment.service"] = ["db"]; // payment আর cart.service ডাকে না
console.log("after fix:", findCycle(modules));আউটপুট:
cycle: cart.service -> order.service -> payment.service -> cart.service
after fix: null
পথে থাকা (“open”) কোনো module-এ আবার পৌঁছানো মানেই চক্র। Service-এর মধ্যে এমন চক্র প্রায়ই নকশার সংকেত: দায়িত্বের ভাগ ঠিক হয়নি। সমাধান হতে পারে একটি তৃতীয় module, অথবা event-এর মাধ্যমে খবর দেওয়া। কঠিন লাগলে আপাতত মনে রাখুন: নির্ভরতার তীর একদিকে রাখুন।
৭ · Monolith, modular monolith, microservices — সৎ হিসাব
এতক্ষণ কথা ছিল এক প্রোগ্রামের ভেতরের গঠন নিয়ে। এবার প্রশ্ন: পুরো সিস্টেম কয়টি আলাদা চলমান প্রোগ্রাম হবে? এই তিনটি শব্দ আসলে deploy-এর হিসাব, কোডের মান নয়। এলোমেলো monolith যেমন হয়, চমৎকার monolith-ও হয়।
একই দোকান, তিন রকম গঠন
Monolith, modular monolith and microservices compared
| বিষয় | Monolith | Modular monolith | Microservices |
|---|---|---|---|
| Deploy | একটি | একটি | অনেক, আলাদা আলাদা |
| দুই অংশের ডাক | function call | function call | network |
| Transaction | সহজ, এক database | সহজ, এক database | কঠিন, saga-র (ধাপে ধাপে কাজ, ব্যর্থ হলে উল্টো ধাপ) মতো নকশা লাগে |
| আলাদা scale | পুরোটা একসাথে | পুরোটা একসাথে | প্রতিটি আলাদা |
| Debug | সহজ stack trace | সহজ stack trace | একটি request-এর পথ অনেক সেবার log জুড়ে খুঁজতে হয় |
| দলের দরকার | ১ জন থেকে | ১ জন থেকে | কয়েকটি স্বাধীন দল, DevOps সামর্থ্য |
| বড় ঝুঁকি | জট বেঁধে যাওয়া | সীমা ভেঙে ফেলা | Distributed monolith |
একটি হিসাব — network-এর দাম
ধরুন একটি অর্ডারে পাঁচটি সেবা পরপর লাগে, আর প্রতিটি আলাদাভাবে ৯৯.৯% সময় ঠিক থাকে। ধরে নিচ্ছি একটির ব্যর্থতা আরেকটির ওপর নির্ভর করে না, আর পাঁচটিই না চললে অর্ডার হবে না।
৩০ দিনে ৭২০ ঘণ্টা। একটি সেবা হলে অচল সময় 720 × 0.001 = ০.৭২ ঘণ্টা। পাঁচটি চেইনে হলে 720 × 0.00499 ≈ ৩.৫৯ ঘণ্টা, অর্থাৎ প্রায় পাঁচ গুণ। গতির হিসাবও আছে। একই কম্পিউটারে আমরা একটি ছোট মাপ নিয়েছি: একটি function call গড়ে প্রায় ০.০০০০৫ মিলিসেকেন্ড, আর একই কাজ HTTP দিয়ে (একই কম্পিউটারের ভেতরে, এনক্রিপশন বা database ছাড়া) প্রায় ০.১৪ মিলিসেকেন্ড। অর্থাৎ কয়েক হাজার গুণ। এটি এক কম্পিউটারের এক run, সাধারণ ফল নয়, আর JavaScript ইঞ্জিনের চলার সময়ের optimization ছোট function call-কে আরও ছোট দেখাতে পারে। আসল network-এ দেরি সাধারণত আরও বেশি।
ছোট বাংলাদেশি দলের জন্য সৎ কথা। ধরুন আপনারা ২–৫ জন, DevOps-এর আলাদা মানুষ নেই, একটি VPS (ভাড়া করা ভার্চুয়াল server) বা PaaS-এ (কোড দিলেই চালিয়ে দেয় এমন ভাড়া করা প্ল্যাটফর্ম) চলছে। Microservices মানে তখন পাঁচটি deploy, পাঁচটি log, network-এর ব্যর্থতা সামলানো, আর ডেটা ভাগের মাথাব্যথা। যে সময়ে ক্রেতার জন্য feature বানানোর কথা, সেই সময় যাবে pipeline-এ। Scale-এর ভয় থেকে শুরুতেই সেবা ভাগ করার দরকার সাধারণত পড়ে না; একটি ভালো monolith অনেক দূর যায়। আবার উল্টো ভুলও আছে: যদি সত্যিই আলাদা দল, আলাদা release-এর চাপ, বা একটি অংশের সম্পূর্ণ আলাদা চাহিদা থাকে, তখন আলাদা করা যুক্তিসংগত।
Martin Fowler ২০১৫ সালের Monolith First লেখায় বলেন, তিনি যেসব সফল microservice গল্প শুনেছেন, প্রায় সবই একটি monolith থেকে শুরু। তিনি নিজেই জানান, প্রমাণ এখনো কম আর মতভেদ আছে। সেবার সীমা, Conway-এর সূত্র, strangler fig আর service mesh-এর পূর্ণ আলোচনা আছে Microservices পর্বে। এই পর্বের কাজ শুধু সিদ্ধান্তের ছবিটি দেওয়া।
৮ · BFF আর API gateway — দুটি পরিচিত দরজা
Client আর backend-এর মাঝখানে প্রায়ই একটি স্তর বসে। নাম দুটি গুলিয়ে যায়, কারণ দুটিই দরজার মতো। হোটেলের সাথে মিলিয়ে দেখুন।
API gateway — মূল ফটক
API gateway হলো সব অনুরোধের এক প্রবেশপথ। Routing, rate limit (কতবার ডাকা যাবে তার সীমা), HTTPS, প্রাথমিক auth-এর মতো সাধারণ কাজ করে। হোটেলের নিরাপত্তা ফটকের মতো: সবাই এখান দিয়ে ঢোকেন, কিন্তু ফটক রান্না করে না। Business logic এখানে রাখবেন না।
BFF — নিজের রিসেপশন
BFF (Backend for Frontend) হলো একটি নির্দিষ্ট UI-এর জন্য বানানো ছোট backend. মোবাইল অ্যাপের একটি পাতায় তিনটি API-র data দরকার হলে BFF সেগুলো জুড়ে ঠিক যা দরকার তা দেয়। সাধারণত সেই UI-এর দলই এটি চালায়।
Sam Newman তাঁর লেখায় BFF নামটির কৃতিত্ব SoundCloud-এর প্রাক্তন প্রকৌশলী Phil Calçado-কে দেন। Next.js-এর দাপ্তরিক docs-এও Backend for Frontend নামে একটি গাইড আছে, কারণ তার route handler স্বাভাবিকভাবেই এই ভূমিকা নিতে পারে। বাজারঘরে শুরুতে gateway বা আলাদা BFF কোনোটিই লাগবে না, যদি একটি অ্যাপ আর একটি client থাকে। একটি reverse proxy (সামনে বসে অনুরোধ নিয়ে অ্যাপে পাঠানো server) থাকলেই চলে। গভীর আলোচনা, ঝুঁকি আর “smart gateway” ফাঁদের জন্য দেখুন API gateway ও proxy.
৯ · আসল framework-এ কে কোথায়, আর বাজারঘরের সিদ্ধান্ত
Framework নাম দেয় নিজের মতো করে, কিন্তু দায়িত্বের ভাগ প্রায় একই থাকে। এই টেবিলটি হাতের কাছে রাখলে নতুন framework শিখতে সুবিধা হবে।
| দায়িত্ব | Express | Django | Laravel | Next.js |
|---|---|---|---|---|
| ঠিকানা মেলানো | app.get(), Router | urls.py | routes/web.php | app/-এর folder |
| অনুরোধ সামলানো | handler ফাংশন | View (ফাংশন বা class) | Controller | Server Component, Server Action, route handler |
| Data | আপনার পছন্দ (Prisma ইত্যাদি) | Model (ORM) | Eloquent Model | আপনার পছন্দ, Data Access Layer |
| চেহারা | আপনার পছন্দ (অথবা শুধু JSON) | Template | Blade view | React component |
| Folder-এর মত | প্রায় কোনো মত নেই | app-ভিত্তিক গঠন | গঠন দেয়, তবে কঠোর নয় | routing-এ কঠোর, বাকিটা খোলা |
- Express: সে নিজেকে “unopinionated” বলে। গঠন আপনার দায়িত্ব। স্বাধীনতার দাম হলো শৃঙ্খলা নিজেকে রাখতে হয়। তাই এই পর্বের স্তরগুলো সবচেয়ে বেশি লাগে এখানে।
- Django: MVT নামটিই বিভ্রান্তির উৎস। Django-র দাপ্তরিক FAQ বলছে, তাদের “view” ঠিক করে কোন data দেখানো হবে, আর template ঠিক করে কীভাবে; controller-এর কাজটি মোটামুটি framework নিজেই করে। পর্ব ৬৮-তে হাতে-কলমে।
- Laravel: web MVC-র সোজা রূপ, পর্ব ৭০-এ দেখবেন। তার docs বলে, class কোথায় রাখবেন সেই বিষয়ে প্রায় কোনো বাধা নেই, তাই অনেক দল নিজেরাই service layer যোগ করে।
- Next.js: Server Component সরাসরি database ছুঁতে পারে, এতে স্তরগুলো হারিয়ে গেছে বলে মনে হয়। আসলে তা নয়। বর্তমান docs নতুন প্রজেক্টে একটি আলাদা Data Access Layer রাখার পরামর্শ দেয়, যা শুধু server-এ চলে, অনুমতি যাচাই করে আর ন্যূনতম data ফেরত দেয়। পর্ব ৫২ আর পর্ব ৫৪ দেখবেন।
বাজারঘরের সিদ্ধান্ত
ধরা যাক দলে তিনজন, নতুন পণ্য, ক্রেতা কত আসবেন এখনো অজানা। আমাদের সিদ্ধান্ত: feature folder-সহ modular monolith, প্রতিটি feature-এর ভেতরে তিন স্তর, এক database. নিয়ম চারটি:
- প্রতিটি feature (
catalog,cart,order,payment,auth) বাইরে শুধু একটিindex.jsদেখায়। - এক feature আরেক feature-এর repository বা table সরাসরি ছোঁবে না; তার service ডাকবে।
- Circular import থাকলে CI (কোড জমা দিলে নিজে চলা যাচাই) আটকে দেবে (পর্ব ৮১)।
- বড় সিদ্ধান্ত ছোট করে লিখে রাখা হবে, কেন নেওয়া হলো সহ।
| যে সংকেত দেখলে | পরের পদক্ষেপ |
|---|---|
| Database query ধীর | আগে index, query আর cache দেখুন (পর্ব ৪২, পর্ব ৪৪) |
| ছবি বা email-এ request আটকে যায় | Background job (পর্ব ৬২), এখনো সেবা ভাগ নয় |
| একটি অংশের চাপ বাকিদের চেয়ে অনেক আলাদা | শুধু সেই module আলাদা করার কথা ভাবুন |
| দুটি দল প্রতিদিন একই deploy-তে আটকে যায় | সীমা অনুযায়ী প্রথম সেবা বের করুন |
Modular monolith-এর সুবিধা এখানেই: সীমা আগে থেকে পরিষ্কার থাকলে পরে বের করা সস্তা হয়। ১০০ থেকে ১০ লাখ ব্যবহারকারীর পথ দেখবেন পর্ব ৮৭-তে, আর stack বাছাইয়ের ছক পর্ব ৮৫-তে।
আরও গভীরে: Service Layer, Repository আর Active Record নামগুলো Martin Fowler-এর ২০০২ সালের Patterns of Enterprise Application Architecture বইয়ে সুসংগঠিত রূপ পায়; Eric Evans-এর ২০০৩ সালের Domain-Driven Design থেকে আসে bounded context-এর চিন্তা। Hexagonal আর Clean Architecture নিয়মটি আরও কঠোর করে: নির্ভরতা শুধু ভেতরের দিকে, আর database-ও একটি বাইরের যন্ত্রাংশ। দেখুন Clean ও hexagonal পর্ব।
পরীক্ষা ও ইন্টারভিউতে:
- MVC-তে controller-এর কাজ কী? ইনপুট গ্রহণ করে model আর view-এর মধ্যে সমন্বয় করা।
- Django-র view কি MVC-র view? না, ভূমিকায় সেটি controller-এর কাছাকাছি; দেখানোর কাজ template-এর।
- MVVM আর MVC-র মূল পার্থক্য? MVVM-এ view model আর view binding দিয়ে স্বয়ংক্রিয়ভাবে মিলে থাকে।
- Modular monolith কী? একটি deploy, কিন্তু ভেতরে স্পষ্ট সীমার module.
- API gateway আর BFF-এর পার্থক্য? Gateway সবার জন্য সাধারণ দরজা, BFF একটি নির্দিষ্ট UI-এর জন্য বানানো backend.
১০ · AI-এর যুগে — architecture-এর পরামর্শ যাচাই
AI খুব দ্রুত folder বানিয়ে দিতে পারে। কিন্তু সে আপনার দলের আকার, বাজেট আর ক্রেতার সংখ্যা জানে না, যদি না আপনি বলেন। তাই AI প্রায়ই প্রথম উত্তরে “Kubernetes-এ পাঁচটি microservice” বলে ফেলে, কারণ লেখালেখিতে সেটিই বেশি চোখে পড়ে।
| AI-এর সাধারণ ভুল | কীভাবে ধরবেন |
|---|---|
| ছোট প্রজেক্টে অকারণে সেবা, queue, অনেক স্তর | প্রতিটি অংশের জন্য জিজ্ঞেস করুন: এটি না থাকলে আজ কী ভাঙবে? |
| Controller-এই নিয়ম আর SQL একসাথে | Diff পড়ুন: service-এ req বা controller-এ query আছে কি না |
পুরোনো Next.js নকশা (যেমন pages/ আর getServerSideProps) | আপনার version-এর দাপ্তরিক docs মেলান |
| অস্তিত্বহীন package বা option | npm view <নাম> চালান, docs দেখুন |
| Server Action বা route-এ অনুমতি যাচাই বাদ | লগইন ছাড়া অন্যের id দিয়ে ডেকে দেখুন; তত্ত্ব আছে broken access control পর্বে |
catch-এ error গিলে ফেলা | ইচ্ছা করে ব্যর্থ করিয়ে দেখুন error কোথায় যায় |
| অকারণে নতুন dependency | প্রতিটি নতুন package-এর কারণ জিজ্ঞেস করুন |
AI-কে সিদ্ধান্ত নিতে দেবেন না; তাকে দিয়ে বিকল্প আর তার দাম বের করান। নিচের কাঠামোয় প্রেক্ষাপট দিন, আর শেষ অনুরোধটি খেয়াল করুন: সে যেন নিজের পরামর্শের বিপক্ষেও বলে।
AI-এর দেওয়া নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১Rails বা Laravel যাকে MVC বলে, সেটি কি আসল MVC? একটি নাম এভাবে বদলে গেলে সমস্যা কী?
কড়া অর্থে না। Reenskaug-এর MVC-র কেন্দ্রে ছিল ব্যবহারকারীর মনের ছবি আর কম্পিউটারের data-র মধ্যে সেতু বানানো, আর চলমান একটি model-এর সাথে যুক্ত view. Model বদলালে view সাথে সাথে খবর পেত। ওয়েবে HTTP অনুরোধ আসে, উত্তর যায়, সংযোগ শেষ। Server-এর model browser-এর পাতাকে খবর দিতে পারে না। তাই ১৯৯০-এর দশকের শেষে Java-র web জগতে যে নকশা আসে (অনেকে একে “Model 2” বলতেন), তা MVC-র চিন্তা থেকে অনুপ্রাণিত, কিন্তু আসলে একটি request-response পাইপলাইন।
Django এই বিভ্রান্তি নিয়ে খোলাখুলি কথা বলে। তাদের FAQ বলছে, “view” বলতে তারা বোঝে কোন data দেখানো হবে, আর চেহারার কাজ template-এর। Controller-এর ভূমিকায় সম্ভবত framework নিজেই। তাই তারা MTV নামটিও ব্যবহার করে।
ক্ষতি কোথায়? প্রথমত, আলোচনায় দুজন একই শব্দে আলাদা জিনিস বোঝান। Android-এ কেউ “controller” বললে এক জিনিস, Laravel-এ আরেক। দ্বিতীয়ত, ইন্টারভিউতে মুখস্থ সংজ্ঞা বিপদে ফেলে। কেউ যদি বলেন “model view-কে খবর দেয়”, সেটি desktop MVC-তে ঠিক, Rails-এ নয়। তৃতীয়ত, শেখার সময় মানুষ ধরে নেন তিনটি অক্ষর মানলেই নকশা ভালো হলো, অথচ fat controller MVC-র নামেই বেঁচে থাকে।
তাহলে কি নামটি বাদ? দরকার নেই। ভাষা এভাবেই বদলায়। কাজের অভ্যাস হলো শব্দের বদলে প্রশ্ন করা: এই কোড কে কে চেনে, data কোন দিকে যায়, কতক্ষণ বাঁচে?
মূল উপলব্ধি: Pattern-এর নাম মুখস্থ না করে দায়িত্বের ভাগ আর নির্ভরতার দিক দেখুন। সেগুলো যেকোনো framework-এ একই থাকে, নামগুলো থাকে না।
প্র ০২নতুন startup কি শুরুতেই microservices দিয়ে শুরু করবে, যাতে পরে ঝামেলা না হয়?
“পরে ঝামেলা” এড়াতে গিয়ে প্রায়ই “এখনই ঝামেলা” কেনা হয়। Martin Fowler ২০১৫ সালের Monolith First লেখায় যুক্তি তিনটি দেন। এক, microservices-এর একটি বাড়তি খরচ আছে, যাকে তিনি “microservice premium” বলেন; জটিল সিস্টেমেই সেটি উসুল হয়। দুই, নতুন পণ্যে সবচেয়ে বড় ঝুঁকি হলো মানুষ পণ্যটি চায় কি না, তাই দ্রুত শেখা জরুরি। তিন, সেবার সীমা শুরুতে ঠিক করা কঠিন। ভুল সীমা monolith-এ বদলানো একটি refactor, আর সেবায় বদলানো কয়েকটি দলের প্রকল্প।
একই লেখায় তিনি স্বীকার করেন যে সবাই একমত নন, আর প্রমাণও সীমিত। বিপক্ষের যুক্তি হলো, শুরু থেকে সেবা থাকলে সীমার শৃঙ্খলা তৈরি হয়, আর একটি জট বাঁধা monolith ভাঙা সত্যিই কষ্টের।
২০২৩ সালে Prime Video-র একটি প্রকৌশল blog-এ জানানো হয়, একটি নজরদারি সেবা বিচ্ছিন্ন সেবার নকশা থেকে এক প্রক্রিয়ায় এনে খরচ অনেক কমেছে। এটি নিয়ে ব্যাপক তর্ক হয়। সমালোচকেরা বলেন, ঘটনাটি একটি সেবার ভেতরের নকশা বদল, পুরো কোম্পানির সিদ্ধান্ত নয়। অর্থাৎ একটি শিরোনাম দেখে কোনো পক্ষেরই জয় ঘোষণা করা যায় না।
ছোট দলের জন্য Conway-এর ১৯৬৮ সালের পর্যবেক্ষণও কাজের: সংগঠনের যোগাযোগের কাঠামো নকশায় ছাপ ফেলে। তিনজনের দল এক টেবিলে বসে কথা বলে, তাদের মাঝখানে network-এর সীমা বসানোর কারণ কম। Modular monolith মাঝের পথ, কারণ সীমার শৃঙ্খলা পাওয়া যায়, network-এর দাম ছাড়াই।
মূল উপলব্ধি: Microservices মূলত সংগঠনের সমস্যার সমাধান: অনেক দলকে স্বাধীনভাবে deploy করতে দেওয়া। যে সমস্যা আপনার নেই, তার সমাধানের দাম দেবেন না।
প্র ০৩একসময় বলা হতো React হলো MVC-র “V”. এখন কি কথাটি ঠিক? React কি MVVM?
শুরুর দিকের React ওয়েবসাইটে সত্যিই লেখা ছিল, অনেকে React-কে MVC-র V হিসেবে ব্যবহার করেন। ২০১৩ সালে এটি বুদ্ধিমানের কথা ছিল, কারণ তখন Backbone বা Angular-এর সাথে পাশাপাশি বসানো সহজ হতো। কিন্তু component-এর ভেতরে state, event আর markup একসাথে থাকে। JSX আসলে “প্রযুক্তি অনুযায়ী ভাগ” না করে “দায়িত্ব অনুযায়ী ভাগ” করার একটি বাজি: HTML, CSS আর JS আলাদা ফাইলে না রেখে একটি “কার্টের লাইন” component-এ রাখা।
আজকের React একা View নয়। Hook দিয়ে logic আলাদা করা যায়, server state-এর library cache সামলায়, আর Server Component server-এ চলে। এখন Next.js-এর একটি page ফাইল একই সাথে route, data আনা আর markup-এর জায়গা হতে পারে। সেই হিসাবে MVC-র তিন অক্ষর এখানে আর পরিষ্কারভাবে বসে না।
MVVM কি? কিছু মিল আছে: state আর derived data দেখতে view model-এর মতো, UI সেখান থেকে তৈরি। কিন্তু দুটি বড় পার্থক্য আছে। React-এ binding দুইমুখী নয়। Input বদলালে state নিজে বদলায় না; আপনাকে onChange থেকে স্পষ্টভাবে বদলাতে হয়। react.dev এটিকে one-way data flow বলে, আর বলে এতে দুইমুখী binding-এর চেয়ে একটু বেশি লিখতে হয়। আবার React নির্দিষ্ট property পাহারা দিয়ে বদলায় না; সে component-কে নতুন করে চালিয়ে ফল মিলিয়ে দেখে।
Vue-র reactivity আর Angular-এর template দুইমুখী binding-এর সুবিধা দেয়, আর বহু দল এতে দ্রুত form বানান। এখানে কোনো পক্ষ “ভুল” নয়। দুইমুখী binding কম কোড দেয়, একমুখী flow সহজে অনুসরণ করা যায়।
মূল উপলব্ধি: React-কে কোনো পুরোনো অক্ষরে না বসিয়ে তার নিজের ভাষায় ভাবুন: state হলো উৎস, UI হলো তার ফল, ঘটনা হলো বদলের একমাত্র পথ।
প্র ০৪Business logic service-এ থাকবে, নাকি model-এ? “Fat model, skinny controller” কি ভুল?
এখানে সম্প্রদায় সত্যিই দুই ভাগে বিভক্ত, আর দুই পক্ষেরই ভালো যুক্তি আছে।
Model-এর পক্ষে: Rails আর Laravel-এর জগতে দীর্ঘদিনের পরামর্শ ছিল “fat model, skinny controller”. Active Record নকশায় একটি object-ই একটি row আর তার আচরণ। order.cancel() লিখলে নিয়ম data-র পাশেই থাকে। Object-oriented নকশার মূল কথাও এটাই। Martin Fowler ২০০৩ সালে “Anemic Domain Model” লেখায় সতর্ক করেন: যে object-এ শুধু data থাকে আর সব আচরণ service-এ, সেটি object-oriented নকশার সুবিধা হারায়।
Service-এর পক্ষে: বাস্তবের অনেক কাজ একটি model-এ ধরে না। “অর্ডার দিন” কাজটি cart, stock, payment আর email ছোঁয়। এটি কোন model-এ রাখবেন? রাখলে সেই model ধীরে ধীরে god object হয়ে যায়। Prisma-র মতো ORM-এ row সাধারণ object, method থাকে না, তাই service-ই স্বাভাবিক জায়গা। TypeScript আর JavaScript-এর জগতে সাধারণ ফাংশনে লেখার অভ্যাসও এদিকেই টানে।
একটি বাস্তব মধ্যপথ: একটি জিনিসের নিজের সরল নিয়ম তার পাশে রাখুন, যেমন একটি বিশুদ্ধ ফাংশন lineTotal(item). একাধিক জিনিস জড়ালে বা transaction লাগলে service-এ রাখুন। Domain-Driven Design এই ভাগকে আরও নির্দিষ্ট করে: entity, value object আর application service.
যে কথাটি নিয়ে প্রায় কোনো মতভেদ নেই: নিয়ম controller-এ, route handler-এ বা React component-এ থাকবে না। কারণ তখন একই নিয়ম HTTP বা UI-এর সাথে বাঁধা পড়ে, আর পরের client-এ কপি হয়।
মূল উপলব্ধি: “Service না model” মূল প্রশ্ন নয়। মূল প্রশ্ন হলো, নিয়মটি কি HTTP, framework আর UI ছাড়া একা ডাকা ও test করা যায়? উত্তর হ্যাঁ হলে দুই নকশাই ঠিক পথে আছে।
প্র ০৫Prisma তো নিজেই একটি data স্তর। তাহলে তার ওপরে আবার repository লেখার দরকার কী?
ভালো প্রশ্ন, আর উত্তর “নির্ভর করে”, তবে কীসের ওপর নির্ভর করে সেটি বলা যায়।
Repository না লেখার পক্ষে: Prisma বা Drizzle-এর client আগেই type-safe, পড়তে সহজ। প্রতিটি query-র জন্য findById মোড়ক লিখলে একই কথা দুবার লেখা হয়। “পরে database বদলাব” যুক্তিও প্রায়ই দুর্বল, কারণ বাস্তবে database খুব কম প্রজেক্টই বদলায়, আর মোড়ক থাকলেও query-র আচরণ বদলে যায়। ছোট প্রজেক্টে service থেকে সরাসরি ORM ডাকা পুরোপুরি যুক্তিসংগত, আর test-এ একটি আসল test database ব্যবহার করা যায়।
Repository লেখার পক্ষে: এক, ধরুন “মুছে ফেলা পণ্য বাদ দিয়ে” শর্তটি দশ জায়গায় লাগে; একটি জায়গায় থাকলে ভুল কমে। দুই, জটিল query একটি নাম পায়, যেমন findLowStockProducts(), ফলে service-এ নিয়মই পড়া যায়। তিন, service-এর দ্রুত unit test-এ একটি নকল বসানো সহজ হয়। চার, ORM বদলানো না হলেও ORM-এর major version-এর বদল এক জায়গায় সামলানো যায়। পাঁচ, Next.js-এর Data Access Layer-এর মতো নকশায় অনুমতি আর ন্যূনতম data ফেরতের একটি নিয়ন্ত্রিত দরজা পাওয়া যায়।
একটি ফাঁদ: ORM-এর সব method-এর জন্য generic মোড়ক (getAll, update, delete) বানাবেন না। তাতে ORM-এর ক্ষমতা হারায়, অথচ লাভ হয় না। Repository-র method-এর নাম হবে ব্যবসার ভাষায়, আর সংখ্যায় যতটুকু লাগে ততটুকু।
এই কোর্সে শেখার জন্য আমরা repository আলাদা রাখছি, কারণ এতে স্তরের ধারণা পরিষ্কার হয়। আপনার প্রজেক্টে ছোট কিছু হলে একটি স্তর কম রাখতে পারেন। পর্ব ৩৮-এ ORM নিয়ে এই আলোচনা আরও এগোবে।
মূল উপলব্ধি: প্রতিটি স্তরকে নিজের অস্তিত্বের কারণ দেখাতে হবে। কারণ না থাকলে সেটি শুধু একটি বাড়তি ফাইল।
অনুশীলন
হিসাব, ভবিষ্যদ্বাণী আর নকশা — তিন রকম কাজই আছে। আগে নিজে চেষ্টা করুন।
-
Numerical — চেইনের হিসাব: একটি অর্ডারে ৮টি সেবা পরপর লাগে। প্রতিটির availability ৯৯.৯৫%, ব্যর্থতা একে অপরের ওপর নির্ভরশীল নয়, আর প্রতিটি hop-এ (এক সেবা থেকে পরের সেবায় যাওয়া) গড়ে ২ মিলিসেকেন্ড বাড়তি লাগে (৮টি সেবায় ৭টি hop থাকে)। পুরো অর্ডারের availability কত? ৩০ দিনে (৭২০ ঘণ্টা) কত ঘণ্টা অচল থাকবে? বাড়তি দেরি কত?
Availability = 0.99958 ≈ 0.99600, অর্থাৎ প্রায় ৯৯.৬০%.
অচল সময় = 720 × (1 − 0.99600) ≈ ২.৮৮ ঘণ্টা। একটি সেবার ক্ষেত্রে এটি হতো 720 × 0.0005 = ০.৩৬ ঘণ্টা, প্রায় আট ভাগের এক ভাগ।
বাড়তি দেরি = 7 × 2 = ১৪ মিলিসেকেন্ড। ধরে নেওয়া সংখ্যা দিয়ে এটি একটি মোটা হিসাব। Retry, parallel ডাক আর degrade করা (একটি সেবা ব্যর্থ হলেও অর্ডার চালু রাখা) বাস্তব সংখ্যা বদলে দেয়।
-
আউটপুট অনুমান করুন:
let n = 0; const render = () => console.log("render", ++n);আছে। Store নিয়ম মানে: reducer একই state ফেরত দিলে render হয় না। State{ qty: 2 }থেকে শুরু, আর reducer শুধুsetaction-এ{ qty: a.value }ফেরত দেয়, তবেa.value === state.qtyহলে পুরোনো state-ই ফেরত দেয়। ক্রমে dispatch হলো:set 2,set 3,set 3,reset,set 2. শুরুতে আলাদা করে render ডাকা হয়নি। কী ছাপা হবে, আর শেষ state কী?set 2: মান একই, কোনো render নেই।set 3: নতুন state, ছাপা হবেrender 1. আবারset 3: একই, কিছু নেই।reset: reducer এটি চেনে না, পুরোনো state ফেরত, কিছু নেই।set 2: নতুন state, ছাপা হবেrender 2.render 1 render 2শেষ state
{ qty: 2 }. শুরুর মানের সমান হলেও এটি একটি নতুন object, আর মাঝে একবার বদল হয়েছে বলে দুটি render হয়েছে। -
নকশা সমালোচনা: একজন বললেন: “
orders.controller.js-এর ৪০০ লাইনে checkout-এর সব কাজ আছে: coupon হিসাব, stock কমানো, gateway-কে ডাকা, email পাঠানো। আরOrderPage.jsx-তেও coupon-এর একই হিসাব কপি করা আছে, দ্রুত দেখানোর জন্য।” তিন স্তরের নকশায় কোন অংশ কোথায় যাবে? কোন সমস্যাটি সবচেয়ে বিপজ্জনক?- Controller: ইনপুট পড়া, service ডাকা, status ঠিক করা।
- Service: coupon-এর নিয়ম, stock যাচাই, অর্ডারের ক্রম। Stock কমানো আর অর্ডার লেখা একই transaction-এ থাকবে (পর্ব ৪৩)।
- Repository: সব query.
- আলাদা adapter: gateway আর email-এর ডাক, যাতে test-এ নকল বসানো যায়। Email পরে background job-এ যেতে পারে (পর্ব ৬২)।
সবচেয়ে বিপজ্জনক হলো coupon-এর হিসাবের দুটি কপি। দুটির মান আলাদা হতে পারে, আর browser-এর হিসাব বিশ্বাস করলে ব্যবহারকারী দাম বদলে পাঠাতে পারেন। Server-এর হিসাবই একমাত্র সত্য; browser শুধু প্রদর্শনের জন্য দেখাতে পারে, অথবা server থেকে আনতে পারে। Validation-এর schema দুই দিকে ভাগ করার পথ দেখবেন পর্ব ২১-এ।
-
কমান্ড — নিজের প্রজেক্টে চক্র খুঁজুন: নিজের যেকোনো JavaScript প্রজেক্টের root থেকে
npx madge --circular src/index.jsচালান (entry ফাইলের নাম নিজের মতো দিন)। চক্র পেলে কোন দুটি ফাইল জড়িত, আর কোন জিনিস ভাগ করে নিলে চক্র ভাঙবে?চক্র না থাকলে দেখাবে
No circular dependency found!, আর থাকলে তীর দিয়ে পথ দেখাবে, যেমনorder.js > payment.js. Exit code তখন 1 হয় (আমাদের run-এ তাই হয়েছে), তাই CI-তে এটি একটি ধাপ হিসেবে রাখা যায়।সমাধান তিন রকম: (১) দুজনের দরকারি জিনিস তৃতীয় ফাইলে নেওয়া; (২) নির্ভরতার দিক উল্টে দেওয়া, যেমন ফাংশন parameter হিসেবে পাঠানো; (৩) event দিয়ে সংযোগ আলগা করা। TypeScript-এ শুধু type-এর import চক্র হিসেবে দেখা দিলে তা সাধারণত runtime-এ ক্ষতি করে না;
import typeব্যবহার করলে পরিষ্কার হয়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমWeb MVC-তে একটি GET request-এর পথ সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Browser ঠিকানায় HTTP request পাঠায়
- Router ঠিকানা মিলিয়ে controller বেছে নেয়
- Controller model-কে data চায়
- Model database থেকে data ফেরত দেয়
- View সেই data থেকে HTML বানায়
- HTML সহ response browser-এ ফেরে
Web MVC আর মূল desktop MVC-র একটি প্রধান পার্থক্য কোনটি?
- Web MVC-তে model থাকে না
- Web MVC-তে model বদলালে সে view-কে সরাসরি খবর দেয় না; প্রতিটি request-এ view নতুন করে তৈরি হয়
- Desktop MVC-তে router বাধ্যতামূলক ছিল
- Web MVC শুধু JavaScript-এ চলে
একটি Express অ্যাপে “স্টকের বেশি পণ্য কার্টে যোগ করা যাবে না” নিয়মটি কোথায় রাখা সবচেয়ে ভালো?
- শুধু React component-এ
- Route handler বা controller-এ
- Service স্তরে
- Repository-র SQL-এর ভেতরে
Django-র “MVT” নকশায় view-এর ভূমিকা সবচেয়ে কাছাকাছি কোনটির সাথে মেলে?
- Web MVC-র controller
- Web MVC-র view
- Web MVC-র model
- Database engine
React-এ input-এর মান বদলানোর আদর্শ পথ কোনটি?
- সরাসরি
input.valueবদলানো, state পরে দেখা যাবে - React দুইমুখী binding-এ নিজে থেকেই state বদলে দেয়
- Child component parent-এর state object সরাসরি বদলায়
- Event handler state বদলায়, আর নতুন state থেকে UI আবার তৈরি হয়
তিনজনের একটি দল নতুন অনলাইন দোকান বানাচ্ছে, DevOps-এর আলাদা মানুষ নেই। কোন গঠনটি সাধারণত সবচেয়ে যুক্তিসংগত শুরু?
- দশটি microservice, প্রতিটির নিজের database
- Feature-ভিত্তিক modular monolith
- এক ফাইলে সব কোড, কোনো ভাগ ছাড়া
- Distributed monolith
একটি অর্ডারে ৩টি সেবা পরপর লাগে, প্রতিটির availability ৯৯%, ব্যর্থতা স্বাধীন, আর তিনটিই লাগবে। পুরো পথের availability আনুমানিক কত?
- ৯৯.৯৯%
- ৯৯.০০%
- প্রায় ৯৭.০৩%
- প্রায় ৯০%
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- সব pattern-এর গোড়ায় separation of concerns: যা আলাদা কারণে বদলায়, তা আলাদা রাখুন।
- Web MVC এক request বাঁচে; React ভাবে UI = f(state), data একদিকে নামে।
- Controller HTTP, service নিয়ম, repository data. নিয়ম আলাদা হলে test সহজ।
- Fat controller, god component আর circular dependency চিনে ধীরে ধীরে ভাঙুন; নির্ভরতার তীর একদিকে রাখুন।
- ছোট দলে modular monolith দিয়ে শুরু করুন; সংকেত দেখে তবেই সেবা আলাদা করুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৫ · কাজের পরিবেশ — Node, npm, Vite, ESLint, Prettier আর project-এর গঠনপরবর্তী পর্বকোর্সের কাজের জন্য আপনার কম্পিউটার সাজানো।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের রোডম্যাপ।
- Microservicesগভীরে যানসেবার সীমা, distributed monolith আর strangler fig.
- MVC, MVP আর MVVMতত্ত্বClassic MVC, observer আর প্রতিটি স্তরের test.
- Software architectureপাশের ট্র্যাকQuality attribute, ADR আর architecture যাচাই।
- Dependency injectionপাশের ট্র্যাকবাইরে থেকে নির্ভরতা দেওয়ার তত্ত্ব।