Oct 28, 2026 · @rakib Ahasan
ভূমিকা: এই গাইড কেন, কার জন্য, কীভাবে পড়বেন
System design শেখার সবচেয়ে বড় সমস্যা হলো তথ্য ছড়ানো: একটা ব্লগে CAP theorem, আরেকটায় Microservices, তৃতীয়টায় Redis। কোনোটাই বলে না কখন কোনটা লাগে। এই গাইডে সব একসাথে, একটা ধারাবাহিক গল্পের মধ্য দিয়ে, .NET (C# ও ASP.NET Core) এর উদাহরণসহ।
এই গাইড কাদের জন্য
- যারা C# জানেন, কিন্তু "architecture" শব্দটা শুনলে ঘাবড়ে যান (Beginner)
- যারা ২-৩ বছর কাজ করছেন, কিন্তু কেন Clean Architecture বা CQRS লাগে বুঝতে পারেন না (Intermediate)
- যারা Senior/Lead হতে চান এবং system design interview বা real project এ trade-off নিয়ে সিদ্ধান্ত নিতে চান (Advanced)
কীভাবে পড়বেন
| আপনি কোথায় | কী করবেন |
|---|---|
| একদম নতুন | Part 1 থেকে ক্রমানুসারে পড়ুন, কোড হাতে লিখে চালান |
| কিছুটা অভিজ্ঞ | Part 2, 3 ও 4 মন দিয়ে পড়ুন, বাকিগুলো স্কিম করুন |
| Interview প্রস্তুতি | Part 1, 6, 7, 11, 12 আগে পড়ুন |
| Real project এ আটকে আছেন | সূচি দেখে সরাসরি সংশ্লিষ্ট Part এ যান |
গল্পের চরিত্র: ShopEasy
পুরো গাইডে আমরা একটা কাল্পনিক ই-কমার্স অ্যাপ ShopEasy ধরে এগোব। রাফি নামের একজন ডেভেলপার ঢাকায় একা এটা শুরু করে।
- দিন ১: ১০০ জন ইউজার, একটা ASP.NET Core অ্যাপ, একটা SQL Server। সব ঠিকঠাক।
- ৬ মাস পর: ১০,০০০ ইউজার। পেজ ধীর, কোড এলোমেলো, নতুন ডেভেলপার এসে কিছু বুঝছে না।
- ১ বছর পর: ঈদের সেলে ১ লক্ষ ইউজার একসাথে ঢুকল, সাইট বসে গেল। পেমেন্ট দুইবার কাটল।
- ২ বছর পর: ১০ জনের টিম, একসাথে কাজ করলে একে অন্যের কোড ভাঙছে।
প্রতিটি সমস্যার সমাধান হিসেবে একটা নতুন concept আসবে। এভাবে আপনি শুধু "কী" নয়, "কেন" ও শিখবেন। এটাই সত্যিকারের architect এর মানসিকতা।
System Design আসলে কী?
সহজ কথায়: একটা সফটওয়্যার কীভাবে অনেক মানুষের জন্য, নির্ভরযোগ্যভাবে, দ্রুত এবং সাশ্রয়ীভাবে কাজ করবে, তার পরিকল্পনা।
বাড়ি বানানোর সাথে তুলনা করুন। ঘর (code) ঠিক থাকলেও যদি ভিত্তি (architecture) দুর্বল হয়, তিনতলার পর আর বাড়ানো যায় না। আবার একটা ছোট টিনের ঘরের জন্য আকাশচুম্বী ভবনের ভিত্তি বানানোও অপচয়। ভালো architect জানেন কখন কতটা লাগবে।
Architecture vs Design: পার্থক্য
| Application Architecture | System Design | |
|---|---|---|
| ফোকাস | অ্যাপের ভেতরের কাঠামো, layer, module | পুরো সিস্টেম: server, DB, cache, network |
| প্রশ্ন | কোড কীভাবে সাজাব? | লক্ষ ইউজার কীভাবে সামলাব? |
| উদাহরণ | Clean Architecture, CQRS | Load Balancer, Sharding, CDN |
দুটোই একে অপরের সাথে জড়িত। এই গাইডে দুটোই পাবেন।
সবচেয়ে গুরুত্বপূর্ণ কথা: Trade-off
System design এ "সেরা সমাধান" বলে কিছু নেই। আছে কোন পরিস্থিতিতে কোনটা উপযুক্ত। প্রতিটি সিদ্ধান্তে কিছু পাওয়া যায়, কিছু ছাড়তে হয়। এই গাইডের প্রতিটি অধ্যায়ে আমরা এই প্রশ্ন করব: এর দাম কী?
প্রস্তুতি
- C# এর basics (class, interface, async/await, LINQ)
- ASP.NET Core এর প্রাথমিক ধারণা
- SQL এর basics
- কোড চালাতে .NET 9 বা তার পরের SDK (এই গাইডের কোড .NET 8+ এ চলবে)
Part 1: ভিত্তি: নেটওয়ার্ক, Scalability ও Distributed ধারণা
আপনি যদি এই Part ভালোভাবে বোঝেন, বাকি সব Part সহজ লাগবে। এখানে আমরা সেই শব্দগুলো শিখব, যেগুলো system design এর প্রতিটি আলোচনায় আসে।
১.১ Client-Server মডেল
আপনি ব্রাউজারে shopeasy.com লিখলে যা ঘটে:
- ব্রাউজার (Client) একটা অনুরোধ (Request) পাঠায়।
- কোনো কম্পিউটার (Server) সেটা গ্রহণ করে, প্রসেস করে।
- সার্ভার একটা উত্তর (Response) ফেরত দেয়।
রেস্টুরেন্টের উদাহরণ: আপনি (client) ওয়েটারকে অর্ডার দিলেন, রান্নাঘর (server) রান্না করল, ওয়েটার খাবার এনে দিল। ওয়েটার হলো network।
// সবচেয়ে ছোট ASP.NET Core server var app = WebApplication.Create(args); app.MapGet("/hello", () => "Hello from ShopEasy!"); app.Run(); ১.২ ব্রাউজারে URL লিখলে আসলে কী ঘটে (পুরো যাত্রা)
- DNS lookup:
shopeasy.comএকটা নাম। কম্পিউটার চেনে IP ঠিকানা (যেমন203.0.113.10)। DNS হলো ইন্টারনেটের ফোনবুক, নাম দিলে IP বলে দেয়। - TCP connection: client ও server এর মধ্যে সংযোগ তৈরি হয় (3-way handshake: SYN, SYN-ACK, ACK)।
- TLS handshake: HTTPS হলে এনক্রিপশন কী বিনিময় হয়।
- HTTP request:
GET /products HTTP/1.1 - Server processing: load balancer, web server, application, database।
- HTTP response:
200 OKসহ JSON/HTML। - Browser render: পেজ দেখানো।
প্রতিটি ধাপে সময় লাগে। এগুলো বোঝা মানে বোঝা, ধীর কোথায়।
১.৩ HTTP: সার্ভারের সাথে কথা বলার ভাষা
Method গুলো:
| Method | কাজ | Safe? | Idempotent? |
|---|---|---|---|
| GET | ডেটা পড়া | হ্যাঁ | হ্যাঁ |
| POST | নতুন কিছু তৈরি | না | না |
| PUT | সম্পূর্ণ replace | না | হ্যাঁ |
| PATCH | আংশিক পরিবর্তন | না | সাধারণত না |
| DELETE | মুছে ফেলা | না | হ্যাঁ |
Idempotent মানে: একই request একাধিকবার পাঠালে ফলাফল একই থাকে। এটা Part 5 এ অত্যন্ত গুরুত্বপূর্ণ, কারণ নেটওয়ার্ক ফেল করলে client আবার চেষ্টা (retry) করে। ShopEasy তে পেমেন্ট দুবার কাটার মূল কারণ এটাই।
Status code এর পরিবার:
-
2xxসফল (200 OK, 201 Created, 204 No Content) -
3xxredirect (301, 302, 304 Not Modified) -
4xxclient এর ভুল (400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 429 Too Many Requests) -
5xxserver এর ভুল (500, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout)
১.৪ Latency, Throughput ও Bandwidth
তিনটি শব্দ প্রায়ই গুলিয়ে যায়। পানির পাইপের উদাহরণ:
- Latency: পানি পাইপের এক মাথা থেকে অন্য মাথায় পৌঁছাতে কত সময় লাগে (যেমন ১২০ ms)।
- Bandwidth: পাইপ কত মোটা, অর্থাৎ সর্বোচ্চ কত পানি যেতে পারে।
- Throughput: আসলে প্রতি সেকেন্ডে কত পানি যাচ্ছে (যেমন ৫০০ request/sec)।
প্রতিটি অপারেশনের আনুমানিক সময় জানা থাকলে design সহজ হয়:
| অপারেশন | আনুমানিক সময় |
|---|---|
| মেমরি থেকে পড়া | ~100 ns |
| SSD থেকে পড়া | ~100 µs |
| একই datacenter এর মধ্যে network call | ~0.5 ms |
| Database query (index সহ) | ~1-10 ms |
| ঢাকা থেকে সিঙ্গাপুর network round trip | ~60-80 ms |
| ঢাকা থেকে ইউরোপ/আমেরিকা | ~150-250 ms |
(সংখ্যাগুলো আনুমানিক; হার্ডওয়্যার ও নেটওয়ার্ক ভেদে বদলায়।) শিক্ষা: network call ও disk মেমরির চেয়ে বহু গুণ ধীর। তাই caching এত শক্তিশালী।
পারসেন্টাইল: গড় (average) response time প্রায়ই মিথ্যা বলে। p95/p99 latency দেখুন। p99 = ২০০ms মানে ৯৯% request ২০০ms এর মধ্যে শেষ হয়, বাকি ১% এর চেয়ে ধীর। বড় সিস্টেমে ১% ও লক্ষ লক্ষ মানুষ।
১.৫ Scalability: সিস্টেম বড় হলে সামলানোর ক্ষমতা
ShopEasy এর ঈদের সেলে ট্রাফিক ১০০ গুণ বাড়ল। দুটি উপায়:
Vertical Scaling (Scale Up): একই সার্ভারে বেশি CPU/RAM দেওয়া।
- সুবিধা: সহজ, কোড বদলাতে হয় না।
- অসুবিধা: একটা সীমা আছে, দাম অতিরিক্ত বাড়ে, single point of failure।
Horizontal Scaling (Scale Out): আরও অনেক সার্ভার যোগ করা।
- সুবিধা: প্রায় অসীম বাড়ানো যায়, একটা সার্ভার গেলে বাকিরা চলে।
- অসুবিধা: জটিলতা বাড়ে (load balancing, shared state, data consistency)।
উপমা: একজন সুপারম্যান শ্রমিক (vertical) বনাম অনেক সাধারণ শ্রমিক (horizontal)।
Stateless বনাম Stateful: Horizontal scaling এর চাবিকাঠি
ধরুন ইউজারের shopping cart সার্ভার-১ এর মেমরিতে আছে। পরের request load balancer সার্ভার-২ তে পাঠাল। cart উধাও!
// ❌ Stateful: scale করা যায় না public class CartController : Controller { private static Dictionary<int, List<Item>> _carts = new(); // এই server এর মেমরিতে } // ✅ Stateless: state বাইরে (Redis/DB) রাখা হয়েছে public class CartController(ICartStore store) : Controller { public async Task<IActionResult> Get(int userId) => Ok(await store.GetAsync(userId)); // যেকোনো server এ একই ফল } নিয়ম: application server কে stateless রাখুন। state যাবে database, cache বা বাইরের store এ।
১.৬ Availability ও Reliability
Availability = সিস্টেম কত সময় চালু থাকে। "nine" দিয়ে মাপা হয়:
| Availability | বছরে সর্বোচ্চ বন্ধ থাকার সময় |
|---|---|
| 99% (দুই নাইন) | ~৩.৬৫ দিন |
| 99.9% (তিন নাইন) | ~৮.৭৬ ঘণ্টা |
| 99.99% (চার নাইন) | ~৫২.৬ মিনিট |
| 99.999% (পাঁচ নাইন) | ~৫.২৬ মিনিট |
প্রতিটি অতিরিক্ত nine এর খরচ ও জটিলতা নাটকীয়ভাবে বাড়ে। ব্যবসার প্রয়োজন বুঝে লক্ষ্য ঠিক করুন; একটি ব্লগের জন্য ৯৯.৯৯৯% দরকার নেই।
Reliability = সিস্টেম ঠিকঠাক কাজ করছে কি না (ভুল ফলাফল দিচ্ছে না তো?)। একটা সিস্টেম available কিন্তু unreliable হতে পারে (সবসময় চালু, কিন্তু ভুল ব্যালেন্স দেখাচ্ছে)।
Redundancy: একই কাজের জন্য একাধিক কপি রাখা, যাতে একটা ভাঙলে অন্যটা কাজ করে। Single Point of Failure (SPOF): যে একটি জিনিস ভাঙলে পুরো সিস্টেম বসে যায়। Architect এর কাজ হলো SPOF খুঁজে বের করা।
SLI, SLO, SLA:
- SLI: আসলে কী মাপছেন (যেমন সফল request এর শতাংশ)।
- SLO: লক্ষ্য (যেমন ৯৯.৯% সফল)।
- SLA: গ্রাহকের সাথে চুক্তি, ভাঙলে জরিমানা।
১.৭ Distributed System এর মূল সমস্যা
একটা সার্ভারে সব সহজ। একাধিক সার্ভার হলে নতুন সমস্যা আসে, যেগুলোকে বলে Fallacies of Distributed Computing:
- নেটওয়ার্ক নির্ভরযোগ্য নয় (প্যাকেট হারায়)
- Latency শূন্য নয়
- সার্ভারগুলোর ঘড়ি হুবহু এক নয়
- যেকোনো মুহূর্তে একটা node ফেল করতে পারে
- Partial failure: কিছু অংশ কাজ করে, কিছু করে না। সবচেয়ে কঠিন, কারণ আপনি জানেন না অপর পাশ ফেল করেছে নাকি শুধু ধীর।
CAP Theorem
Distributed data store এ তিনটি জিনিসের মধ্যে একসাথে সর্বোচ্চ দুটি পাওয়া যায়:
- C (Consistency): সবাই সবসময় একই সর্বশেষ ডেটা দেখে।
- A (Availability): প্রতিটি request এর একটা উত্তর মেলে (ভুল/পুরনো হলেও)।
- P (Partition tolerance): নেটওয়ার্ক ভেঙে সার্ভারগুলো আলাদা হয়ে গেলেও সিস্টেম চলে।
বাস্তবে network partition অনিবার্য, তাই প্রকৃত পছন্দ হলো CP বনাম AP, partition চলাকালীন।
গল্প: ঢাকা ও চট্টগ্রামে ShopEasy এর দুটো ডেটাসেন্টার। দুটোর মাঝের লাইন কেটে গেল। একজন ইউজার ঢাকায় শেষ ১টা পণ্য কিনল, আরেকজন একই সময়ে চট্টগ্রামে।
- CP বেছে নিলে: চট্টগ্রাম বলবে "এখন অর্ডার নেওয়া যাচ্ছে না"। ডেটা সঠিক থাকল, কিন্তু সেবা বন্ধ।
- AP বেছে নিলে: দুজনের অর্ডারই নেবে, পরে ঝামেলা মেটাবে (একজনকে ক্ষমা চেয়ে refund)। সেবা চালু, কিন্তু ডেটা সাময়িক অসঙ্গত।
ব্যাংকের ব্যালেন্সে সাধারণত CP, সোশ্যাল মিডিয়ার "like count" এ AP চলে।
Consistency মডেল
- Strong consistency: লেখার পরপর যে কেউ পড়লে নতুন মান পাবে। সহজ, কিন্তু ধীর ও ব্যয়বহুল।
- Eventual consistency: লেখার পর কিছুক্ষণ কেউ পুরনো মান দেখতে পারে, কিন্তু শেষে সবাই একই মানে পৌঁছায়। দ্রুত, scale করা সহজ।
- Read-your-writes: আপনি নিজে যা লিখেছেন সেটা অন্তত নিজে দেখবেন।
উদাহরণ: Facebook এ কমেন্ট করার পর বন্ধু দুই সেকেন্ড পর দেখে। এটা eventual consistency, আর তাতে কারও ক্ষতি নেই।
PACELC
CAP শুধু partition এর সময়ের কথা বলে। PACELC বলে: partition (P) হলে A বা C বেছে নাও; নইলে (Else) স্বাভাবিক সময়েও Latency (L) বনাম Consistency (C) এর মধ্যে বেছে নিতে হয়। শক্তিশালী consistency চাইলে একাধিক node এর সম্মতি লাগে, তাই ধীর হয়।
১.৮ Load Balancer: ট্রাফিক ভাগ করার ট্রাফিক পুলিশ
একাধিক server থাকলে ইউজারের request কোনটায় যাবে? Load Balancer ঠিক করে।
Algorithm:
- Round Robin: পালা করে (১, ২, ৩, ১, ২, ৩...)
- Least Connections: যার কাজ কম, তাকে
- IP Hash: একই ইউজার সবসময় একই server এ (sticky session)
- Weighted: শক্তিশালী server কে বেশি
Layer 4 বনাম Layer 7: L4 শুধু IP/port দেখে (দ্রুত)। L7 HTTP এর ভেতরে (URL, header) দেখে সিদ্ধান্ত নেয়, যেমন /api/* এক জায়গায়, /images/* অন্য জায়গায়। Nginx, HAProxy, Azure Application Gateway, AWS ALB জনপ্রিয়।
Health check: load balancer নিয়মিত জিজ্ঞেস করে "তুমি ঠিক আছো?" (/health endpoint)। ফেল করলে সেই server কে ট্রাফিক দেওয়া বন্ধ।
১.৯ Proxy, Reverse Proxy, CDN
- Forward proxy: ক্লায়েন্টের পক্ষ হয়ে বাইরে যায় (যেমন অফিসের ইন্টারনেট ফিল্টার)।
- Reverse proxy: সার্ভারের সামনে বসে, ক্লায়েন্টের request নিয়ে ভেতরের server এ পাঠায়। কাজ: load balancing, TLS termination, caching, নিরাপত্তা। (Nginx, YARP, Cloudflare)
- CDN (Content Delivery Network): ছবি, CSS, JS, ভিডিওর মতো static ফাইলের কপি বিশ্বজুড়ে edge সার্ভারে রাখে। ঢাকার ইউজার সিঙ্গাপুরের edge থেকে ছবি পায়, আমেরিকার origin server থেকে নয়। ফলে latency অনেক কমে।
১.১০ DNS এর আরেকটু গভীরে
- A record: নাম থেকে IPv4
- CNAME: এক নামের alias আরেক নাম
- TTL: cache এ কতক্ষণ রাখা যাবে (কম TTL = দ্রুত বদল, বেশি DNS query)
- DNS দিয়েও load balancing ও geo-routing করা যায় (ইউজারকে কাছের region এ পাঠানো)।
Part 1 এর সারসংক্ষেপ
- Network ও disk ধীর; বেশি call মানে বেশি latency।
- Horizontal scaling এর শর্ত: app server stateless।
- Availability এর প্রতিটি nine এর দাম আছে; SPOF খুঁজুন।
- Distributed system এ failure স্বাভাবিক; CAP/PACELC তে trade-off বুঝে বেছে নিন।
- Load balancer, reverse proxy, CDN হলো ট্রাফিক সামলানোর প্রথম অস্ত্র।
Part 2: Code-level Design: SOLID, Principles ও Design Pattern
বড় architecture এর আগে ছোট কোড ঠিক করতে হয়। ৬ মাস পর ShopEasy এর কোড এমন হয়েছে: একটা OrderService ক্লাসে ২,০০০ লাইন, যেখানে অর্ডার, পেমেন্ট, ইমেইল, ইনভেন্টরি সব মিশে আছে। একটা জায়গা বদলালে তিন জায়গা ভাঙে। এই Part সেই সমস্যার সমাধান।
২.১ খারাপ কোডের উদাহরণ (সমস্যা চিনুন)
public class OrderService { public void PlaceOrder(Order order) { // 1. validation if (order.Items.Count == 0) throw new Exception("Empty"); // 2. database using var conn = new SqlConnection("Server=...;Password=123"); conn.Open(); // ... SQL insert ... // 3. payment if (order.PaymentType == "bkash") { /* bkash API call */ } else if (order.PaymentType == "card") { /* card API call */ } // 4. email var smtp = new SmtpClient("smtp.gmail.com"); smtp.Send("..."); } } সমস্যা: একাধিক দায়িত্ব, hard-coded dependency, নতুন পেমেন্ট (Nagad) এলে এই ক্লাসই বদলাতে হবে, test লেখা প্রায় অসম্ভব (সত্যিকারের DB ও SMTP লাগে)। SOLID এগুলোই ঠিক করে।
২.২ SOLID: পাঁচটি মূলনীতি
S: Single Responsibility Principle (SRP)
একটা ক্লাসের পরিবর্তনের কারণ একটাই হওয়া উচিত। একটা ক্লাস একটা কাজ করবে।
উপমা: একজন শেফ যদি রান্না, হিসাব, ডেলিভারি সব করে, কোনোটাই ভালো হয় না।
public class OrderValidator { public void Validate(Order o) { /* ... */ } } public class OrderRepository { public Task SaveAsync(Order o) { /* ... */ } } public class EmailNotifier { public Task SendAsync(Order o) { /* ... */ } } O: Open/Closed Principle (OCP)
বাড়ানোর জন্য খোলা, বদলানোর জন্য বন্ধ। নতুন ফিচার যোগ করতে পুরনো কোড না ছুঁয়ে নতুন কোড লিখুন।
// ❌ নতুন পেমেন্ট এলেই if-else বাড়ে if (type == "bkash") {...} else if (type == "card") {...} // ✅ Interface দিয়ে public interface IPaymentMethod { string Name { get; } Task<PaymentResult> PayAsync(decimal amount); } public class BkashPayment : IPaymentMethod { /* ... */ } public class CardPayment : IPaymentMethod { /* ... */ } public class NagadPayment : IPaymentMethod { /* নতুন ফাইল, পুরনো কোড অক্ষত */ } L: Liskov Substitution Principle (LSP)
Parent এর জায়গায় Child বসালে প্রোগ্রাম ঠিকমতো চলতে হবে। Child যেন parent এর প্রতিশ্রুতি না ভাঙে।
// ❌ Ostrich bird এর Fly() এ exception ছুড়লে LSP ভাঙে public class Bird { public virtual void Fly() {} } public class Ostrich : Bird { public override void Fly() => throw new NotSupportedException(); } // ✅ আলাদা interface public interface IFlyable { void Fly(); } public class Sparrow : IFlyable { public void Fly() {} } public class Ostrich { /* Fly নেই */ } লক্ষণ: কোডে বারবার if (x is SpecificType) বা NotImplementedException দেখলে LSP সম্ভবত ভাঙছে।
I: Interface Segregation Principle (ISP)
বড় interface ভেঙে ছোট ছোট interface বানান। ক্লায়েন্টকে এমন method মানতে বাধ্য করবেন না যা সে ব্যবহার করে না।
// ❌ মোটা interface public interface IWorker { void Work(); void Eat(); void Sleep(); } // ✅ ছোট interface public interface IWorkable { void Work(); } public interface IFeedable { void Eat(); } D: Dependency Inversion Principle (DIP)
উচ্চস্তরের মডিউল নিম্নস্তরের concrete ক্লাসের ওপর নির্ভর করবে না; উভয়ই abstraction এর ওপর নির্ভর করবে।
উপমা: আপনার ল্যাপটপ চার্জার সরাসরি দেয়ালে ঝালাই করা নেই; মাঝে সকেট (abstraction) আছে, তাই যেকোনো চার্জার লাগে।
public class OrderService( IOrderRepository repo, IPaymentMethod payment, INotifier notifier) // সবই abstraction { public async Task PlaceOrderAsync(Order order) { await payment.PayAsync(order.Total); await repo.SaveAsync(order); await notifier.SendAsync(order); } } এখন unit test এ fake/mock পাস করা যায়, ও production এ আসল implementation।
২.৩ Dependency Injection (DI) ও IoC
DIP হলো নীতি, DI হলো সেটা বাস্তবায়নের কৌশল: dependency নিজে new না করে বাইরে থেকে ঢুকিয়ে দেওয়া। .NET এ DI built-in।
var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>(); builder.Services.AddScoped<IPaymentMethod, BkashPayment>(); builder.Services.AddSingleton<INotifier, EmailNotifier>(); builder.Services.AddScoped<OrderService>(); Lifetime (খুব গুরুত্বপূর্ণ):
| Lifetime | মানে | কখন |
|---|---|---|
| Transient | প্রতিবার নতুন instance | হালকা, stateless সার্ভিস |
| Scoped | প্রতি HTTP request এ একটা | DbContext, Unit of Work |
| Singleton | পুরো অ্যাপে একটা | Cache, config, HttpClient factory |
সাধারণ ভুল (Captive Dependency): Singleton এর ভেতরে Scoped service ঢোকানো। Scoped instance আর কখনো ছাড়া পায় না, ফলে DbContext এর পুরনো state ও thread-safety সমস্যা হয়। .NET development mode এ এটা ধরে ফেলে।
একই interface এর একাধিক implementation লাগলে .NET 8+ এ Keyed Services আছে:
builder.Services.AddKeyedScoped<IPaymentMethod, BkashPayment>("bkash"); builder.Services.AddKeyedScoped<IPaymentMethod, CardPayment>("card"); app.MapPost("/pay", ([FromKeyedServices("bkash")] IPaymentMethod p) => p.PayAsync(100)); ২.৪ অন্যান্য মৌলিক নীতি
- DRY (Don't Repeat Yourself): একই লজিক ৩ জায়গায় কপি করবেন না। তবে সতর্ক: দুটি কোড দেখতে একই, কিন্তু ভিন্ন কারণে বদলায়, সেগুলো জোর করে এক করলে ক্ষতি হয়।
- KISS (Keep It Simple): সহজ সমাধান আগে। জটিল design এর দরকার প্রমাণিত না হওয়া পর্যন্ত নয়।
- YAGNI (You Aren't Gonna Need It): "হয়তো ভবিষ্যতে লাগবে" ভেবে ফিচার বানাবেন না।
- Separation of Concerns: UI, business logic, data access আলাদা রাখুন।
- Law of Demeter:
a.GetB().GetC().DoSomething()এভাবে চেইন করবেন না; শুধু নিকট বন্ধুর সাথে কথা বলুন। - Composition over Inheritance: inheritance এর গভীর hierarchy এর চেয়ে composition (ক্লাসের ভেতর অন্য ক্লাস ব্যবহার) বেশি নমনীয়।
২.৫ Design Pattern: পরীক্ষিত সমাধানের রেসিপি
Design pattern হলো বারবার আসা সমস্যার প্রমাণিত সমাধান। তিন ভাগে: Creational (তৈরি), Structural (সাজানো), Behavioral (আচরণ)। নিচে বাস্তবে সবচেয়ে বেশি লাগে এমনগুলো।
Strategy: আচরণ বদলানো যায় এমন
সমস্যা: ShopEasy তে শিপিং খরচের একাধিক নিয়ম (সাধারণ, এক্সপ্রেস, ফ্রি শিপিং)।
public interface IShippingStrategy { decimal Calculate(Order o); } public class StandardShipping : IShippingStrategy { public decimal Calculate(Order o) => 60m; } public class ExpressShipping : IShippingStrategy { public decimal Calculate(Order o) => 150m; } public class FreeShipping : IShippingStrategy { public decimal Calculate(Order o) => 0m; } public class Checkout(IShippingStrategy shipping) { public decimal Total(Order o) => o.Subtotal + shipping.Calculate(o); } আপনি আগে যে IPaymentMethod দেখলেন, সেটাও Strategy।
Factory: কোন ক্লাস বানাতে হবে সেই সিদ্ধান্ত লুকানো
public class ShippingFactory { public IShippingStrategy Create(Order o) => o switch { { Subtotal: > 5000 } => new FreeShipping(), { IsExpress: true } => new ExpressShipping(), _ => new StandardShipping() }; } Decorator: মূল কোড না ছুঁয়ে আচরণ যোগ
ধরুন প্রতিটি repository call এ caching যোগ করতে চান, কিন্তু মূল ক্লাস বদলাতে চান না।
public class CachedProductRepository(IProductRepository inner, IMemoryCache cache) : IProductRepository { public async Task<Product?> GetAsync(int id) { return await cache.GetOrCreateAsync($"p:{id}", async e => { e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); return await inner.GetAsync(id); }); } } ASP.NET Core এর middleware pipeline ও Decorator/Chain of Responsibility এর বাস্তব উদাহরণ।
Observer / Event: একজনের খবরে অনেকে সাড়া দেয়
অর্ডার হলে ইমেইল যাবে, ইনভেন্টরি কমবে, অ্যানালিটিক্স আপডেট হবে। OrderService এদের কাউকে চেনে না; শুধু ঘোষণা করে "অর্ডার হয়েছে"।
public record OrderPlaced(int OrderId); public interface IEventHandler<T> { Task HandleAsync(T e); } public class SendEmailHandler : IEventHandler<OrderPlaced> { /* ... */ } public class ReduceStockHandler : IEventHandler<OrderPlaced> { /* ... */ } এই ধারণাই পরে Domain Events ও Message Broker এ বড় আকারে ফিরে আসবে।
Repository ও Unit of Work
Repository: ডেটাবেস অ্যাক্সেসের লজিক একটা interface এর পেছনে লুকায়, যাতে business logic জানে না ডেটা SQL এ আছে না MongoDB তে।
public interface IOrderRepository { Task<Order?> GetByIdAsync(int id); Task AddAsync(Order order); } Unit of Work: একাধিক repository এর কাজ একটাই transaction এ কমিট করে (সবকিছু সফল, নয়তো কিছুই না)।
public interface IUnitOfWork { Task<int> SaveChangesAsync(CancellationToken ct = default); } সতর্কতা: EF Core এর DbContext নিজেই Repository + Unit of Work। এর ওপর আরেকটা generic IRepository<T> বসালে অনেক সময় শুধু অকারণ স্তর বাড়ে ও LINQ এর শক্তি হারায়। ছোট/মাঝারি প্রজেক্টে সরাসরি DbContext, অথবা নির্দিষ্ট ও অর্থবহ repository (যেমন IOrderRepository.GetPendingOrders()) ব্যবহার করুন।
Singleton
পুরো অ্যাপে একটাই instance। .NET এ নিজে static singleton লেখার বদলে DI এর AddSingleton ব্যবহার করুন। Global state লুকিয়ে test কঠিন করে, তাই সাবধান।
Builder, Adapter, Facade (সংক্ষেপে)
- Builder: জটিল অবজেক্ট ধাপে ধাপে বানানো (
WebApplication.CreateBuilderএই pattern)। - Adapter: বেমানান interface কে মানানসই করা। যেমন তৃতীয়পক্ষের bKash SDK কে নিজেদের
IPaymentMethodএ মোড়ানো। - Facade: জটিল সাবসিস্টেমের সামনে সহজ একটা দরজা।
২.৬ Anti-pattern: যা এড়াবেন
| Anti-pattern | সমস্যা |
|---|---|
| God Class | একটা ক্লাস সব জানে ও সব করে |
| Anemic Domain Model | Entity তে শুধু property, সব লজিক service এ (Part 3 এ আরও) |
| Service Locator | serviceProvider.GetService<T>() ছড়িয়ে দেওয়া; dependency লুকানো থাকে |
| Premature Optimization | প্রমাণ ছাড়াই "পারফরম্যান্সের জন্য" জটিল করা |
| Over-engineering | ৫ ইউজারের অ্যাপে ১০ স্তর |
| Magic String/Number | "bkash" ছড়িয়ে থাকা, constant/enum নেই |
২.৭ Clean Code এর অভ্যাস (সংক্ষেপে)
- নাম অর্থবহ রাখুন:
GetActiveCustomers(),Get()নয়। - ফাংশন ছোট ও একটা কাজের।
- মন্তব্য দিয়ে খারাপ কোড ঢাকবেন না; কোডই ব্যাখ্যা করবে।
- Exception দিয়ে স্বাভাবিক flow control করবেন না।
- Nullable reference types (
<Nullable>enable</Nullable>) চালু রাখুন। - Magic value এর বদলে
record,enum,const।
Part 2 এর সারসংক্ষেপ
- SOLID মানে: ছোট দায়িত্ব, বাড়ানো সহজ, বদলানো নিরাপদ, abstraction এর ওপর নির্ভরতা।
- DI + interface = test করা যায় ও বদলানো যায় এমন কোড।
- Pattern মুখস্থ নয়, সমস্যা চিনে ব্যবহার করুন। সবচেয়ে বেশি কাজে লাগে Strategy, Factory, Decorator, Observer, Repository।
- YAGNI মনে রাখুন: pattern যেন জটিলতা বাড়ানোর অজুহাত না হয়।
Part 3: Application Architecture: কোড ও সিস্টেম কীভাবে সাজাবেন
এখন প্রশ্ন: ShopEasy এর পুরো প্রজেক্ট কীভাবে সাজালে ১০ জনের টিম একসাথে কাজ করতে পারবে, আর ৫ বছর পরেও কোড বোঝা যাবে? উত্তর নির্ভর করে আপনার প্রয়োজন ও টিমের আকারের ওপর। এখানে প্রতিটি style এর সুবিধা, অসুবিধা ও কখন ব্যবহার করবেন, সব থাকবে।
৩.১ Monolith বনাম Microservices: সবচেয়ে বড় বিতর্ক
Monolith: পুরো অ্যাপ একটাই deployable ইউনিট (একটা .exe/একটা docker image)।
Microservices: অনেক ছোট ছোট স্বাধীন সার্ভিস, প্রতিটার নিজস্ব ডেটাবেস ও deployment।
উপমা: Monolith হলো একটা বড় রেস্টুরেন্ট যার একটাই রান্নাঘর। Microservices হলো ফুড কোর্ট যেখানে প্রতিটি দোকান আলাদা, নিজের মতো চলে, কিন্তু সমন্বয়ের ঝামেলা বেশি।
| বিষয় | Monolith | Microservices |
|---|---|---|
| শুরুতে গতি | খুব দ্রুত | ধীর (অনেক সেটআপ) |
| Deployment | একবারে সব | প্রতিটি আলাদাভাবে |
| Scaling | পুরোটা একসাথে | যেটার দরকার শুধু সেটা |
| Debugging | সহজ (একটাই process) | কঠিন (distributed tracing লাগে) |
| Data consistency | সহজ (এক transaction) | কঠিন (eventual consistency, Saga) |
| টিম | ছোট টিমের জন্য ভালো | অনেক স্বাধীন টিমের জন্য |
| অপারেশন খরচ | কম | বেশি (Kubernetes, monitoring, CI/CD) |
| Technology | একটাই stack | প্রতিটি আলাদা হতে পারে |
মূল শিক্ষা: অনেক স্টার্টআপ শুরুতেই Microservices বানিয়ে "Distributed Monolith" এ পরিণত হয়, যেখানে microservices এর সব খরচ আছে কিন্তু সুবিধা নেই। ছোট টিম ও নতুন প্রডাক্ট হলে Modular Monolith দিয়ে শুরু করুন।
৩.২ Layered (N-Tier) Architecture
সবচেয়ে পরিচিত, সবচেয়ে পুরনো style। কোডকে স্তরে ভাগ করা:
Presentation (Controllers/UI) ↓ Business Logic (Services) ↓ Data Access (Repositories/EF Core) ↓ Database নিয়ম: ওপরের স্তর নিচেরটাকে ডাকে, উল্টোটা নয়।
ShopEasy.sln ├─ ShopEasy.Web (Controllers) ├─ ShopEasy.Business (Services) ├─ ShopEasy.Data (EF Core, Repositories) └─ ShopEasy.Models (Entities, DTOs) সুবিধা: সহজ, সবাই চেনে, ছোট অ্যাপে চমৎকার।
অসুবিধা: ধীরে ধীরে Business স্তর Data স্তরের ওপর নির্ভরশীল হয়ে পড়ে (EF Core এর entity সর্বত্র ছড়ায়)। ডেটাবেস বদলানো কঠিন। Domain logic ছড়িয়ে যায়।
৩.৩ Clean Architecture (Onion / Hexagonal এর পরিবার)
Layered এর সমস্যা সমাধান করতে এই পরিবারের জন্ম। তিনটি নামে (Hexagonal/Ports & Adapters, Onion, Clean) মূল ধারণা একই:
Dependency Rule: নির্ভরতা সবসময় ভেতরের দিকে। ভেতরের স্তর বাইরের কিছু জানে না।
পেঁয়াজের স্তরের মতো কল্পনা করুন (ভেতর থেকে বাইরে):
- Domain (কেন্দ্র): Entity, business নিয়ম। কোনো framework এর ওপর নির্ভর করে না।
- Application: Use case (যেমন "অর্ডার দাও")। Interface (port) ঘোষণা করে, যেমন
IOrderRepository। - Infrastructure: EF Core, ইমেইল, bKash API। Application এর interface গুলো implement করে (adapter)।
- Presentation/API (বাইরে): Controllers/Minimal API।
জাদ...
