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।
জাদুটা কোথায়: Application স্তর IOrderRepository interface ঘোষণা করে, আর Infrastructure সেটা বাস্তবায়ন করে। তাই Business logic জানেই না যে SQL Server আছে। কাল MongoDB এ গেলে শুধু Infrastructure বদলাবে।
Solution structure:
ShopEasy.sln
├─ ShopEasy.Domain (কোনো dependency নেই)
├─ ShopEasy.Application → Domain
├─ ShopEasy.Infrastructure → Application, Domain
└─ ShopEasy.Api → Application, Infrastructure (শুধু DI সাজানোর জন্য)
কোড উদাহরণ:
// Domain
public class Order
{
public int Id { get; private set; }
public OrderStatus Status { get; private set; } = OrderStatus.Pending;
private readonly List<OrderItem> _items = new();
public IReadOnlyList<OrderItem> Items => _items;
public decimal Total => _items.Sum(i => i.Price * i.Quantity);
public void AddItem(Product p, int qty)
{
if (qty <= 0) throw new DomainException("Quantity must be positive");
_items.Add(new OrderItem(p.Id, p.Price, qty));
}
public void Confirm()
{
if (_items.Count == 0) throw new DomainException("Empty order");
Status = OrderStatus.Confirmed;
}
}
// Application
public interface IOrderRepository
{
Task<Order?> GetAsync(int id, CancellationToken ct);
Task AddAsync(Order order, CancellationToken ct);
}
// Infrastructure
public class EfOrderRepository(AppDbContext db) : IOrderRepository
{
public Task<Order?> GetAsync(int id, CancellationToken ct)
=> db.Orders.Include(o => o.Items).FirstOrDefaultAsync(o => o.Id == id, ct);
public async Task AddAsync(Order order, CancellationToken ct)
=> await db.Orders.AddAsync(order, ct);
}
লক্ষ্য করুন Order এর private set ও Confirm() method: business নিয়ম entity এর ভেতরেই। এটাই Rich Domain Model। বিপরীত হলো Anemic Model (শুধু getter/setter, সব লজিক service এ), যেটা এড়ানো ভালো।
কখন Clean Architecture: মাঝারি-বড় প্রজেক্ট, জটিল business logic, দীর্ঘমেয়াদি, test সহজ করতে চান।
কখন নয়: ছোট CRUD অ্যাপ, প্রোটোটাইপ। অনেক boilerplate ফাইল, ছোট অ্যাপে অপ্রয়োজনীয় জটিলতা।
৩.৪ Vertical Slice Architecture
Layered এ একটা ফিচার (যেমন "অর্ডার দাও") ছড়িয়ে থাকে চারটা ফোল্ডারে (Controller, Service, Repository, Model)। Vertical Slice এ ফিচার অনুযায়ী কোড সাজানো হয়, প্রতিটি slice এর সবকিছু একসাথে।
Features/
├─ Orders/
│ ├─ PlaceOrder/
│ │ ├─ PlaceOrderCommand.cs
│ │ ├─ PlaceOrderHandler.cs
│ │ ├─ PlaceOrderValidator.cs
│ │ └─ PlaceOrderEndpoint.cs
│ └─ GetOrder/ ...
└─ Products/ ...
সুবিধা: একটা ফিচার বদলাতে এক জায়গায় কাজ, ছোট ছোট ফিচারে আলাদা সিদ্ধান্ত (সহজ ফিচারে সরাসরি DbContext, জটিলটায় domain model) নেওয়া যায়, টিমে merge conflict কম।
অসুবিধা: slice গুলোর মধ্যে কোড ডুপ্লিকেশনের ঝুঁকি, শৃঙ্খলা না থাকলে অগোছালো।
ব্যবহারিক পরামর্শ: Clean Architecture এর ভেতরে Application স্তরকে Vertical Slice এ সাজানোও খুব জনপ্রিয়।
৩.৫ CQRS ও MediatR
CQRS (Command Query Responsibility Segregation): ডেটা পরিবর্তনের মডেল (Command) ও পড়ার মডেল (Query) আলাদা রাখা।
কেন? লেখার সময় আমরা কঠোর নিয়ম, validation, transaction চাই। পড়ার সময় চাই গতি ও সুবিধাজনক আকার (যেমন একটা ড্যাশবোর্ডে ৫টা table জোড়া ডেটা)। দুটোর প্রয়োজন আলাদা।
Level ১ (সহজ, প্রায় সবখানে চলে): একই ডেটাবেস, কিন্তু Command ও Query এর কোড আলাদা।
Level ২: আলাদা read database (যেমন read এর জন্য denormalized view বা Elasticsearch)। eventual consistency আসে।
// Command
public record PlaceOrderCommand(int CustomerId, List<OrderLineDto> Lines)
: IRequest<int>;
public class PlaceOrderHandler(IOrderRepository repo, IUnitOfWork uow)
: IRequestHandler<PlaceOrderCommand, int>
{
public async Task<int> Handle(PlaceOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.CustomerId);
foreach (var l in cmd.Lines) order.AddItem(l.ProductId, l.Qty);
order.Confirm();
await repo.AddAsync(order, ct);
await uow.SaveChangesAsync(ct);
return order.Id;
}
}
// Query: সরাসরি projection, entity লোড না করে
public record GetOrderSummaryQuery(int Id) : IRequest<OrderSummaryDto?>;
public class GetOrderSummaryHandler(AppDbContext db)
: IRequestHandler<GetOrderSummaryQuery, OrderSummaryDto?>
{
public Task<OrderSummaryDto?> Handle(GetOrderSummaryQuery q, CancellationToken ct)
=> db.Orders.AsNoTracking()
.Where(o => o.Id == q.Id)
.Select(o => new OrderSummaryDto(o.Id, o.Status.ToString(), o.Total))
.FirstOrDefaultAsync(ct);
}
MediatR একটা জনপ্রিয় লাইব্রেরি, যা request কে সঠিক handler এ পৌঁছে দেয় ও Pipeline Behavior (validation, logging, transaction এর মতো cross-cutting কাজ) যোগ করার সুযোগ দেয়। কিছু সংস্করণ বাণিজ্যিক লাইসেন্সের আওতায় এসেছে, তাই প্রজেক্টে ব্যবহারের আগে বর্তমান লাইসেন্স নিয়ম দেখে নিন। চাইলে নিজেই সহজ dispatcher লিখতে পারেন, বা Wolverine/Mediator (source generator ভিত্তিক) এর মতো বিকল্প আছে। CQRS এর জন্য MediatR বাধ্যতামূলক নয়।
// Pipeline Behavior: প্রতিটি command এ স্বয়ংক্রিয় validation
public class ValidationBehavior<TReq, TRes>(IEnumerable<IValidator<TReq>> validators)
: IPipelineBehavior<TReq, TRes> where TReq : notnull
{
public async Task<TRes> Handle(TReq req, RequestHandlerDelegate<TRes> next, CancellationToken ct)
{
var failures = validators
.Select(v => v.Validate(req))
.SelectMany(r => r.Errors)
.ToList();
if (failures.Count != 0) throw new ValidationException(failures);
return await next();
}
}
কখন CQRS: পড়া ও লেখার প্রয়োজন খুব আলাদা, জটিল domain, read-heavy সিস্টেম। কখন নয়: সাধারণ CRUD।
৩.৬ Domain-Driven Design (DDD)
DDD হলো জটিল business কে কোডে সঠিকভাবে মডেল করার পদ্ধতি। মূল কথা: ডেভেলপার ও ব্যবসায়ী একই ভাষায় কথা বলবে, আর কোড সেই ভাষা প্রতিফলিত করবে।
Strategic Design (বড় ছবি):
-
Ubiquitous Language: টিমের সবার জন্য একই শব্দভান্ডার। ব্যবসায়ী "Order" বললে কোডেও
Order,PurchaseবাTransactionনয়। -
Bounded Context: একটা মডেলের সীমানা। ShopEasy তে "Product" শব্দের অর্থ ভিন্ন: Catalog এ (নাম, ছবি, বর্ণনা), Inventory তে (স্টক সংখ্যা), Shipping এ (ওজন, আকার)। প্রতিটি context এ আলাদা মডেল রাখুন, একটা বিশাল
Productক্লাস নয়। Microservices এর সীমানা ঠিক করার সবচেয়ে ভালো দিশা এই Bounded Context। - Context Map: context গুলোর পারস্পরিক সম্পর্ক (কে কার ওপর নির্ভর করে)।
Tactical Design (কোডের building block):
| Building block | অর্থ | উদাহরণ |
|---|---|---|
| Entity | পরিচয় (ID) আছে, সময়ের সাথে বদলায় |
Order, Customer
|
| Value Object | পরিচয় নেই, মান দিয়ে চেনা, immutable |
Money, Address
|
| Aggregate | সম্পর্কিত object এর গুচ্ছ, একটা consistency সীমানা |
Order (+ তার OrderItem) |
| Aggregate Root | aggregate এর একমাত্র প্রবেশদ্বার | Order |
| Domain Event | ব্যবসায় যা ঘটেছে | OrderPlaced |
| Domain Service | কোনো entity তে না ঢোকা লজিক | PricingService |
| Repository | Aggregate root সংরক্ষণ ও আনা | IOrderRepository |
Value Object উদাহরণ:
public readonly record struct Money(decimal Amount, string Currency)
{
public static Money operator +(Money a, Money b)
{
if (a.Currency != b.Currency) throw new DomainException("Currency mismatch");
return new Money(a.Amount + b.Amount, a.Currency);
}
}
decimal price এর বদলে Money ব্যবহার করলে "টাকা + ডলার" যোগ করার ভুল কম্পাইলার/রানটাইমেই ধরা পড়ে।
Aggregate নিয়ম:
- বাইরের কেউ aggregate এর ভেতরের object সরাসরি বদলাবে না; Root এর মাধ্যমে।
- এক transaction এ একটাই aggregate বদলান।
- aggregate গুলো একে অপরকে শুধু ID দিয়ে চেনে (পুরো object রেফারেন্স নয়)।
- Aggregate ছোট রাখুন।
Domain Event:
public abstract class Entity
{
private readonly List<IDomainEvent> _events = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _events;
protected void Raise(IDomainEvent e) => _events.Add(e);
}
public class Order : Entity
{
public void Confirm()
{
Status = OrderStatus.Confirmed;
Raise(new OrderConfirmed(Id)); // ঘোষণা, কে শুনবে জানে না
}
}
কখন DDD: জটিল business নিয়ম (ব্যাংকিং, বীমা, লজিস্টিক)। কখন নয়: সাধারণ CRUD বা ডেটা-কেন্দ্রিক অ্যাপ। DDD এর strategic অংশ (Ubiquitous Language, Bounded Context) প্রায় সব প্রজেক্টে কাজে লাগে, tactical অংশ শুধু জটিল domain এ।
৩.৭ Modular Monolith: বাস্তবে সবচেয়ে বুদ্ধিমান শুরু
একটাই deployable, কিন্তু ভেতরে কঠোর সীমানাসহ স্বাধীন module (প্রতিটি Bounded Context একটা module)।
ShopEasy (একটাই অ্যাপ)
├─ Modules/Catalog (নিজের DbContext/schema, public API)
├─ Modules/Ordering
├─ Modules/Inventory
└─ Modules/Payments
নিয়ম:
- Module গুলো একে অপরের ভেতরের ক্লাস ব্যবহার করবে না; শুধু ঘোষিত public interface/contract বা event এর মাধ্যমে।
- প্রতিটি module এর নিজস্ব ডেটাবেস schema, অন্য module এর table এ সরাসরি JOIN নয়।
- Module এর মধ্যে যোগাযোগ in-process (দ্রুত, সহজ transaction)।
- এই নিয়ম architecture test (যেমন
NetArchTest) দিয়ে স্বয়ংক্রিয়ভাবে প্রয়োগ করুন।
সুবিধা: Monolith এর সরলতা ও Microservices এর পরিষ্কার সীমানা, দুটোই। ভবিষ্যতে কোনো module আলাদা service এ সরানো সহজ।
৩.৮ Microservices বিস্তারিত
যখন প্রয়োজন সত্যিই আছে (বড় টিম, আলাদা scaling, স্বাধীন deployment):
ভালো microservice এর বৈশিষ্ট্য:
- একটা Bounded Context (ব্যবসায়িক সামর্থ্য) এর মালিক
- নিজস্ব ডেটাবেস (Database per Service; অন্যের DB তে ঢোকা নিষিদ্ধ)
- স্বাধীনভাবে deploy ও scale
- ছোট API/contract এর মাধ্যমে যোগাযোগ
যোগাযোগের ধরন:
| ধরন | কী | কখন |
|---|---|---|
| Synchronous (HTTP/gRPC) | সরাসরি call, উত্তরের জন্য অপেক্ষা | তাৎক্ষণিক উত্তর লাগে (যেমন দাম জানা) |
| Asynchronous (Message/Event) | বার্তা পাঠিয়ে এগিয়ে যাওয়া | ফলাফলের জন্য অপেক্ষা না করলেও চলে (ইমেইল, ইনভেন্টরি আপডেট) |
Microservices এর লুকানো খরচ:
- Network failure, latency, retry, timeout (Part 7)
- Distributed transaction (Saga, Part 6)
- Distributed tracing ও কেন্দ্রীয় logging (Part 9)
- সংস্করণ সামঞ্জস্য (contract versioning)
- বেশি infrastructure ও DevOps দক্ষতা
- Testing এর জটিলতা (contract test, integration test)
Microservices এ যাওয়ার আগে চেকলিস্ট:
- [ ] টিম কি একাধিক স্বাধীন দলে ভাগ হয়েছে?
- [ ] আলাদা অংশের scaling এর প্রয়োজন সত্যিই ভিন্ন?
- [ ] Monolith এর deployment কি টিমের গতির বাধা?
- [ ] CI/CD, monitoring, container এর পরিপক্কতা আছে?
- [ ] Bounded Context পরিষ্কারভাবে চেনা গেছে?
একটাও "না" হলে সম্ভবত এখনও সময় হয়নি।
Strangler Fig Pattern: পুরনো Monolith একবারে ফেলে নতুন বানানোর (Big Bang rewrite) ঝুঁকি এড়াতে ধীরে ধীরে একটা একটা ফিচার নতুন service এ সরান, সামনে একটা proxy/gateway দিয়ে ট্রাফিক ভাগ করুন, শেষে পুরনোটা আপনাআপনি ফাঁকা হয়ে যায়।
৩.৯ Event-Driven Architecture (EDA)
সার্ভিসগুলো সরাসরি একে অপরকে call না করে event প্রকাশ করে; আগ্রহীরা শোনে ও সাড়া দেয়।
ShopEasy তে: Ordering প্রকাশ করল OrderPlaced। Inventory শুনে স্টক কমাল, Notification ইমেইল পাঠাল, Analytics হিসাব রাখল। Ordering এদের কাউকে চেনেই না, নতুন কেউ যোগ হলেও Ordering বদলাতে হয় না (loose coupling)।
Event এর ধরন:
- Event Notification: "কিছু ঘটেছে" (ছোট বার্তা, দরকারে শ্রোতা জিজ্ঞেস করবে)
- Event-Carried State Transfer: event এ প্রয়োজনীয় ডেটাও থাকে (শ্রোতার আর ফেরত call লাগে না)
- Event Sourcing: বর্তমান অবস্থা না রেখে ঘটনার ধারাবাহিক তালিকা রাখা; যেকোনো সময়ের অবস্থা ঘটনাগুলো আবার চালিয়ে বের করা যায় (ব্যাংকের লেজারের মতো)। অডিট ও ইতিহাসে চমৎকার, কিন্তু জটিল; সবার জন্য নয়।
সুবিধা: loose coupling, সহজে নতুন কনজিউমার, প্রবল scalability। অসুবিধা: প্রবাহ বোঝা ও debug কঠিন, eventual consistency, বার্তার ক্রম ও ডুপ্লিকেট সামলানো (Part 6)।
৩.১০ Serverless ও অন্যান্য style
- Serverless (Azure Functions, AWS Lambda): server পরিচালনা না করে শুধু ফাংশন চালান, ব্যবহার অনুযায়ী বিল। সুবিধা: শূন্য ট্রাফিকে খরচ শূন্য, স্বয়ংক্রিয় scale। অসুবিধা: cold start, চলার সময়সীমা, vendor lock-in, স্থানীয় debug কঠিন। event-triggered কাজে (ছবি resize, scheduled job) দারুণ।
- SOA (Service-Oriented Architecture): Microservices এর পূর্বসূরি; বড় ও কেন্দ্রীয় bus (ESB) নির্ভর।
- Microkernel / Plugin: ছোট কোর + প্লাগইন (VS Code এর মতো)।
- Backend for Frontend (BFF): প্রতিটি ক্লায়েন্টের (web, mobile) জন্য আলাদা ছোট backend, যা ক্লায়েন্টের প্রয়োজনমতো ডেটা সাজায়।
৩.১১ কোন architecture কখন: সিদ্ধান্তের গাইড
| পরিস্থিতি | সুপারিশ |
|---|---|
| ১-৩ জনের টিম, নতুন প্রডাক্ট, MVP | সরল Layered বা Vertical Slice Monolith |
| সাধারণ CRUD, অ্যাডমিন প্যানেল | Layered, ন্যূনতম pattern |
| জটিল business, দীর্ঘমেয়াদি | Clean Architecture + DDD (tactical) |
| ৫-২০ জন, একাধিক ডোমেইন | Modular Monolith |
| ২০+ জন, স্বাধীন টিম, ভিন্ন scaling | Microservices |
| ফাইল/ইভেন্ট ট্রিগার করা ছোট কাজ | Serverless |
| Read ও write এর চাহিদা অনেক আলাদা | CQRS |
| অডিট/ইতিহাস অপরিহার্য | Event Sourcing (সতর্কভাবে) |
সোনালি নিয়ম: সবচেয়ে সরল যা প্রয়োজন মেটায় তা দিয়ে শুরু করুন। যখন ব্যথা অনুভব করবেন, তখনই জটিলতা যোগ করুন, আগে নয়।
৩.১২ ShopEasy তে প্রয়োগ
রাফির টিম এখন ৮ জন। সিদ্ধান্ত: Microservices নয়, Modular Monolith (Catalog, Ordering, Inventory, Payments)। প্রতিটি module Clean Architecture ধাঁচে, জটিল Ordering এ DDD, রিপোর্টিং এ CQRS এর পড়ার দিক। Module এর মধ্যে যোগাযোগ interface ও in-process event দিয়ে। এতে সবার কাজ আলাদা, ডেপ্লয় সহজ, আর ভবিষ্যতে Payments আলাদা service করা যাবে।
Part 3 এর সারসংক্ষেপ
- Architecture মানে trade-off; "সেরা" নেই, "উপযুক্ত" আছে।
- Layered সহজ; Clean Architecture দীর্ঘমেয়াদি test-যোগ্য কোডের জন্য; Vertical Slice ফিচার-কেন্দ্রিক।
- CQRS: পড়া-লেখার মডেল আলাদা; DDD: জটিল business মডেল করা; Bounded Context: সীমানা।
- Modular Monolith বেশিরভাগ ক্ষেত্রে সবচেয়ে বাস্তবসম্মত শুরু।
- Microservices এর সুবিধা বাস্তব, কিন্তু খরচও বিশাল; প্রয়োজন প্রমাণিত হলে তবেই।
Part 4: Database Design: ডেটা সংরক্ষণ ও দ্রুত অ্যাক্সেস
বাস্তবে বেশিরভাগ system এর বাধা application কোড নয়, database। ShopEasy এর পেজ ধীর হওয়ার ৮০% কারণ ছিল খারাপ query ও index এর অভাব। এই Part সেই জায়গায় আপনাকে শক্ত করবে।
৪.১ SQL বনাম NoSQL
Relational (SQL): table, row, column, সম্পর্ক (foreign key), শক্তিশালী transaction। (SQL Server, PostgreSQL, MySQL)
NoSQL: একাধিক ধরন:
| ধরন | উদাহরণ | কখন |
|---|---|---|
| Document | MongoDB, Cosmos DB | নমনীয় কাঠামো, product catalog |
| Key-Value | Redis, DynamoDB | cache, session, দ্রুত lookup |
| Wide-Column | Cassandra | বিপুল লেখা, time-series |
| Graph | Neo4j | সম্পর্ক-কেন্দ্রিক (বন্ধুর বন্ধু) |
| Search | Elasticsearch | full-text search |
সিদ্ধান্তের নিয়ম: সন্দেহ হলে Relational দিয়ে শুরু করুন (PostgreSQL বা SQL Server)। সম্পর্ক, transaction ও আড-হক query এ এর শক্তি অসাধারণ। NoSQL নিন যখন নির্দিষ্ট প্রয়োজন আছে (বিপুল scale, নমনীয় schema, বিশেষ ডেটা মডেল)।
Polyglot Persistence: একই সিস্টেমে ভিন্ন কাজে ভিন্ন DB। ShopEasy: অর্ডার ও পেমেন্ট SQL এ, পণ্য সার্চ Elasticsearch এ, cart ও session Redis এ।
৪.২ ডেটাবেস ডিজাইনের মূলনীতি
Normalization
ডুপ্লিকেট ডেটা সরানোর প্রক্রিয়া।
খারাপ (সব একটা table এ):
| OrderId | CustomerName | CustomerPhone | Product | Price |
|---|---|---|---|---|
| 1 | রাফি | 0171... | Phone | 20000 |
| 2 | রাফি | 0171... | Case | 500 |
রাফির ফোন বদলালে সব row আপডেট করতে হবে; একটা ভুলে গেলে ডেটা অসঙ্গত।
ভালো: Customers, Orders, OrderItems, Products আলাদা table, ID দিয়ে সংযুক্ত।
- 1NF: প্রতিটি cell এ একটাই মান (কমা দিয়ে একাধিক মান নয়)
- 2NF: Composite key এর অংশের ওপর আংশিক নির্ভরতা নেই
- 3NF: Non-key column শুধু key এর ওপর নির্ভর করে, অন্য non-key এর ওপর নয়
Denormalization
পড়ার গতির জন্য ইচ্ছাকৃতভাবে কিছু ডুপ্লিকেট রাখা (যেমন Order এ CustomerName এর কপি)। Read-heavy সিস্টেমে কাজে লাগে, তবে সংগতি রাখার দায় আপনার। নিয়ম: আগে normalize করুন, প্রমাণিত দরকারে denormalize।
Primary Key এর পছন্দ
-
Auto-increment int/bigint: সহজ, ছোট, দ্রুত; কিন্তু আন্দাজযোগ্য (
/orders/5এর পর/orders/6), একাধিক DB তে মেলানো কঠিন। -
GUID (UUID): distributed এ স্বাধীনভাবে তৈরি করা যায়; কিন্তু random GUID clustered index এ page split ঘটিয়ে ধীর করে। সমাধান: sequential GUID (.NET 9 এ
Guid.CreateVersion7()) বা ULID। - ডেটা নিরাপত্তায়: বাইরের API তে ধারাবাহিক ID প্রকাশ করলে ডেটা গণনা করা সহজ হয়; প্রয়োজনে public ID আলাদা রাখুন।
Data Type বাছাই
- টাকা:
decimal(কখনোইfloat/doubleনয়, ভগ্নাংশ ভুল হয়) - সময়: UTC তে
DateTimeOffset/datetime2সংরক্ষণ করুন - Unicode (বাংলা) এর জন্য
nvarchar(SQL Server) বা UTF-8 (PostgreSQL)
৪.৩ Index: দ্রুত query এর চাবিকাঠি
বইয়ের শেষের সূচিপত্র (index) এর মতো। সূচি না থাকলে একটা শব্দ খুঁজতে পুরো বই পড়তে হয় (Full Table Scan, O(n))। সূচি থাকলে সরাসরি পাতায় যাওয়া যায় (B-Tree, O(log n))।
-- ১০ লক্ষ order এ এই query ধীর (scan)
SELECT * FROM Orders WHERE CustomerId = 42;
-- index দিলে দ্রুত (seek)
CREATE INDEX IX_Orders_CustomerId ON Orders(CustomerId);
Index এর ধরন:
- Clustered: ডেটা যে ক্রমে ডিস্কে সাজানো (টেবিলে একটাই; সাধারণত PK)
- Non-clustered: আলাদা সূচি যা মূল ডেটার দিকে ইঙ্গিত করে
- Composite: একাধিক column (ক্রম গুরুত্বপূর্ণ)
-
Covering: query এর সব দরকারি column index এই থাকে (
INCLUDE), মূল table এ যেতে হয় না - Unique, Filtered/Partial: বিশেষ ক্ষেত্রে
Composite index এর leftmost নিয়ম: (CustomerId, Status) index টি WHERE CustomerId = ? ও WHERE CustomerId = ? AND Status = ? এ কাজ করে, কিন্তু শুধু WHERE Status = ? এ কাজে লাগে না।
Index এর দাম: প্রতিটি index পড়া দ্রুত করে, কিন্তু লেখা (INSERT/UPDATE/DELETE) ধীর ও জায়গা বেশি লাগায়। তাই সব column এ index দেবেন না। যে column গুলো WHERE, JOIN, ORDER BY এ ঘনঘন আসে, সেগুলোতে দিন।
Index নষ্ট করে যে ভুলগুলো:
WHERE YEAR(CreatedAt) = 2026 -- column এ function: index কাজ করে না
WHERE Name LIKE '%rafi' -- শুরুতে wildcard: scan
WHERE Phone = 1711111111 -- type mismatch (string কে number এর সাথে)
ঠিক করুন: WHERE CreatedAt >= '2026-01-01' AND CreatedAt < '2027-01-01'।
Execution Plan পড়তে শিখুন (SSMS এ "Include Actual Execution Plan", PostgreSQL এ EXPLAIN ANALYZE)। "Index Seek" ভালো, "Table Scan" ছোট table ছাড়া সন্দেহজনক।
৪.৪ Transaction ও ACID
উপমা: ব্যাংক ট্রান্সফার। ১০০০ টাকা A থেকে কেটে B তে যোগ হবে। মাঝপথে সার্ভার ক্র্যাশ করলে? টাকা উধাও হওয়া চলবে না।
- Atomicity: সব সফল নয়তো কিছুই নয়
- Consistency: ডেটা সবসময় বৈধ অবস্থা থেকে বৈধ অবস্থায় যায় (constraint মানা হয়)
- Isolation: একসাথে চলা transaction একে অপরকে বিভ্রান্ত করে না
- Durability: কমিট হলে ক্র্যাশেও ডেটা থাকে
await using var tx = await db.Database.BeginTransactionAsync(ct);
try
{
from.Balance -= amount;
to.Balance += amount;
await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
(SaveChangesAsync() একা নিজেই একটা transaction।)
Isolation Level ও সমস্যা
একসাথে অনেকে ডেটা বদলালে যা ঘটতে পারে:
- Dirty Read: কমিট না হওয়া ডেটা পড়া
- Non-repeatable Read: একই transaction এ দুবার পড়ে ভিন্ন মান
- Phantom Read: একই query তে দ্বিতীয়বার নতুন row দেখা যায়
| Level | Dirty | Non-repeatable | Phantom |
|---|---|---|---|
| Read Uncommitted | সম্ভব | সম্ভব | সম্ভব |
| Read Committed (সাধারণ default) | না | সম্ভব | সম্ভব |
| Repeatable Read | না | না | সম্ভব |
| Serializable | না | না | না |
উচ্চ isolation নিরাপদ, কিন্তু ধীর ও deadlock এর ঝুঁকি বেশি। SQL Server এ Read Committed Snapshot Isolation (RCSI) ও PostgreSQL এ MVCC পড়াকে লেখা থেকে আলাদা রেখে ভালো পারফরম্যান্স দেয়।
৪.৫ Concurrency নিয়ন্ত্রণ: ShopEasy এর "শেষ ১টা পণ্য" সমস্যা
দুজন একসাথে শেষ ফোনটা কিনছে। দুজনেই দেখল স্টক = ১। দুজনেই অর্ডার করল। স্টক হলো -১! একে Lost Update / Race Condition বলে।
১. Pessimistic Locking: পড়ার সময়ই row লক করা; অন্যরা অপেক্ষা করে। নিরাপদ, কিন্তু ধীর ও deadlock ঝুঁকি।
২. Optimistic Concurrency (EF Core এ জনপ্রিয়): লক নেই; সংরক্ষণের সময় দেখা হয় অন্য কেউ ইতোমধ্যে বদলেছে কি না।
public class Product
{
public int Id { get; set; }
public int Stock { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; } = default!; // SQL Server
}
try
{
product.Stock -= 1;
await db.SaveChangesAsync(ct);
}
catch (DbUpdateConcurrencyException)
{
// কেউ আগে বদলে ফেলেছে; আবার পড়ে পুনরায় চেষ্টা করুন, বা ব্যবহারকারীকে জানান
}
৩. Atomic আপডেট: সবচেয়ে সহজ ও কার্যকর অনেক সময়।
var rows = await db.Products
.Where(p => p.Id == id && p.Stock >= qty)
.ExecuteUpdateAsync(s => s.SetProperty(p => p.Stock, p => p.Stock - qty), ct);
if (rows == 0) return Results.Conflict("Out of stock");
এক SQL এ "যদি স্টক থাকে, তবে কমাও", ফলে দুজন একসাথে পারে না।
৪.৬ ORM: EF Core বনাম Dapper
| বিষয় | EF Core | Dapper |
|---|---|---|
| ধরন | পূর্ণ ORM | Micro-ORM |
| SQL লেখা | LINQ দিয়ে | নিজে লেখেন |
| Change tracking | আছে | নেই |
| Migration | আছে | নেই |
| গতি | ভালো (সঠিক ব্যবহারে) | সর্বোচ্চ |
| কখন | সাধারণ CRUD, domain model | জটিল রিপোর্ট, গতি-সংবেদনশীল query |
বাস্তব পরামর্শ: একসাথে দুটো ব্যবহার করা যায়: লেখার জন্য EF Core, জটিল read query তে Dapper বা raw SQL।
EF Core এর সাধারণ সমস্যা ও সমাধান
১. N+1 Query সমস্যা (সবচেয়ে কুখ্যাত):
// ❌ ১টা query orders এর জন্য + প্রতিটি order এর জন্য আরও ১টা = N+1
var orders = await db.Orders.ToListAsync();
foreach (var o in orders)
Console.WriteLine(o.Items.Count); // lazy loading এ প্রতিবার DB hit
// ✅ একটা query তে
var orders = await db.Orders.Include(o => o.Items).ToListAsync();
// ✅ আরও ভালো: শুধু যা দরকার
var data = await db.Orders
.Select(o => new { o.Id, ItemCount = o.Items.Count })
.ToListAsync();
১০০০ অর্ডার হলে আগেরটা ১০০১টা DB call; পরেরটা ১টা।
২. শুধু পড়ার জন্য AsNoTracking(): change tracking এর খরচ বাঁচে।
৩. পুরো entity না এনে Select projection: ১০টা column লাগলে ৫০টা আনবেন না।
৪. Pagination ছাড়া সব ডেটা আনা: .Take()/.Skip() বা Keyset pagination ব্যবহার করুন (Part 5)।
৫. ToList() আগে filter পরে: ❌ db.Orders.ToList().Where(...) পুরো table মেমরিতে আনে। ✅ Where আগে, ToList শেষে।
৬. Cartesian Explosion: একাধিক collection এ Include করলে row সংখ্যা গুণ হয়। সমাধান AsSplitQuery()।
৭. Compiled Query, Bulk অপারেশন: ExecuteUpdate/ExecuteDelete দিয়ে বড় আপডেট; বিপুল insert এ EFCore.BulkExtensions জাতীয় লাইব্রেরি।
৮. Migration: schema পরিবর্তন কোডে ভার্সন করুন (dotnet ef migrations add ...)। Production এ সরাসরি অ্যাপ চালু হওয়ার সময় migrate না করে CI/CD pipeline এ আলাদা ধাপ বা script হিসেবে চালানো নিরাপদ।
৪.৭ বড় scale এর কৌশল
ShopEasy তে ডেটা ও ট্রাফিক বাড়ছে; এক DB সার্ভার হাঁপিয়ে উঠছে। ধাপে ধাপে:
ধাপ ১: Query ও Index ঠিক করুন, Cache যোগ করুন। (৯০% সমস্যা এখানেই মেটে।)
ধাপ ২: Vertical scaling: বড় সার্ভার।
ধাপ ৩: Read Replica (Replication): মূল DB (Primary) তে লেখা, কপি (Replica) গুলোতে পড়া।
- Primary → Replica এ ডেটা অনুলিপি হয় (সাধারণত async)
- Replication lag: সাময়িক পুরনো ডেটা (eventual consistency)
- Read-heavy অ্যাপে (বেশিরভাগ ওয়েব) বিরাট সুবিধা
- Primary ফেল করলে Replica কে Primary বানানো যায় (failover, high availability)
// পড়ার জন্য আলাদা connection string
builder.Services.AddDbContext<WriteDbContext>(o => o.UseSqlServer(primaryConn));
builder.Services.AddDbContext<ReadDbContext>(o => o.UseSqlServer(replicaConn));
(এটাই CQRS এর বাস্তব ব্যবহারের একটা রূপ।)
ধাপ ৪: Partitioning: একটা বিশাল table কে ছোট ভাগে ভাঙা (একই DB সার্ভারে)। যেমন Orders কে মাস অনুযায়ী ভাগ। পুরনো ডেটা আর্কাইভ সহজ, query দ্রুত।
ধাপ ৫: Sharding (Horizontal Partitioning): ডেটা একাধিক আলাদা DB সার্ভারে ভাগ। যেমন ইউজার ID অনুযায়ী: ১-১০ লক্ষ Shard-A তে, ১০-২০ লক্ষ Shard-B তে।
- Shard key বাছাই সবচেয়ে গুরুত্বপূর্ণ সিদ্ধান্ত; খারাপ key এ কিছু shard এ চাপ জমে (hot shard)
- Range-based: ID এর পরিসর অনুযায়ী (সহজ, কিন্তু hot spot ঝুঁকি)
- Hash-based: ID এর hash দিয়ে (সমানভাবে ছড়ায়, কিন্তু range query কঠিন)
- Directory-based: একটা lookup table বলে দেয় কোন ডেটা কোথায়
- Consistent Hashing: shard যোগ/বাদ দিলে কম ডেটা সরাতে হয়
- দাম: shard পেরিয়ে JOIN ও transaction কঠিন বা অসম্ভব; জটিলতা বিপুল
সোনালি নিয়ম: Sharding শেষ অস্ত্র। প্রথমে index, cache, replica, partitioning ও ডেটা আর্কাইভ সব চেষ্টা করুন।
৪.৮ ডেটা আর্কাইভ, Soft Delete ও Audit
-
Soft Delete:
IsDeletedফ্ল্যাগ; EF Core এর Global Query Filter দিয়ে স্বয়ংক্রিয়ভাবে লুকানো যায়। গোপনীয়তা আইনে (যেমন ইউজার ডেটা মুছে ফেলার অনুরোধ) সতর্ক থাকুন। -
Audit Trail: কে, কখন, কী বদলাল।
CreatedAt/By,UpdatedAt/Byবা আলাদা audit table। SQL Server এ Temporal Table আছে। - ডেটা সংরক্ষণ নীতি (Retention): পুরনো লগ/ডেটা কতদিন রাখবেন তা ঠিক করুন।
৪.৯ Backup, Recovery ও Migration কৌশল
- RPO (Recovery Point Objective): সর্বোচ্চ কতটুকু ডেটা হারানো সহ্য করবেন (যেমন ৫ মিনিট)
- RTO (Recovery Time Objective): কত সময়ে পুনরায় চালু করতে হবে (যেমন ১ ঘণ্টা)
- নিয়মিত backup নিন এবং restore পরীক্ষা করুন। যে backup কখনো restore করে দেখা হয়নি, সেটা backup নয়, আশা।
- Zero-downtime migration: Expand → Migrate → Contract পদ্ধতি। নতুন column আগে যোগ করুন (নতুন ও পুরনো কোড দুটোই চলে), ডেটা সরান, শেষে পুরনোটা সরান। এক ধাপে column এর নাম বদলালে ডেপ্লয়ের সময় পুরনো কোড ভাঙে।
Part 4 এর সারসংক্ষেপ
- সন্দেহে Relational; প্রয়োজন বুঝে NoSQL; Polyglot হতে পারে।
- Index পড়া দ্রুত করে কিন্তু লেখা ধীর করে; execution plan পড়ুন।
- Concurrency সামলান: atomic update বা optimistic concurrency।
- N+1,
AsNoTracking, projection, pagination: EF Core এর মৌলিক পারফরম্যান্স অভ্যাস। - Scale এর ক্রম: query/index → cache → replica → partition → shard।
Part 5: API Design: সিস্টেমের মুখ
API হলো আপনার সিস্টেমের সাথে বাইরের জগতের চুক্তি। একবার প্রকাশ করলে বদলানো কঠিন, কারণ মোবাইল অ্যাপ, পার্টনার সবাই এর ওপর নির্ভর করে। তাই শুরু থেকেই ভালোভাবে ডিজাইন করুন।
৫.১ REST এর মূলকথা
REST একটা architectural style: resource কেন্দ্রিক। সবকিছু একটা "জিনিস" (noun), তার ওপর HTTP method দিয়ে কাজ।
ভালো URL ডিজাইন:
| কাজ | ❌ খারাপ | ✅ ভালো |
|---|---|---|
| সব অর্ডার | GET /getAllOrders |
GET /orders |
| একটা অর্ডার | GET /getOrder?id=5 |
GET /orders/5 |
| নতুন অর্ডার | POST /createOrder |
POST /orders |
| আপডেট | POST /updateOrder |
PUT /orders/5 |
| মুছুন | GET /deleteOrder/5 |
DELETE /orders/5 |
| অর্ডারের আইটেম | GET /getOrderItems/5 |
GET /orders/5/items |
নিয়ম: বহুবচন noun, ক্রিয়া নয়; ছোট হাতের অক্ষর ও hyphen; নেস্টিং ২ স্তরের বেশি নয়।
সঠিক Status Code ফেরত দিন:
-
201 Created+Locationheader নতুন resource তৈরিতে -
204 No Contentসফল কিন্তু ফেরত দেওয়ার কিছু নেই -
400ভুল ইনপুট,404পাওয়া যায়নি,409সংঘর্ষ (conflict),422যাচাই ব্যর্থ -
401পরিচয় নেই (login করেনি),403পরিচয় আছে কিন্তু অনুমতি নেই -
429অনেক বেশি request
৫০০ কখনো নিজে ছুড়বেন না কোনো স্বাভাবিক ব্যবসায়িক ভুলের জন্য। ৫০০ মানে "আমাদের সার্ভারে সমস্যা"।
৫.২ ASP.NET Core তে API: Minimal API বনাম Controller
Minimal API (আধুনিক, কম boilerplate):
var orders = app.MapGroup("/api/v1/orders")
.RequireAuthorization();
orders.MapGet("/{id:int}", async (int id, IMediator m, CancellationToken ct) =>
{
var result = await m.Send(new GetOrderSummaryQuery(id), ct);
return result is null ? Results.NotFound() : Results.Ok(result);
});
orders.MapPost("/", async (PlaceOrderCommand cmd, IMediator m, CancellationToken ct) =>
{
var id = await m.Send(cmd, ct);
return Results.Created($"/api/v1/orders/{id}", new { id });
});
Controller: বড় প্রজেক্টে সুপরিচিত কাঠামো, filter, model binding এর সুবিধা।
[ApiController]
[Route("api/v1/[controller]")]
public class OrdersController(IMediator mediator) : ControllerBase
{
[HttpGet("{id:int}")]
public async Task<ActionResult<OrderSummaryDto>> Get(int id, CancellationToken ct)
{
var r = await mediator.Send(new GetOrderSummaryQuery(id), ct);
return r is null ? NotFound() : Ok(r);
}
}
দুটোই ঠিক; টিমের পছন্দ ও প্রজেক্ট বুঝে বেছে নিন। CancellationToken সবসময় পাস করুন: ক্লায়েন্ট সংযোগ ছেড়ে দিলে সার্ভারের কাজ থামে, সম্পদ বাঁচে।
৫.৩ DTO: Entity সরাসরি বাইরে দেবেন না
Database entity কখনো API response বা request হিসেবে সরাসরি ব্যবহার করবেন না।
কারণ: ভেতরের কাঠামো ফাঁস, গোপন ফিল্ড (যেমন PasswordHash) বেরিয়ে যাওয়ার ঝুঁকি, Mass Assignment আক্রমণ (ক্লায়েন্ট IsAdmin=true পাঠিয়ে দিল), আর DB বদলালে API ভাঙা।
// Request DTO: শুধু যা ইউজার পাঠাতে পারবে
public record CreateProductRequest(string Name, decimal Price);
// Response DTO: শুধু যা দেখানো উচিত
public record ProductResponse(int Id, string Name, decimal Price);
৫.৪ Validation ও Error Response
ইনপুট কখনো বিশ্বাস করবেন না। FluentValidation বা DataAnnotations:
public class CreateProductValidator : AbstractValidator<CreateProductRequest>
{
public CreateProductValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.Price).GreaterThan(0);
}
}
Error এর আদর্শ ফরম্যাট: RFC 9457 Problem Details (.NET এ built-in):
builder.Services.AddProblemDetails();
app.UseExceptionHandler(); // IExceptionHandler দিয়ে কাস্টমাইজ করুন
// ফল:
// { "type": "...", "title": "Validation failed", "status": 422,
// "errors": { "Price": ["must be greater than 0"] }, "traceId": "..." }
Global Exception Handler: প্রতিটি controller এ try-catch নয়; এক জায়গায় ধরুন, লগ করুন, ক্লায়েন্টকে নিরাপদ বার্তা দিন (stack trace কখনো নয়)।
৫.৫ Pagination: বিশাল তালিকা সামলানো
১০ লক্ষ পণ্য একসাথে ফেরত দেওয়া যায় না।
Offset Pagination: GET /products?page=3&pageSize=20
var items = await db.Products.OrderBy(p => p.Id)
.Skip((page - 1) * size).Take(size).ToListAsync(ct);
সহজ, কিন্তু বড় পৃষ্ঠা নম্বরে ধীর (OFFSET 1000000 মানে ১০ লক্ষ row পেরিয়ে যেতে হয়) ও পড়ার মাঝে ডেটা বদলালে row ডুপ্লিকেট/হারায়।
Keyset (Cursor) Pagination: GET /products?after=12345&limit=20
var items = await db.Products
.Where(p => p.Id > afterId)
.OrderBy(p => p.Id)
.Take(limit).ToListAsync(ct);
Index ব্যবহার করে সবসময় দ্রুত, infinite scroll এর জন্য আদর্শ। দাম: নির্দিষ্ট পৃষ্ঠায় লাফ দেওয়া যায় না।
নিয়ম: সবসময় একটা সর্বোচ্চ pageSize সীমা রাখুন (যেমন ১০০)।
৫.৬ Filtering, Sorting ও Search
GET /products?category=phone&minPrice=10000&sort=-price (- মানে descending)।
সতর্কতা: sort/filter এর ফিল্ড নাম ইউজারের কাছ থেকে নিয়ে সরাসরি query তে বসাবেন না; অনুমোদিত তালিকা (allow-list) রাখুন। বিশাল full-text search এর জন্য database নয়, Elasticsearch/Azure AI Search জাতীয় ব্যবহার করুন।
৫.৭ API Versioning
আজ আপনার API কাজ করে। কাল আপনি ফিল্ড বদলালেন, পুরনো মোবাইল অ্যাপ ভাঙল! ভার্সন দিয়ে বাঁচুন।
| পদ্ধতি | উদাহরণ | মন্তব্য |
|---|---|---|
| URL path | /api/v1/orders |
সবচেয়ে স্পষ্ট ও জনপ্রিয় |
| Query string | /orders?api-version=1.0 |
সহজ |
| Header | X-Api-Version: 1 |
URL পরিষ্কার, কম আবিষ্কারযোগ্য |
.NET এ Asp.Versioning.Http প্যাকেজ।
ভাঙা-না-ভাঙা পরিবর্তন:
- ✅ নিরাপদ: নতুন ঐচ্ছিক ফিল্ড/endpoint যোগ
- ❌ ভাঙে: ফিল্ড মুছা/নাম বদল, type বদল, নতুন বাধ্যতামূলক ইনপুট
পুরনো ভার্সন কবে বন্ধ হবে (Deprecation নীতি) আগে জানান।
৫.৮ Idempotency: পেমেন্ট দুবার কাটা ঠেকানো
ShopEasy এর সেই সমস্যা: ইউজার "Pay" চাপল, নেটওয়ার্ক ধীর, response ফেরেনি, অ্যাপ আবার চেষ্টা করল। দুবার টাকা কাটল।
সমাধান: Idempotency Key। ক্লায়েন্ট প্রতিটি আসল কাজের জন্য একটা অনন্য key পাঠায়। সার্ভার একই key দ্বিতীয়বার পেলে নতুন কাজ না করে আগের ফলাফল ফেরত দেয়।
app.MapPost("/payments", async (
[FromHeader(Name = "Idempotency-Key")] string key,
PaymentRequest req, AppDbContext db, IPaymentGateway gw, CancellationToken ct) =>
{
var existing = await db.IdempotencyRecords.FindAsync([key], ct);
if (existing is not null)
return Results.Json(existing.Response, statusCode: existing.StatusCode);
var result = await gw.ChargeAsync(req, ct);
db.IdempotencyRecords.Add(new(key, 201, result));
await db.SaveChangesAsync(ct); // key এর ওপর unique constraint রাখুন
return Results.Created($"/payments/{result.Id}", result);
});
(প্রোডাকশনে "processing" অবস্থার জন্য লক/অবস্থা-ট্র্যাকিং দরকার, নইলে একই key এ দুটো সমান্তরাল request একসাথে ঢুকতে পারে। key এর ওপর unique constraint এ এটা সামলানো যায়।)
GET/PUT/DELETE প্রকৃতিগতভাবে idempotent; POST এর জন্য এটা আপনাকে বানাতে হবে।
৫.৯ Rate Limiting ও Throttling
এক ক্লায়েন্ট বা বট সবার সার্ভার দখল করতে পারে। রক্ষা:
builder.Services.AddRateLimiter(o =>
{
o.RejectionStatusCode = 429;
o.AddFixedWindowLimiter("api", c =>
{
c.PermitLimit = 100;
c.Window = TimeSpan.FromMinutes(1);
});
});
app.UseRateLimiter();
app.MapGet("/products", () => ...).RequireRateLimiting("api");
অ্যালগরিদম:
- Fixed Window: প্রতি মিনিটে ১০০। সহজ; জানালার সীমানায় দ্বিগুণ burst সম্ভব
- Sliding Window: আরও মসৃণ
- Token Bucket: বালতিতে টোকেন জমে, প্রতি request এ একটা খরচ; ছোট burst অনুমোদিত, গড় হার নিয়ন্ত্রিত
- Concurrency Limiter: একসাথে সর্বোচ্চ N টা চলমান request
একাধিক server থাকলে counter শেয়ার করতে Redis লাগে। Retry-After header দিন।
৫.১০ gRPC, GraphQL ও REST: কখন কোনটা
| REST | gRPC | GraphQL | |
|---|---|---|---|
| প্রোটোকল | HTTP/JSON | HTTP/2 + Protobuf (binary) | HTTP + JSON, একটাই endpoint |
| গতি | ভালো | সর্বোচ্চ | ভালো |
| ব্রাউজার সাপোর্ট | সরাসরি | সীমিত (gRPC-Web লাগে) | সরাসরি |
| Contract | OpenAPI |
.proto ফাইল (কঠোর) |
Schema |
| Streaming | সীমিত | দ্বিমুখী streaming | Subscription |
| কখন | পাবলিক API, সাধারণ | ভেতরের microservice যোগাযোগ, কম latency | বিভিন্ন ক্লায়েন্ট, নমনীয় ডেটা চাহিদা |
GraphQL এ ক্লায়েন্ট ঠিক করে কী কী ডেটা চাই (over/under-fetching কমে)। তবে caching, N+1, query জটিলতা সীমিত করা ও নিরাপত্তা সামলাতে বাড়তি যত্ন লাগে। .NET এ HotChocolate জনপ্রিয়।
gRPC উদাহরণ:
syntax = "proto3";
service Inventory {
rpc CheckStock (StockRequest) returns (StockReply);
}
message StockRequest { int32 product_id = 1; }
message StockReply { int32 available = 1; }
৫.১১ HTTP Caching ও Conditional Request
-
Cache-Control: public, max-age=300ব্রাউজার/CDN কে ৫ মিনিট রাখতে বলে। -
ETag: response এর "আঙুলের ছাপ"। ক্লায়েন্ট পরেরবার
If-None-Match: "abc"পাঠায়; ডেটা না বদলালে সার্ভার304 Not Modified(বডি ছাড়া) দেয়, ব্যান্ডউইথ বাঁচে। -
Optimistic concurrency ও ETag/
If-Match: আপডেটের সময় পাঠানো ETag না মিললে412 Precondition Failed, ফলে একজনের বদল অন্যজন অজান্তে মুছে দেয় না। - Output Caching (
app.UseOutputCache()) সার্ভার-পাশে response cache করে।
৫.১২ API Gateway ও BFF
মাইক্রোসার্ভিসে ক্লায়েন্ট ১০টা সার্ভিসকে সরাসরি ডাকবে না। মাঝে API Gateway একটা একক প্রবেশপথ:
- Routing, Authentication, Rate limiting, SSL termination, Logging এক জায়গায়
- Request aggregation (একাধিক সার্ভিস থেকে ডেটা জুড়ে একটা response)
.NET এ YARP (Yet Another Reverse Proxy, Microsoft এর), Ocelot; ক্লাউডে Azure API Management, AWS API Gateway, Kong।
BFF (Backend for Frontend): মোবাইল ও ওয়েবের জন্য আলাদা gateway, যাতে প্রতিটি ক্লায়েন্ট ঠিক যা চায় তা পায়।
৫.১৩ API ডকুমেন্টেশন
OpenAPI/Swagger দিয়ে স্বয়ংক্রিয় ডকুমেন্টেশন। .NET 9 এ built-in Microsoft.AspNetCore.OpenApi আছে; UI এর জন্য Scalar বা Swagger UI যুক্ত করা যায়। ভালো API ডকুমেন্টে উদাহরণ ও error কোড থাকে।
৫.১৪ Async কাজ ও Webhook
দীর্ঘ কাজ (রিপোর্ট তৈরি, ভিডিও প্রসেসিং) HTTP request এ আটকে রাখবেন না।
Async Request-Reply:
-
POST /reports→202 Accepted+Location: /reports/job-77 - ক্লায়েন্ট
GET /reports/job-77poll করে →status: processing - শেষে
status: completed, url: ...
Webhook: সার্ভার নিজেই ক্লায়েন্টের URL এ খবর পাঠায় (যেমন পেমেন্ট সফল হলে gateway আপনার সার্ভারে POST করে)। যাচাই করুন (signature), retry সামলান, idempotent রাখুন।
Part 5 এর সারসংক্ষেপ
- Resource-ভিত্তিক URL, সঠিক method ও status code।
- DTO ব্যবহার করুন; entity বাইরে নয়।
- Pagination বাধ্যতামূলক; বড় ডেটায় keyset।
- POST এর জন্য Idempotency Key, রক্ষায় Rate Limiting।
- Public API তে REST, ভেতরে gRPC, নমনীয়তায় GraphQL; Gateway দিয়ে প্রবেশপথ এক।
Part 6: Caching, Messaging ও Asynchronous Processing
ShopEasy এর ঈদের সেলের দিন হোমপেজ খুলতে ১০ সেকেন্ড লাগছে, কারণ প্রতিটি visitor এ একই "জনপ্রিয় পণ্য" query ডেটাবেসে যাচ্ছে। আবার অর্ডার করার সময় ইমেইল পাঠাতে গিয়ে ইউজারকে ৫ সেকেন্ড অপেক্ষা করতে হচ্ছে। এই Part এই দুই সমস্যার সমাধান: Cache (একই কাজ বারবার না করা) ও Messaging (ধীর কাজ পেছনে পাঠানো)।
৬.১ Caching: সবচেয়ে শক্তিশালী পারফরম্যান্স অস্ত্র
উপমা: বারবার লাইব্রেরি গিয়ে একই বই না খুলে প্রয়োজনীয় পাতার ফটোকপি টেবিলে রাখা।
মনে করুন, মেমরি থেকে পড়া ~100 ns, আর DB query ~5 ms (৫০,০০০ গুণ ধীর)। বহু ব্যবহারকারী একই ডেটা চাইলে cache বিরাট জয়।
Cache কোথায় বসে (স্তর):
- Browser cache: ক্লায়েন্টের নিজের কাছে
- CDN: ইউজারের কাছের edge সার্ভারে
- API Gateway/Reverse proxy cache
- Application in-memory cache: অ্যাপের নিজের মেমরিতে
- Distributed cache (Redis): একাধিক সার্ভারের শেয়ার করা
- Database cache: DB এর নিজস্ব buffer
৬.২ .NET এ Cache এর ধরন
১. In-Memory Cache (IMemoryCache): সবচেয়ে দ্রুত, কিন্তু প্রতিটি server এর নিজস্ব; একাধিক server এ আলাদা কপি, অসঙ্গত হতে পারে।
public class ProductService(IMemoryCache cache, AppDbContext db)
{
public Task<List<ProductDto>?> GetPopularAsync() =>
cache.GetOrCreateAsync("popular", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
entry.SlidingExpiration = TimeSpan.FromMinutes(1);
return await db.Products.AsNoTracking()
.OrderByDescending(p => p.SalesCount)
.Take(20)
.Select(p => new ProductDto(p.Id, p.Name, p.Price))
.ToListAsync();
});
}
২. Distributed Cache (IDistributedCache + Redis): সব সার্ভার একই cache দেখে। এটা network call (~১ ms), তাই memory এর চেয়ে ধীর, কিন্তু DB এর চেয়ে বহু দ্রুত।
builder.Services.AddStackExchangeRedisCache(o =>
o.Configuration = builder.Configuration.GetConnectionString("Redis"));
৩. HybridCache (.NET 9+): দুটো স্তর একসাথে (L1 in-memory + L2 Redis) এবং cache stampede protection সহ। নতুন কোডে এটাই সবচেয়ে সুবিধাজনক।
builder.Services.AddHybridCache();
public class ProductService(HybridCache cache, AppDbContext db)
{
public ValueTask<ProductDto?> GetAsync(int id, CancellationToken ct) =>
cache.GetOrCreateAsync($"product:{id}",
async token => await LoadFromDbAsync(id, token),
cancellationToken: ct);
}
৪. Output Caching ও Response Caching: পুরো HTTP response cache।
৬.৩ Caching এর কৌশল (Pattern)
| Pattern | কীভাবে কাজ করে | কখন |
|---|---|---|
| Cache-Aside (Lazy Loading) | অ্যাপ আগে cache দেখে; না পেলে DB থেকে এনে cache এ রাখে | সবচেয়ে সাধারণ, সবচেয়ে নিরাপদ শুরু |
| Read-Through | cache নিজেই DB থেকে আনে | cache লাইব্রেরি সমর্থন করলে |
| Write-Through | লেখার সময় cache ও DB দুটোতেই | cache সবসময় সতেজ চাই, লেখা ধীর |
| Write-Behind (Write-Back) | আগে cache এ, পরে async DB তে | লেখা অনেক, ডেটা হারানোর ঝুঁকি সহ্য্য |
| Refresh-Ahead | মেয়াদ শেষের আগেই আগাম নবায়ন | পূর্বানুমেয় হট ডেটা |
Cache-Aside কোড:
var key = $"product:{id}";
var cached = await cache.GetStringAsync(key, ct);
if (cached is not null) return JsonSerializer.Deserialize<ProductDto>(cached);
var product = await db.Products.FindAsync([id], ct); // cache miss
await cache.SetStringAsync(key, JsonSerializer.Serialize(product),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) }, ct);
return product;
৬.৪ Cache Invalidation: কম্পিউটার বিজ্ঞানের কঠিন সমস্যা
প্রবাদ: "Computer science এ দুটো কঠিন জিনিস: cache invalidation আর নামকরণ।" কারণ cache এ থাকা ডেটা পুরনো (stale) হয়ে যেতে পারে।
কৌশল:
- TTL (Time-To-Live): নির্দিষ্ট সময় পর মেয়াদ শেষ। সবচেয়ে সহজ; সামান্য পুরনো ডেটা সহনীয় হলে চমৎকার।
-
সক্রিয় invalidate: ডেটা বদলালে cache মুছে দেওয়া (
cache.Remove(key))। - Event-based: ডেটা বদলের event শুনে বিভিন্ন সার্ভারের cache মোছা।
-
Versioned key:
product:5:v12(সংস্করণ বদলালে নতুন key)।
সাধারণ সমস্যা:
- Cache Stampede (Thundering Herd): জনপ্রিয় key র মেয়াদ শেষ; এক মুহূর্তে হাজারো request সবাই DB তে যায়। সমাধান: লক/single-flight (HybridCache স্বয়ংক্রিয় করে), TTL তে র্যান্ডম জিটার, আগাম নবায়ন।
-
Cache Penetration: এমন key র বারবার অনুরোধ যা DB তেও নেই (যেমন
/products/-1); প্রতিবার DB hit। সমাধান: "পাওয়া যায়নি" ফলাফলও সংক্ষিপ্ত সময়ের জন্য cache, বা Bloom Filter। - Cache Avalanche: একসাথে অনেক key র মেয়াদ শেষ। সমাধান: TTL এ জিটার।
- Hot Key: একটা key এ বিপুল চাপ। সমাধান: local L1 cache, key প্রতিলিপি।
Eviction Policy: মেমরি ভরলে কী ফেলা হবে: LRU (সবচেয়ে কম সম্প্রতি ব্যবহৃত), LFU (সবচেয়ে কম ঘনঘন), FIFO।
কী cache করবেন, কী নয়: ঘনঘন পড়া, কম বদলানো, তৈরি করা ব্যয়বহুল ডেটা (পণ্য তালিকা, কনফিগ, রেফারেন্স ডেটা)। ব্যবহারকারী-নির্দিষ্ট সংবেদনশীল ডেটা (ভুল key তে অন্যের তথ্য দেখানোর ঝুঁকি) ও দ্রুত বদলানো ডেটা (শেষ স্টক সংখ্যা) সতর্কভাবে। কখনো প্রমাণ ছাড়া cache যোগ করবেন না; আগে মাপুন।
৬.৫ Redis: শুধু cache নয়
Redis একটা in-memory data structure store:
-
String: সাধারণ cache, counter (
INCRatomic) - Hash: অবজেক্ট (যেমন user session)
- List: queue, সাম্প্রতিক কার্যকলাপ
- Set / Sorted Set: unique তালিকা, leaderboard, rate limiting
- Pub/Sub, Streams: বার্তা আদান-প্রদান
- Distributed Lock: একাধিক সার্ভারে একই কাজ একবারই চালাতে
সতর্কতা: Redis মূলত মেমরিতে; persistence (RDB/AOF) থাকলেও এটাকে একমাত্র "source of truth" বানাবেন না, আর সংযোগ ব্যর্থ হলে অ্যাপ যেন DB তে fallback করতে পারে (graceful degradation)।
৬.৬ Message Queue: ধীর কাজ পেছনে পাঠানো
অর্ডার দিলে: বিলিং, ইনভয়েস PDF, ইমেইল, SMS, স্টক আপডেট। সব একসাথে করলে ইউজার ১০ সেকেন্ড অপেক্ষা করে, আর ইমেইল সার্ভার ডাউন থাকলে পুরো অর্ডারই ব্যর্থ!
সমাধান: ইউজারকে বলুন "অর্ডার গৃহীত"। বাকি কাজের জন্য বার্তা (message) queue তে রাখুন; আলাদা worker পরে সেগুলো করবে।
উপমা: রেস্টুরেন্টে ক্যাশিয়ার অর্ডার নিয়ে টিকিট রান্নাঘরে ঝুলিয়ে দেয়। ক্যাশিয়ার রান্নার জন্য অপেক্ষা করে না, পরের গ্রাহক নেয়।
শব্দাবলি:
- Producer: বার্তা পাঠায়
- Queue/Topic: বার্তা জমা থাকার জায়গা
- Consumer: বার্তা নিয়ে কাজ করে
- Broker: পুরো ব্যবস্থার ম্যানেজার (RabbitMQ, Kafka, Azure Service Bus)
সুবিধা:
- Decoupling: প্রেরক জানেই না কে নেবে
- Load leveling (Buffer): হঠাৎ ট্রাফিক-ঢেউ queue শোষে নেয়, consumer নিজের গতিতে সামলায়
- Resilience: consumer ডাউন থাকলেও বার্তা হারায় না
- Scalability: consumer সংখ্যা বাড়ালেই দ্রুত
৬.৭ Queue বনাম Pub/Sub বনাম Stream
- Point-to-Point Queue: প্রতিটি বার্তা একজন consumer পায় (কাজ ভাগ করা: Competing Consumers)। উদাহরণ: ১০০০ ইমেইল ১০ জন worker ভাগ করে।
-
Publish/Subscribe (Topic): প্রতিটি বার্তা সব subscriber পায়।
OrderPlaced→ Inventory, Email, Analytics সবাই। - Log/Stream (Kafka): বার্তা মুছে যায় না, ক্রমানুসারে লগে থাকে; consumer নিজের অবস্থান (offset) মনে রেখে পড়ে, চাইলে পুরনোটা আবার পড়তে পারে (replay)।
| RabbitMQ | Kafka | Azure Service Bus | |
|---|---|---|---|
| মডেল | Smart broker, queue/exchange | Distributed log | Managed enterprise broker |
| গতি/ধারণক্ষমতা | ভালো | অসাধারণ (লক্ষ/সেকেন্ড) | ভালো |
| Replay | সাধারণত না | হ্যাঁ | সীমিত |
| ক্রম (Ordering) | queue তে | partition এর ভেতরে | session দিয়ে |
| কখন | সাধারণ task queue, routing | ইভেন্ট স্ট্রিম, বিগ ডেটা, analytics | Azure এ, নির্ভরযোগ্য enterprise messaging |
.NET এ সহজ করার লাইব্রেরি: MassTransit, Wolverine, NServiceBus, বা সরাসরি client SDK। (বাণিজ্যিক লাইসেন্স নিয়ম বদলাতে পারে, ব্যবহারের আগে দেখে নিন।)
MassTransit উদাহরণ:
// Event
public record OrderPlaced(int OrderId, string CustomerEmail);
// Publish
await publishEndpoint.Publish(new OrderPlaced(order.Id, email), ct);
// Consumer
public class SendConfirmationEmail(IEmailSender mail) : IConsumer<OrderPlaced>
{
public Task Consume(ConsumeContext<OrderPlaced> ctx) =>
mail.SendAsync(ctx.Message.CustomerEmail, $"Order {ctx.Message.OrderId} confirmed");
}
// Registration
builder.Services.AddMassTransit(x =>
{
x.AddConsumer<SendConfirmationEmail>();
x.UsingRabbitMq((ctx, cfg) =>
{
cfg.Host("rabbitmq://localhost");
cfg.ConfigureEndpoints(ctx);
});
});
৬.৮ Messaging এর কঠিন সমস্যাগুলো (এখানেই পেশাদারিত্ব)
১. Delivery Guarantee
- At-most-once: সর্বোচ্চ একবার, হারাতে পারে
- At-least-once: অন্তত একবার, ডুপ্লিকেট হতে পারে (সবচেয়ে সাধারণ)
- Exactly-once: বাস্তবে প্রায় অসম্ভব, ব্যয়বহুল। কার্যকর সমাধান: At-least-once + Idempotent Consumer
২. Idempotent Consumer
বার্তা দুবার এলে প্রভাব একবারের সমান হতে হবে।
public async Task Consume(ConsumeContext<OrderPlaced> ctx)
{
var id = ctx.MessageId!.Value;
if (await db.ProcessedMessages.AnyAsync(m => m.Id == id)) return; // আগেই করা হয়েছে
await DoWorkAsync(ctx.Message);
db.ProcessedMessages.Add(new ProcessedMessage(id));
await db.SaveChangesAsync(); // কাজ + রেকর্ড একই transaction এ
}
৩. Retry ও Dead Letter Queue (DLQ)
প্রক্রিয়া ব্যর্থ হলে: কয়েকবার retry (exponential backoff সহ: ১s, ২s, ৪s...)। তবু ব্যর্থ হলে Dead Letter Queue এ রাখুন, যাতে একটা "বিষাক্ত বার্তা" (poison message) পুরো queue আটকে না দেয়। DLQ নিয়মিত পর্যবেক্ষণ করুন ও alert দিন।
৪. Message Ordering
ক্রম সবসময় নিশ্চিত নয়। যদি ক্রম জরুরি (যেমন একই অর্ডারের Created → Paid → Shipped), Kafka তে একই key (OrderId) কে একই partition এ পাঠান; অথবা consumer ক্রম-নিরপেক্ষ ডিজাইন করুন (version/timestamp দেখে পুরনো বার্তা বাদ)।
৫. Outbox Pattern: ডেটাবেস ও বার্তার সামঞ্জস্য
সমস্যা: অর্ডার DB তে সংরক্ষণ হলো কিন্তু ঠিক তখন অ্যাপ ক্র্যাশ করল, বার্তা পাঠানো হলো না। বা উল্টো: বার্তা গেল কিন্তু DB তে সেভ ব্যর্থ। কারণ DB ও broker আলাদা সিস্টেম, এক transaction এ আনা যায় না (Dual Write সমস্যা)।
সমাধান: Transactional Outbox।
- অর্ডার সংরক্ষণের একই transaction এ
Outboxtable এ বার্তাও লিখুন। - আলাদা একটি background process Outbox পড়ে broker এ পাঠায়, সফল হলে "পাঠানো হয়েছে" চিহ্নিত করে।
- At-least-once ফলে consumer idempotent রাখুন।
using var tx = await db.Database.BeginTransactionAsync(ct);
db.Orders.Add(order);
db.OutboxMessages.Add(new OutboxMessage
{
Id = Guid.NewGuid(),
Type = nameof(OrderPlaced),
Payload = JsonSerializer.Serialize(new OrderPlaced(order.Id, email)),
CreatedAt = DateTime.UtcNow
});
await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct); // অর্ডার ও বার্তা: হয় দুটোই, নয় কোনোটাই
MassTransit এ built-in Outbox সমর্থন আছে। Inbox Pattern হলো consumer-পাশের অনুরূপ (ডুপ্লিকেট প্রতিরোধ)।
৬.৯ Saga Pattern: Distributed Transaction সামলানো
Monolith এ একটা DB transaction সব ঠিক রাখে। Microservices এ Order, Payment, Inventory আলাদা DB তে। একটা ধাপ ফেল করলে আগের ধাপগুলো কীভাবে ফেরত নেবেন? Two-Phase Commit (2PC) ধীর ও scalable নয়। সমাধান: Saga, একাধিক local transaction এর ধারা, ব্যর্থ হলে Compensating Action (উল্টো ক্ষতিপূরণ কাজ)।
ShopEasy অর্ডার Saga:
- Order তৈরি (Pending)
- Payment নেওয়া
- Inventory সংরক্ষণ
- Shipping শুরু → Order Confirmed
যদি ধাপ ৩ ব্যর্থ (স্টক শেষ):
- Payment ফেরত (Refund) ← compensating action
- Order বাতিল (Cancelled)
দুই ধরন:
- Choreography: কেন্দ্রীয় নিয়ন্ত্রক নেই; প্রতিটি সার্ভিস event শোনে ও পরের event ছাড়ে। সরল, কিন্তু ধাপ বাড়লে প্রবাহ বোঝা কঠিন।
- Orchestration: একটা কেন্দ্রীয় "Saga orchestrator" সবাইকে নির্দেশ দেয় (MassTransit State Machine, Temporal, Durable Functions)। প্রবাহ স্পষ্ট, কিন্তু orchestrator একটি নির্ভরতার কেন্দ্র।
মনে রাখুন: compensation সবসময় নিখুঁত "undo" নয়। ইমেইল পাঠানো ফেরানো যায় না; একটা "ভুলের জন্য দুঃখিত" ইমেইলই ক্ষতিপূরণ। এভাবে চিন্তা করতে হবে।
৬.১০ Background Job ও Scheduled Task
সব কাজ বার্তার প্রয়োজন নেই; কিছু কাজ নির্ধারিত সময়ে বা পেছনে চলে।
.NET এর built-in BackgroundService:
public class StaleCartCleaner(IServiceScopeFactory scopeFactory, ILogger<StaleCartCleaner> log)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
using var scope = scopeFactory.CreateScope(); // Scoped service এর জন্য
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Carts.Where(c => c.UpdatedAt < DateTime.UtcNow.AddDays(-7))
.ExecuteDeleteAsync(stoppingToken);
}
catch (Exception ex) { log.LogError(ex, "Cleanup failed"); } // loop যেন না মরে
}
}
}
builder.Services.AddHostedService<StaleCartCleaner>();
সতর্কতা: অ্যাপের একাধিক instance চললে প্রতিটি এই কাজ চালাবে! একবার চালাতে চাইলে distributed lock বা Hangfire/Quartz.NET এর মতো লাইব্রেরি (যা persistent job store ও cluster-aware) ব্যবহার করুন।
| টুল | কখন |
|---|---|
BackgroundService |
সহজ, একক-instance বা idempotent কাজ |
| Hangfire | persistent job, retry, ড্যাশবোর্ড, fire-and-forget/delayed/recurring |
| Quartz.NET | জটিল schedule (cron) |
| Azure Functions Timer | serverless schedule |
Channel (System.Threading.Channels): একই process এর ভেতরে producer-consumer queue (বাইরের broker ছাড়া হালকা)। তবে process ক্র্যাশ করলে বার্তা হারায়; গুরুত্বপূর্ণ কাজে durable broker নিন।
৬.১১ ShopEasy তে প্রয়োগ
রাফি এখন:
- হোমপেজের জনপ্রিয় পণ্য: HybridCache (TTL ৫ মিনিট)
- পণ্য ছবি: CDN
- অর্ডারের পর ইমেইল/SMS/ইনভয়েস: RabbitMQ + MassTransit + Outbox, consumer গুলো idempotent
- প্রতি রাতের রিপোর্ট: Hangfire
- Order → Payment → Inventory: Saga (orchestration)
ফল: ঈদের সেলে হোমপেজ ২০০ ms, অর্ডার ১ সেকেন্ডে গৃহীত, ইমেইল সার্ভার ডাউন থাকলেও কোনো অর্ডার ব্যর্থ হয় না।
Part 6 এর সারসংক্ষেপ
- Cache যোগ করার আগে মাপুন; Cache-Aside দিয়ে শুরু; HybridCache আধুনিক পছন্দ।
- Invalidation সবচেয়ে কঠিন; TTL + stampede প্রতিরোধ মাথায় রাখুন।
- ধীর ও অ-জরুরি কাজ queue তে; broker ও ডেটাবেসের সামঞ্জস্যে Outbox।
- At-least-once + Idempotent Consumer + Retry + DLQ হলো messaging এর চার স্তম্ভ।
- Distributed transaction এর জন্য Saga; compensation ভাবুন।
Part 7: Distributed System ও Resilience: ব্যর্থতা মেনেই বাঁচা
মাইক্রোসার্ভিসে ShopEasy তে একটা অর্ডার এখন ৫টা সার্ভিস ডাকে। একটা সার্ভিস ধীর হলো (Payment gateway ৩০ সেকেন্ড নিচ্ছে)। ফল: Order সার্ভিসের সব thread আটকে গেল, তারপর API Gateway, তারপর পুরো সাইট ডাউন। একটা ছোট সমস্যা ঝর্ণার মতো সবখানে ছড়াল। এটাকে বলে Cascading Failure। এই Part এ শিখব কীভাবে এমন না হয়।
মূলমন্ত্র: "Everything fails, all the time" (ওয়ার্নার ভোগেলস, Amazon এর CTO)। ব্যর্থতা এড়ানো যায় না, তাই ব্যর্থতা মেনে নিয়ে নকশা করতে হয়।
৭.১ Timeout: সবচেয়ে মৌলিক সুরক্ষা
ডিফল্ট HttpClient timeout ১০০ সেকেন্ড। এটা প্রায় সবসময় অনেক বেশি। কোনো নেটওয়ার্ক call এ timeout না থাকলে একটা ধীর নির্ভরতা আপনার সব thread খেয়ে ফেলতে পারে।
builder.Services.AddHttpClient<IPaymentClient, PaymentClient>(c =>
{
c.BaseAddress = new Uri("https://payment.internal");
c.Timeout = TimeSpan.FromSeconds(3); // সীমা ঠিক করুন
});
নিয়ম: প্রতিটি বাইরের call (HTTP, DB, cache, queue) এ timeout দিন, এবং ভেতরের timeout বাইরের চেয়ে ছোট রাখুন। (ক্লায়েন্ট ৫s অপেক্ষা করে, কিন্তু সার্ভার ১০s ধরে কাজ করছে, এটা অপচয়।)
Deadline propagation: সম্পূর্ণ request এর একটা মোট সময়সীমা ধরে CancellationToken সবখানে পাস করুন। ক্লায়েন্ট চলে গেলে বা সময় শেষ হলে পুরো শৃঙ্খলে কাজ বন্ধ।
৭.২ Retry: আবার চেষ্টা, কিন্তু বুদ্ধি করে
অনেক ব্যর্থতা ক্ষণস্থায়ী (transient): নেটওয়ার্ক ঝলক, ৫০৩, deadlock। আবার চেষ্টায় কাজ হয়।
কিন্তু অন্ধ retry বিপজ্জনক:
- Retry Storm: সার্ভার ধুঁকছে; ১০০০ ক্লায়েন্ট একসাথে retry করে তাকে শেষ করে দিল।
- অ-idempotent কাজ (POST payment) retry করলে ডুপ্লিকেট (Part 5 এর Idempotency Key)।
ভালো retry এর উপাদান:
- সীমিত সংখ্যক (২-৪ বার)
- Exponential Backoff: ১s → ২s → ৪s (মাঝের ফাঁক বাড়ানো)
- Jitter: র্যান্ডম সময় যোগ, যাতে সবাই একসাথে না আসে
- শুধু retry-যোগ্য ব্যর্থতায় (৫০৩, timeout, ৪২৯); ৪০০/৪০৪ এ নয়
৭.৩ Circuit Breaker: ফিউজ
বাড়ির ফিউজের মতো: সমস্যা ধরা পড়লে সংযোগ কেটে দেয়, যাতে বড় বিপদ না হয়।
তিন অবস্থা:
- Closed (স্বাভাবিক): request যায়। ব্যর্থতা গোনা হয়।
- Open (কাটা): ব্যর্থতা সীমা ছাড়ালে বন্ধ। request তৎক্ষণাৎ ব্যর্থ (অপেক্ষা ছাড়াই), নির্ভরতাকে সুস্থ হওয়ার সময় দেয়।
- Half-Open (পরীক্ষা): কিছু সময় পর অল্প কয়েকটা request ছেড়ে দেখা। সফল হলে Closed, নইলে আবার Open।
ফল: Payment gateway ডাউন থাকলে প্রতিটি অর্ডার ৩০s আটকে না থেকে সাথে সাথে ব্যর্থ হয় (বা বিকল্প পথে যায়), আমাদের সার্ভার সুস্থ থাকে।
৭.৪ Polly ও Microsoft.Extensions.Resilience
.NET এ resilience এর জনপ্রিয় লাইব্রেরি Polly। .NET 8+ এ এটি Microsoft.Extensions.Http.Resilience এ সহজে HttpClient এর সাথে মেলানো যায়।
builder.Services.AddHttpClient<IPaymentClient, PaymentClient>(c =>
c.BaseAddress = new Uri("https://payment.internal"))
.AddStandardResilienceHandler(); // ডিফল্ট: rate limiter, total timeout, retry, circuit breaker, attempt timeout
নিজের মতো কাস্টমাইজ:
.AddResilienceHandler("payment", pipeline =>
{
pipeline.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
Delay = TimeSpan.FromMilliseconds(200)
});
pipeline.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
MinimumThroughput = 10,
SamplingDuration = TimeSpan.FromSeconds(30),
BreakDuration = TimeSpan.FromSeconds(15)
});
pipeline.AddTimeout(TimeSpan.FromSeconds(3));
});
(Polly v8 এর Resilience Pipeline API এখানে দেখানো। পুরনো Policy.Handle<...>() শৈলী v7 এর।)
৭.৫ Bulkhead, Fallback ও Graceful Degradation
Bulkhead (জাহাজের বদ্ধ কুঠুরি): একটা কুঠুরিতে পানি ঢুকলেও বাকি জাহাজ ভাসে। কোডে: প্রতিটি নির্ভরতার জন্য আলাদা resource পুল/concurrency সীমা, যাতে একটা ধীর নির্ভরতা সব thread না খায়। (ConcurrencyLimiter, আলাদা HttpClient, আলাদা queue)
Fallback: ব্যর্থ হলে বিকল্প উত্তর।
Graceful Degradation: সব ফিচার না পারলেও মূল কাজ চালু রাখা।
ShopEasy তে "Recommendation" সার্ভিস ডাউন হলে:
- ❌ পুরো পণ্য পেজ ৫০০ ফেরত দিল
- ✅ "আপনার জন্য সুপারিশ" অংশ লুকিয়ে বা জনপ্রিয় পণ্যের cached তালিকা দেখিয়ে বাকি পেজ স্বাভাবিক
পেমেন্ট ডাউন থাকলে অর্ডার "Pending Payment" রেখে পরে সম্পন্ন করার সুযোগ দেওয়া যেতে পারে।
৭.৬ Load Balancing ও Service Discovery
মাইক্রোসার্ভিসে সার্ভিসের instance সংখ্যা ও IP প্রতিনিয়ত বদলায় (scale up/down, deploy)। কোডে hard-coded IP চলে না।
-
Service Discovery: সার্ভিস নিজেকে নিবন্ধন করে; অন্যরা নাম দিয়ে খুঁজে নেয় (Consul, Kubernetes DNS, Eureka)। Kubernetes এ
http://payment-serviceলিখলেই DNS ঠিক instance এ পাঠায়। - Client-side বনাম Server-side load balancing: ক্লায়েন্ট নিজেই তালিকা থেকে বেছে নেয়, বা মাঝে একটা load balancer থাকে।
- Service Mesh (Istio, Linkerd): retry, timeout, mTLS, tracing প্রতিটি সার্ভিসের পাশে sidecar proxy দিয়ে কোডের বাইরে সামলায়। বড় সিস্টেমে কার্যকর, তবে জটিলতা বাড়ে।
YARP দিয়ে Gateway (.NET):
{
"ReverseProxy": {
"Routes": {
"orders": { "ClusterId": "orders", "Match": { "Path": "/api/orders/{**rest}" } }
},
"Clusters": {
"orders": {
"LoadBalancingPolicy": "RoundRobin",
"HealthCheck": { "Active": { "Enabled": true, "Path": "/health" } },
"Destinations": {
"d1": { "Address": "http://orders-1:8080" },
"d2": { "Address": "http://orders-2:8080" }
}
}
}
}
}
৭.৭ Health Check
builder.Services.AddHealthChecks()
.AddSqlServer(connString, name: "db", tags: ["ready"])
.AddRedis(redisConn, name: "redis", tags: ["ready"]);
// Liveness: প্রক্রিয়াটা বেঁচে আছে? (ফেল করলে restart)
app.MapHealthChecks("/health/live", new() { Predicate = _ => false });
// Readiness: ট্রাফিক নেওয়ার জন্য প্রস্তুত? (ফেল করলে load balancer ট্রাফিক বন্ধ রাখে)
app.MapHealthChecks("/health/ready", new() { Predicate = c => c.Tags.Contains("ready") });
Liveness আর Readiness আলাদা রাখুন। DB ডাউন হলে সব pod কে restart করা (liveness ফেল) সমাধান নয়; শুধু ট্রাফিক বন্ধ (readiness) যথেষ্ট।
৭.৮ Distributed System এর আরও মূল ধারণা
Eventual Consistency এর সাথে বাঁচা
মাইক্রোসার্ভিস ও replica তে ডেটা কিছুক্ষণ অসঙ্গত থাকতে পারে। UI/UX ও ব্যবসায়িক নিয়মে এটা মাথায় রাখুন:
- অর্ডার দেওয়ার পর পেজে "আপনার অর্ডার প্রক্রিয়াধীন" দেখান (লিস্টে আসতে সামান্য দেরি হতে পারে)
- Read-your-writes এর জন্য লেখার পর কিছুক্ষণ primary থেকে পড়ুন
Leader Election ও Consensus (ধারণা)
কয়েকটি সার্ভারের মধ্যে একজনকে "নেতা" বাছতে বা একটা মানে সবার একমত হতে Consensus অ্যালগরিদম (Raft, Paxos) লাগে। আপনি সাধারণত নিজে লিখবেন না; Redis Sentinel, etcd, ZooKeeper, বা ক্লাউড-ম্যানেজড সার্ভিস ব্যবহার করবেন। শুধু জানুন: কেন একাধিক node এর অর্ধেকের বেশির সম্মতি (Quorum) লাগে (যাতে network partition এ দুটো দল আলাদা সিদ্ধান্ত না নেয়, "split-brain")।
Distributed Lock
একাধিক server এ একই কাজ একবারই চালাতে (যেমন রাতের রিপোর্ট)। Redis (RedLock), DB row lock বা কোনো coordinator। সতর্কতা: লক মেয়াদ ও ক্র্যাশ সামলান (লকধারী মরে গেলে যেন লক চিরকাল না থাকে), আর সম্ভব হলে লকের বদলে idempotent ডিজাইনকে প্রাধান্য দিন।
ঘড়ির সমস্যা
দুটো সার্ভারের ঘড়ি হুবহু এক নয়। "কে আগে লিখেছে" নির্ণয়ে সার্ভারের wall-clock এর ওপর ভরসা করবেন না। Logical clock, version number, বা DB এর ক্রম ব্যবহার করুন। সময় সংরক্ষণে সবসময় UTC।
Fan-out ও Tail Latency
একটা request ১০টা সার্ভিস ডাকলে মোট ধীরতা নির্ভর করে সবচেয়ে ধীরটির ওপর। প্রতিটির p99 ১% ধীর হলেও, ১০টা ডাকলে প্রায় ১০% request ধীর হবে। তাই: সমান্তরালে ডাকুন (Task.WhenAll), ডাকার সংখ্যা কমান, hedged request (ধীর হলে দ্বিতীয় প্রতিলিপিতেও পাঠান), cache।
var productTask = productClient.GetAsync(id, ct);
var stockTask = inventoryClient.GetAsync(id, ct);
var reviewTask = reviewClient.GetAsync(id, ct);
await Task.WhenAll(productTask, stockTask, reviewTask); // একটার পর একটা নয়, একসাথে
৭.৯ Chaos Engineering ও পরীক্ষা
ব্যর্থতা সামলানোর কোড যদি কখনো পরীক্ষা না হয়, প্রকৃত বিপদে কাজ করবে না। Netflix এর Chaos Monkey এর ধারণা: ইচ্ছাকৃতভাবে সার্ভার বন্ধ করে দেখুন সিস্টেম টেকে কি না। শুরুতে ছোট: staging এ একটা নির্ভরতা বন্ধ করে দেখুন কী ঘটে। .NET এ Polly এর Chaos strategies দিয়ে কৃত্রিম ব্যর্থতা/বিলম্ব ঢোকানো যায়।
৭.১০ Resilience চেকলিস্ট
- [ ] প্রতিটি বাইরের call এ timeout আছে?
- [ ] Retry তে backoff ও jitter, এবং শুধু idempotent/retry-যোগ্য কাজে?
- [ ] Circuit breaker আছে?
- [ ] Bulkhead/concurrency সীমা দিয়ে একটা নির্ভরতাকে আলাদা রাখা হয়েছে?
- [ ] Fallback/graceful degradation ভাবা হয়েছে?
- [ ] Health check (liveness ও readiness আলাদা)?
- [ ] সব queue consumer idempotent?
- [ ] ব্যর্থতার পরিস্থিতি পরীক্ষা করা হয়েছে?
Part 7 এর সারসংক্ষেপ
- Cascading failure ঠেকাতে Timeout, Retry (backoff+jitter), Circuit Breaker, Bulkhead, Fallback একসাথে ব্যবহার করুন।
- .NET এ
AddStandardResilienceHandlerদিয়ে দ্রুত শুরু। - Service Discovery ও Health Check মাইক্রোসার্ভিসের ভিত্তি।
- ঘড়ি, নেটওয়ার্ক, ক্রম কিছুই নিখুঁত নয়; সেই অনুযায়ী ডিজাইন করুন।
Part 8: Security: নিরাপত্তা শুরু থেকেই
শেষে যোগ করা নিরাপত্তা সবসময় ফাঁকা থাকে। ShopEasy তে একদিন দেখা গেল, ইউজার URL এ /orders/5 কে /orders/6 করে অন্যের অর্ডার দেখতে পারছে। ভয়াবহ। এই Part এ নিরাপত্তার মৌলিক ও বাস্তব দিক।
৮.১ মৌলিক ধারণা
- Authentication (AuthN): আপনি কে? (পরিচয় যাচাই: login)
- Authorization (AuthZ): আপনি কী করতে পারবেন? (অনুমতি)
উপমা: অফিসে ঢুকতে আইডি কার্ড দেখানো (AuthN), আর কোন ফ্লোরে যেতে পারবেন তা কার্ডের অনুমতি (AuthZ)।
CIA Triad: Confidentiality (গোপনীয়তা), Integrity (অখণ্ডতা), Availability (প্রাপ্যতা)।
মূলনীতি:
- Least Privilege: শুধু যতটুকু দরকার ততটুকু অনুমতি
- Defense in Depth: একাধিক স্তরে নিরাপত্তা, একটা ভাঙলে অন্যটা বাঁচায়
- Zero Trust: ভেতরের নেটওয়ার্ক বলেও কাউকে অন্ধ বিশ্বাস নয়; প্রতিটি request যাচাই
- Secure by Default
৮.২ Password ও Credential
কখনো password সরল টেক্সটে রাখবেন না; এমনকি সাধারণ hash (MD5/SHA-256) ও যথেষ্ট নয়।
ব্যবহার করুন ধীর, salt-যুক্ত অ্যালগরিদম: bcrypt, scrypt, Argon2, PBKDF2। ASP.NET Core Identity এর PasswordHasher এটা সামলায়।
Salt হলো প্রতিটি password এর সাথে র্যান্ডম মান, যাতে একই password এর hash আলাদা হয় ও rainbow table অকার্যকর।
আরও: Multi-Factor Authentication (MFA), পাসওয়ার্ড লিক-তালিকার সাথে মিলিয়ে দেখা, ব্যর্থ লগইনে lockout/rate limit।
৮.৩ Session, Cookie ও Token
Cookie-ভিত্তিক Session: সার্ভার session মনে রাখে, ব্রাউজারে ID পাঠায়। ব্রাউজার-অ্যাপে সহজ ও নিরাপদ (সঠিক ফ্ল্যাগে)। কিন্তু একাধিক সার্ভারে session শেয়ার করতে লাগে (Redis)।
Token-ভিত্তিক (JWT): সার্ভার কিছু মনে রাখে না (stateless); token নিজেই তথ্য বহন করে।
JWT (JSON Web Token) এর গঠন: header.payload.signature
header = { "alg": "RS256", "typ": "JWT" }
payload = { "sub": "42", "name": "Rafi", "role": "admin", "exp": 1790000000 }
signature= সাইন করা (গোপন কী দিয়ে), যাতে কেউ বদলালে ধরা পড়ে
গুরুত্বপূর্ণ: JWT এর payload এনক্রিপ্ট করা নয়, শুধু সাইন করা; যে কেউ পড়তে পারে। তাই গোপন কিছু রাখবেন না।
JWT এর ঝুঁকি ও সর্বোত্তম অভ্যাস:
- Access token ছোট মেয়াদী (৫-১৫ মিনিট); দীর্ঘ মেয়াদে Refresh token (নিরাপদ জায়গায়, rotation সহ)
- একবার ইস্যু করা token সহজে বাতিল করা যায় না; তাই ছোট মেয়াদ ও দরকারে denylist/revocation
- ব্রাউজারে
localStorageএ token রাখলে XSS এ চুরির ঝুঁকি;HttpOnly, Secure, SameSitecookie নিরাপদ বিকল্প - অ্যালগরিদম ও
iss,aud,expসবসময় যাচাই করুন
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
o.Authority = "https://auth.shopeasy.com";
o.Audience = "shopeasy-api";
o.TokenValidationParameters = new()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(30)
};
});
builder.Services.AddAuthorization();
app.UseAuthentication();
app.UseAuthorization(); // এই ক্রম গুরুত্বপূর্ণ
৮.৪ OAuth 2.0 ও OpenID Connect (OIDC)
আপনি "Google দিয়ে Login" ক্লিক করেন। ShopEasy আপনার Google password কখনো দেখে না। এটাই OAuth/OIDC।
- OAuth 2.0: Authorization এর ফ্রেমওয়ার্ক। "এই অ্যাপকে আমার ডেটায় সীমিত অ্যাক্সেস দিন।"
- OpenID Connect: OAuth এর ওপর Authentication স্তর (ইউজার কে, তা জানায়)।
Authorization Code Flow with PKCE (ব্রাউজার/মোবাইল ও আজকের সেরা অনুশীলন):
- ব্যবহারকারীকে Identity Provider (IdP) এ পাঠানো হয়, সেখানে login
- IdP একটা code ফেরত পাঠায়
- অ্যাপ সেই code বিনিময়ে access token (+ ID token, refresh token) নেয় (PKCE এই বিনিময় চুরি থেকে রক্ষা করে)
- অ্যাপ access token দিয়ে API ডাকে
Client Credentials Flow: সার্ভার-থেকে-সার্ভার (কোনো ইউজার নেই)।
নিজের Identity Server না বানানো ভালো। ম্যানেজড সার্ভিস (Microsoft Entra ID, Auth0, Keycloak, AWS Cognito) বা মানসম্মত লাইব্রেরি ব্যবহার করুন। ASP.NET Core Identity নিজস্ব ইউজার ডেটাবেসের জন্য, .NET 8+ এ Identity API endpoints আছে। (Duende IdentityServer শক্তিশালী কিন্তু বাণিজ্যিক লাইসেন্স লাগতে পারে।)
৮.৫ Authorization: Role, Claim, Policy
// Role-based
[Authorize(Roles = "Admin")]
// Policy-based (নমনীয়, প্রস্তাবিত)
builder.Services.AddAuthorizationBuilder()
.AddPolicy("CanRefund", p => p.RequireClaim("permission", "orders.refund"))
.AddPolicy("Adult", p => p.RequireAssertion(ctx => /* বয়স ≥ 18 */ true));
app.MapPost("/orders/{id}/refund", Refund).RequireAuthorization("CanRefund");
সবচেয়ে সাধারণ ও বিপজ্জনক ভুল: BOLA/IDOR (Broken Object Level Authorization)। মানে "এই ইউজার এই নির্দিষ্ট রেকর্ডের মালিক কি না" যাচাই না করা। ShopEasy তে /orders/5 সমস্যা ছিল এটাই।
// ❌ শুধু login যথেষ্ট ধরা হয়েছে
app.MapGet("/orders/{id}", async (int id, AppDbContext db) =>
await db.Orders.FindAsync(id));
// ✅ মালিকানা যাচাই
app.MapGet("/orders/{id}", async (int id, ClaimsPrincipal user, AppDbContext db) =>
{
var userId = int.Parse(user.FindFirstValue(ClaimTypes.NameIdentifier)!);
var order = await db.Orders.FirstOrDefaultAsync(o => o.Id == id && o.CustomerId == userId);
return order is null ? Results.NotFound() : Results.Ok(order); // অন্যের হলে 404, অস্তিত্বও ফাঁস নয়
}).RequireAuthorization();
সাধারণত Controller এ নয়, query তেই মালিকানা ফিল্টার রাখুন। বড় সিস্টেমে resource-based authorization (IAuthorizationService) ব্যবহার।
৮.৬ OWASP Top 10: বাস্তবে যা যা ঘটে
OWASP হলো ওয়েব নিরাপত্তার সবচেয়ে সুপরিচিত তালিকা (সংস্করণ ও ক্রম হালনাগাদ হয়; বর্তমান তালিকা OWASP এর সাইটে দেখুন)। মূল ঝুঁকিগুলো:
| ঝুঁকি | কী | প্রতিরোধ |
|---|---|---|
| Broken Access Control | অনুমতি ছাড়া অন্যের ডেটা/কাজ | প্রতিটি request এ সার্ভারে মালিকানা/role যাচাই; default deny |
| Injection (SQL) | ইনপুটে ঢোকানো SQL কোড | Parameterized query; কখনো string জুড়ে SQL নয় |
| Cryptographic Failures | দুর্বল/অনুপস্থিত এনক্রিপশন | HTTPS সর্বত্র; আধুনিক অ্যালগরিদম; সংবেদনশীল ডেটা এনক্রিপ্ট |
| Insecure Design | নকশাতেই ত্রুটি | Threat modeling |
| Security Misconfiguration | ডিফল্ট পাসওয়ার্ড, খোলা debug | হার্ডেনিং, ন্যূনতম এক্সপোজার |
| Vulnerable Components | পুরনো/ঝুঁকিপূর্ণ NuGet প্যাকেজ | নিয়মিত আপডেট, dotnet list package --vulnerable, Dependabot |
| Authentication Failures | দুর্বল login | MFA, rate limit, lockout |
| Integrity Failures | সাইন-না-করা আপডেট/ডেটা | সাইন যাচাই, CI/CD সুরক্ষা |
| Logging Failures | আক্রমণ ধরা পড়ে না | নিরাপত্তা ঘটনা লগ ও alert |
| SSRF | সার্ভারকে দিয়ে ভেতরের URL ডাকানো | বাইরে থেকে পাওয়া URL এর allow-list |
SQL Injection উদাহরণ:
// ❌ ভয়ংকর: ইনপুট "x'; DROP TABLE Users;--"
var sql = $"SELECT * FROM Users WHERE Name = '{name}'";
// ✅ EF Core LINQ (স্বয়ংক্রিয় parameter)
await db.Users.Where(u => u.Name == name).ToListAsync();
// ✅ Raw SQL হলেও parameterized
await db.Users.FromSqlInterpolated($"SELECT * FROM Users WHERE Name = {name}").ToListAsync();
// (FromSqlRaw এ string interpolation কখনো নয়)
XSS (Cross-Site Scripting): আক্রমণকারী ওয়েবপেজে স্ক্রিপ্ট ঢোকায়। প্রতিরোধ: আউটপুট এনকোড (Razor/Blazor স্বয়ংক্রিয় করে; Html.Raw এ সতর্ক), Content-Security-Policy header, ইনপুট যাচাই।
CSRF (Cross-Site Request Forgery): ভুক্তভোগীর login অবস্থা ব্যবহার করে অন্য সাইট থেকে জাল request। প্রতিরোধ: SameSite cookie, Anti-forgery token ([ValidateAntiForgeryToken]; cookie-ভিত্তিক সাইটে জরুরি)। Bearer-token API তে সাধারণত CSRF সমস্যা কম।
Mass Assignment: DTO ব্যবহার করুন (Part 5)।
৮.৭ Secret ও Configuration ব্যবস্থাপনা
Connection string, API key, password কখনো Git এ কমিট করবেন না। (একবার কমিট হলে ইতিহাসে থেকে যায়; ধরে নিন ফাঁস হয়েছে, সাথে সাথে বদলান।)
-
ডেভেলপমেন্ট:
dotnet user-secrets - প্রোডাকশন: Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, Kubernetes Secrets (এনক্রিপশন সক্রিয় করে)
- Managed Identity: অ্যাপ নিজেই ক্লাউডের পরিচয়ে Key Vault/DB তে ঢোকে, কোনো password কোডে বা কনফিগে নেই। সবচেয়ে নিরাপদ।
- Secret নিয়মিত rotate করুন; লগে কখনো secret প্রিন্ট নয়।
builder.Configuration.AddAzureKeyVault(
new Uri("https://shopeasy-kv.vault.azure.net/"),
new DefaultAzureCredential()); // Managed Identity/dev credential
৮.৮ ডেটা সুরক্ষা ও গোপনীয়তা
-
Encryption in Transit: সর্বত্র HTTPS/TLS (
app.UseHttpsRedirection(), HSTS)। সার্ভিস-থেকে-সার্ভিস mTLS - Encryption at Rest: DB/ডিস্ক এনক্রিপশন; সংবেদনশীল কলাম (NID, কার্ড) আলাদা এনক্রিপ্ট
- কার্ডের তথ্য নিজে রাখবেন না; PCI-DSS জটিল। Payment gateway এর tokenization ব্যবহার করুন।
- PII (ব্যক্তিগত তথ্য): শুধু প্রয়োজনীয়টুকু সংগ্রহ (Data Minimization), লগে মাস্ক, আইন (যেমন GDPR বা স্থানীয় ডেটা সুরক্ষা আইন) অনুযায়ী মুছে ফেলা/রপ্তানির সুযোগ রাখুন
- ASP.NET Core Data Protection API: cookie/token এনক্রিপশনের কী একাধিক সার্ভারে শেয়ার করুন (নইলে এক সার্ভারের cookie অন্যটি পড়তে পারে না)
৮.৯ API ও অবকাঠামো সুরক্ষা
-
CORS: শুধু বিশ্বস্ত origin অনুমোদন (
AllowAnyOrigin+ credentials এক সাথে নয়) -
Security Headers:
Strict-Transport-Security,Content-Security-Policy,X-Content-Type-Options: nosniff,X-Frame-Options - Rate Limiting ও WAF (Web Application Firewall: Cloudflare, Azure Front Door) বট ও সাধারণ আক্রমণ ঠেকায়
- ইনপুট সীমা: request body এর আকার সীমা, ফাইল আপলোডে type/আকার/ভাইরাস স্ক্যান, সংরক্ষণ ওয়েব-রুটের বাইরে
- Container নিরাপত্তা: non-root ইউজার, ছোট base image, image স্ক্যান
- নেটওয়ার্ক: DB ইন্টারনেটে খোলা নয়; private network/VNet
-
Dependency নিরাপত্তা:
dotnet list package --vulnerable, Dependabot/Renovate, SBOM - Audit Log: কে কখন কী করল (বিশেষত অ্যাডমিন কাজ)
৮.১০ Threat Modeling: STRIDE
ডিজাইনের সময়ই জিজ্ঞেস করুন "কী ভুল হতে পারে?" STRIDE:
- Spoofing (ভুয়া পরিচয়)
- Tampering (ডেটা বদল)
- Repudiation ("আমি করিনি" অস্বীকার; audit log সমাধান)
- Information Disclosure (তথ্য ফাঁস)
- Denial of Service (সেবা বন্ধ)
- Elevation of Privilege (অনুমতি বাড়ানো)
প্রতিটি data flow ও trust boundary তে এই ছয়টি প্রশ্ন করুন।
৮.১১ নিরাপত্তা চেকলিস্ট
- [ ] সর্বত্র HTTPS, HSTS
- [ ] Password Argon2/bcrypt/PBKDF2 দিয়ে হ্যাশ; MFA সমর্থন
- [ ] প্রতিটি endpoint এ authentication ও resource-level authorization (BOLA)
- [ ] সব ইনপুট যাচাই; DTO ব্যবহার; parameterized query
- [ ] Secret কোডে নেই; Key Vault/Managed Identity
- [ ] CORS, security header, rate limiting
- [ ] Dependency নিয়মিত আপডেট ও স্ক্যান
- [ ] সংবেদনশীল ডেটা এনক্রিপ্ট; লগে PII/secret নেই
- [ ] Audit log ও নিরাপত্তা alert
- [ ] Error বার্তায় ভেতরের তথ্য (stack trace) ফাঁস হয় না
Part 8 এর সারসংক্ষেপ
- AuthN ≠ AuthZ; সবচেয়ে বড় ঝুঁকি অনুমতি যাচাই ভুলে যাওয়া (BOLA)।
- নিজে crypto বা identity server বানাবেন না; প্রমাণিত সমাধান ব্যবহার করুন।
- Secret কোডের বাইরে, Managed Identity সবচেয়ে ভালো।
- Defense in Depth ও Least Privilege; ডিজাইনের সময় threat model।
Part 9: Observability, DevOps ও Cloud
রাত ২টায় ফোন: "সাইট ধীর!" আপনি জানেন না কোথায় সমস্যা: কোন সার্ভিস, কোন query, কোন সার্ভার। যা মাপা যায় না, তা ঠিকও করা যায় না। এই Part এ শিখব সিস্টেমকে "দেখার" উপায় ও নিরাপদে deploy করার পদ্ধতি।
৯.১ Observability এর তিন স্তম্ভ
| স্তম্ভ | কী বলে | উদাহরণ |
|---|---|---|
| Logs | কী ঘটেছে (ঘটনার বিবরণ) | "Order 5 payment failed: timeout" |
| Metrics | কতটা/কত ঘন (সংখ্যা, সময়ের সাথে) | request/sec, error rate, p99 latency, CPU |
| Traces | একটা request পুরো সিস্টেমে কোথায় কত সময় নিল | Gateway 5ms → Order 20ms → Payment 3000ms |
Monitoring জানে "কী ভাঙছে" (পূর্বনির্ধারিত প্রশ্ন); Observability আপনাকে নতুন, অপ্রত্যাশিত প্রশ্ন করতে দেয় ("কেন শুধু বুধবারে মোবাইল ইউজাররা ধীর?")।
৯.২ Logging: Serilog ও Structured Logging
Unstructured log (খারাপ): "User 42 placed order 5 for 1200": খুঁজতে regex লাগে।
Structured log (ভালো): ফিল্ডসহ, query করা যায়।
// ❌ string interpolation: structured হয় না
log.LogInformation($"Order {id} placed by {userId}");
// ✅ message template: id ও userId আলাদা searchable ফিল্ড
log.LogInformation("Order {OrderId} placed by {UserId} total {Total}", id, userId, total);
Serilog সেটআপ:
builder.Host.UseSerilog((ctx, cfg) => cfg
.ReadFrom.Configuration(ctx.Configuration)
.Enrich.FromLogContext()
.Enrich.WithProperty("Service", "ordering")
.WriteTo.Console(new CompactJsonFormatter())
.WriteTo.Seq("http://seq:5341")); // বা Elasticsearch, Loki, Application Insights
app.UseSerilogRequestLogging();
Log Level: Trace < Debug < Information < Warning < Error < Critical। Production এ সাধারণত Information+।
লগের নিয়ম:
- Correlation ID/Trace ID প্রতিটি লগে, যাতে এক request এর সব লগ জোড়া যায়
- Password, token, কার্ড, PII লগ নয়
- Exception এ stack trace সহ
log.LogError(ex, "...") - অতিরিক্ত লগ খরচ ও গোলমাল বাড়ায়; অর্থবহ ঘটনা লগ করুন
- কেন্দ্রীয় লগ সংগ্রহ (Seq, ELK/Elastic, Grafana Loki, Azure Monitor); প্রতিটি সার্ভারে ঢুকে ফাইল পড়া নয়
৯.৩ Metrics ও চার সোনালি সংকেত
Google SRE এর "Four Golden Signals":
- Latency: কত সময় লাগছে (p50, p95, p99)
- Traffic: কত request
- Errors: কত শতাংশ ব্যর্থ
- Saturation: সম্পদ কতটা ভরা (CPU, মেমরি, DB connection pool, queue গভীরতা)
আরও পরিচিত কাঠামো: RED (Rate, Errors, Duration) সার্ভিসের জন্য; USE (Utilization, Saturation, Errors) সম্পদের জন্য।
// কাস্টম business metric (System.Diagnostics.Metrics)
public class OrderMetrics
{
private readonly Counter<int> _placed;
public OrderMetrics(IMeterFactory factory)
{
var meter = factory.Create("ShopEasy.Orders");
_placed = meter.CreateCounter<int>("orders.placed");
}
public void OrderPlaced(string paymentMethod) =>
_placed.Add(1, new KeyValuePair<string, object?>("payment", paymentMethod));
}
Business metric ও গুরুত্বপূর্ণ: শুধু CPU নয়, "প্রতি মিনিটে কত অর্ডার" দেখুন। অর্ডার হঠাৎ শূন্যে নামলে সার্ভার সুস্থ থাকলেও সমস্যা আছে।
৯.৪ Distributed Tracing ও OpenTelemetry
মাইক্রোসার্ভিসে একটা অর্ডার ৫টা সার্ভিস ঘোরে। ধীর কোথায়? Trace এক request এর পুরো যাত্রার ম্যাপ। প্রতিটি ধাপ একটা span, সবাই একই Trace ID শেয়ার করে (HTTP header traceparent দিয়ে এক সার্ভিস থেকে পরেরটায় পৌঁছায়, W3C Trace Context)।
OpenTelemetry (OTel) হলো এই বিষয়ে মান ও ভেন্ডর-নিরপেক্ষ সমাধান; .NET এ এটাই প্রচলিত পথ।
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("ordering"))
.WithTracing(t => t
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddSource("ShopEasy.Orders")
.AddOtlpExporter()) // Jaeger, Tempo, Azure Monitor, Datadog ইত্যাদিতে
.WithMetrics(m => m
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter("ShopEasy.Orders")
.AddOtlpExporter());
নিজের span:
private static readonly ActivitySource Source = new("ShopEasy.Orders");
using var activity = Source.StartActivity("CalculateDiscount");
activity?.SetTag("order.id", orderId);
জনপ্রিয় টুল: Prometheus + Grafana (metrics), Jaeger/Tempo (trace), Loki/Seq/ELK (log), Azure Application Insights, Datadog, New Relic। .NET Aspire dashboard স্থানীয় ডেভেলপমেন্টে এগুলো একসাথে দেখায়।
৯.৫ Alerting ও Incident Response
- Alert হবে লক্ষণে (symptom), কারণে নয়: "error rate > ৫%" বা "p99 > ২ সেকেন্ড" (ব্যবহারকারী ক্ষতিগ্রস্ত)। "CPU ৮০%" একা কোনো সমস্যা নয়।
- Alert fatigue এড়ান: বেশি alert মানে কেউ আর পাত্তা দেয় না। শুধু যেগুলোতে তৎক্ষণাৎ মানুষের পদক্ষেপ লাগে, সেগুলো pager এ।
- SLO ও Error Budget: SLO ৯৯.৯% মানে মাসে ~৪৩ মিনিট ব্যর্থতা "বাজেট"। বাজেট ফুরালে নতুন ফিচার থামিয়ে স্থিতিশীলতায় মন দিন।
- Runbook: প্রতিটি alert এর সাথে "কী করবেন" লিখিত।
- Blameless Postmortem: ঘটনার পর দোষারোপ নয়, "কী শিখলাম ও কী বদলাব"।
৯.৬ Docker: যেখানে চলবে সেই পরিবেশসহ প্যাকেজ
"আমার মেশিনে চলে!" সমস্যার সমাধান। Container অ্যাপ, তার runtime ও dependency একসাথে প্যাকেজ করে; যেকোনো জায়গায় একইভাবে চলে। ভার্চুয়াল মেশিনের চেয়ে হালকা (OS kernel শেয়ার করে)।
Multi-stage Dockerfile (.NET):
# ধাপ ১: build (SDK দিয়ে, বড় image)
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore # আলাদা ধাপে, যাতে layer cache কাজে লাগে
COPY . .
RUN dotnet publish -c Release -o /app --no-restore
# ধাপ ২: final (শুধু runtime, ছোট ও নিরাপদ)
FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY --from=build /app .
USER $APP_UID # non-root
EXPOSE 8080
ENTRYPOINT ["dotnet", "ShopEasy.Api.dll"]
Docker Compose দিয়ে স্থানীয়ভাবে পুরো সিস্টেম (API + SQL + Redis + RabbitMQ) একসাথে:
services:
api:
build: .
ports: ["8080:8080"]
environment:
ConnectionStrings__Default: "Server=db;Database=shop;User=sa;Password=${DB_PASSWORD};TrustServerCertificate=true"
depends_on: [db, redis]
db:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: ${DB_PASSWORD}
redis:
image: redis:7
নিয়ম: Container stateless ও disposable (যেকোনো মুহূর্তে মরতে/নতুন হতে পারে); কনফিগ environment variable এ; লগ stdout এ; secret image এর ভেতরে নয়।
৯.৭ Kubernetes (K8s): Container এর ম্যানেজার
শত শত container কে কে চালাবে, ফেল করলে কে আবার তুলবে, লোড বাড়লে কে বাড়াবে? Kubernetes।
মূল ধারণা:
- Pod: একটা বা একাধিক container এর ক্ষুদ্রতম একক
- Deployment: কয়টা pod চলবে ও কীভাবে আপডেট হবে
- Service: pod গুলোর সামনে স্থির নাম/IP ও load balancing
- Ingress: বাইরের ট্রাফিকের প্রবেশপথ
- ConfigMap/Secret: কনফিগ ও গোপন তথ্য
- HPA (Horizontal Pod Autoscaler): CPU/মেট্রিক দেখে pod বাড়ায়-কমায়
- Namespace: পরিবেশ/টিম আলাদা করা
apiVersion: apps/v1
kind: Deployment
metadata: { name: ordering }
spec:
replicas: 3
strategy:
type: RollingUpdate # একটা একটা করে pod বদলায়, downtime নেই
selector: { matchLabels: { app: ordering } }
template:
metadata: { labels: { app: ordering } }
spec:
containers:
- name: api
image: shopeasy/ordering:1.4.2
ports: [{ containerPort: 8080 }]
resources:
requests: { cpu: "250m", memory: "256Mi" }
limits: { cpu: "500m", memory: "512Mi" }
livenessProbe: { httpGet: { path: /health/live, port: 8080 } }
readinessProbe: { httpGet: { path: /health/ready, port: 8080 } }
সতর্কতা: Kubernetes শক্তিশালী কিন্তু জটিল, শেখা ও পরিচালনার খরচ বেশি। ছোট টিমের জন্য ম্যানেজড বিকল্প (Azure App Service, Azure Container Apps, AWS ECS/Fargate, Google Cloud Run) প্রায়ই যথেষ্ট ও সহজ। প্রয়োজন প্রমাণিত না হলে K8s এড়ান।
৯.৮ CI/CD: স্বয়ংক্রিয় build, test ও deploy
- CI (Continuous Integration): প্রতিটি কোড পরিবর্তনে স্বয়ংক্রিয়ভাবে build ও test; সমস্যা তাড়াতাড়ি ধরা
- CD (Continuous Delivery/Deployment): পাস করা কোড স্বয়ংক্রিয়ভাবে (বা এক ক্লিকে) প্রোডাকশনে
একটা আদর্শ pipeline:
- Code commit/PR
- Build + restore
- Unit test + coverage
- স্ট্যাটিক বিশ্লেষণ ও dependency/নিরাপত্তা স্ক্যান
- Docker image build ও registry তে push
- Staging এ deploy + integration/smoke test
- অনুমোদন (দরকারে) → Production deploy
- Health check যাচাই; ব্যর্থ হলে স্বয়ংক্রিয় rollback
GitHub Actions উদাহরণ:
name: ci
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with: { dotnet-version: "9.0.x" }
- run: dotnet restore
- run: dotnet build --no-restore -c Release
- run: dotnet test --no-build -c Release
অন্যান্য: Azure DevOps Pipelines, GitLab CI, Jenkins। Infrastructure as Code (IaC): অবকাঠামো কোডে (Terraform, Bicep, Pulumi), ক্লিক করে নয়; ভার্সন নিয়ন্ত্রিত ও পুনরুৎপাদনযোগ্য।
৯.৯ নিরাপদ Deployment কৌশল
| কৌশল | কীভাবে | সুবিধা/অসুবিধা |
|---|---|---|
| Rolling | একটা একটা instance বদলানো | সহজ; পুরনো ও নতুন ভার্সন কিছুক্ষণ একসাথে (সামঞ্জস্য লাগে) |
| Blue-Green | দুটো পূর্ণ পরিবেশ; ট্রাফিক একবারে সুইচ | তাৎক্ষণিক rollback; খরচ দ্বিগুণ |
| Canary | প্রথমে ৫% ট্রাফিক নতুন ভার্সনে, ঠিক থাকলে বাড়ানো | ঝুঁকি কম; জটিল |
| Feature Flag | কোড deploy করে ফিচার বন্ধ রাখা, পরে চালু | deploy ও release আলাদা; ঝামেলা হলে সুইচ অফ |
Database পরিবর্তন সবসময় backward-compatible রাখুন (Part 4 এর Expand-Contract), নইলে rolling deploy এ ভাঙবে।
৯.১০ Cloud ধারণা
সেবার স্তর:
- IaaS: ভার্চুয়াল মেশিন (আপনি OS থেকে সামলান)। যেমন Azure VM, EC2
- PaaS: প্ল্যাটফর্ম (আপনি শুধু কোড দেন)। যেমন Azure App Service
- FaaS/Serverless: ফাংশন। যেমন Azure Functions, Lambda
- SaaS: তৈরি সফটওয়্যার
Azure বনাম AWS (মূল সেবার সমতুল্যতা, .NET প্রসঙ্গে):
| কাজ | Azure | AWS |
|---|---|---|
| ওয়েব অ্যাপ হোস্ট | App Service | Elastic Beanstalk / App Runner |
| Container | Container Apps / AKS | ECS-Fargate / EKS |
| Serverless | Functions | Lambda |
| SQL DB | Azure SQL | RDS |
| NoSQL | Cosmos DB | DynamoDB |
| Cache | Azure Cache for Redis | ElastiCache |
| Queue/Bus | Service Bus | SQS / SNS |
| ফাইল | Blob Storage | S3 |
| Secret | Key Vault | Secrets Manager |
| Monitoring | Application Insights | CloudWatch |
(সেবার নাম ও ফিচার বদলায়; সিদ্ধান্তের আগে সংশ্লিষ্ট ক্লাউডের হালনাগাদ ডকুমেন্ট দেখুন।)
Cloud এর গুরুত্বপূর্ণ ধারণা:
- Region ও Availability Zone (AZ): Region ভৌগোলিক এলাকা, AZ সেই এলাকার ভেতরের আলাদা ডেটাসেন্টার। একাধিক AZ তে চালালে একটা ডেটাসেন্টার গেলেও টিকে থাকে। একাধিক Region তে Disaster Recovery
- Auto-scaling: লোড অনুযায়ী স্বয়ংক্রিয় বাড়া-কমা
- Cost Management: ক্লাউড খরচ নীরবে বাড়ে। বাজেট alert, অপ্রয়োজনীয় রিসোর্স বন্ধ, Reserved/Spot instance বিবেচনা, ডেটা ট্রান্সফার ফি লক্ষ্য রাখা
- Well-Architected Framework: Azure/AWS দুটোরই ডিজাইন নির্দেশিকা (নির্ভরযোগ্যতা, নিরাপত্তা, খরচ, পারফরম্যান্স, অপারেশনাল শ্রেষ্ঠত্ব)
- Multi-tenancy: একই অ্যাপে একাধিক গ্রাহককে (SaaS) সেবা; ডেটা বিচ্ছিন্নতার মডেল (shared DB with TenantId, schema per tenant, DB per tenant)
৯.১১ .NET Aspire
.NET Aspire হলো ক্লাউড-নেটিভ, বিতরিত অ্যাপ বানানোর একটা অ-মতবাদী (opinionated) stack। স্থানীয়ভাবে একাধিক সার্ভিস, DB, Redis, RabbitMQ একসাথে চালানো, service discovery, কনফিগ, OpenTelemetry ও ড্যাশবোর্ড প্রায় স্বয়ংক্রিয়ভাবে মেলে।
// AppHost প্রজেক্ট: সিস্টেমের গঠন কোডে
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddRedis("cache");
var db = builder.AddSqlServer("sql").AddDatabase("shopdb");
var orders = builder.AddProject<Projects.Ordering_Api>("ordering")
.WithReference(db)
.WithReference(redis);
builder.AddProject<Projects.Web>("web")
.WithReference(orders); // service discovery স্বয়ংক্রিয়
builder.Build().Run();
মাইক্রোসার্ভিস বা একাধিক নির্ভরতার প্রজেক্টে ডেভেলপার অভিজ্ঞতা চমৎকার করে। (Aspire দ্রুত বিবর্তিত হচ্ছে; ব্যবহারের আগে সর্বশেষ ডকুমেন্ট দেখুন।)
৯.১২ Configuration ও 12-Factor App
আধুনিক ক্লাউড অ্যাপের নীতিমালা (সবগুলো না মানলেও মূলগুলো জরুরি):
- একটাই কোডবেস, একাধিক deploy
- Dependency স্পষ্ট ঘোষণা (
.csproj) - কনফিগ পরিবেশে (environment), কোডে নয়
- Backing service (DB, cache) কে সংযুক্ত রিসোর্স হিসেবে দেখুন
- Build/Release/Run আলাদা
- Process stateless
- দ্রুত চালু ও সুন্দরভাবে বন্ধ (graceful shutdown:
IHostApplicationLifetime, চলমান request শেষ করে বন্ধ) - লগ event stream হিসেবে stdout এ
- Dev/Staging/Prod যতটা সম্ভব সদৃশ
// অগ্রাধিকার ক্রমে (পরেরটা আগেরটাকে ওভাররাইড করে)
// appsettings.json → appsettings.{Environment}.json → user-secrets(dev) → Environment variables → Command line
var conn = builder.Configuration.GetConnectionString("Default");
// Options pattern: টাইপ-সেফ কনফিগ
builder.Services.AddOptions<PaymentOptions>()
.BindConfiguration("Payment")
.ValidateDataAnnotations()
.ValidateOnStart(); // ভুল কনফিগে অ্যাপ চালুই হবে না, রাতে নয়, এখনই ধরা
Part 9 এর সারসংক্ষেপ
- Logs + Metrics + Traces: তিনটিই দরকার; OpenTelemetry মানসম্মত পথ।
- Structured logging, correlation ID; alert হবে লক্ষণে, সংখ্যায় কম।
- Docker multi-stage, Compose স্থানীয় পরিবেশে; Kubernetes প্রয়োজন হলে, নইলে ম্যানেজড PaaS।
- CI/CD ও IaC; নিরাপদ deploy (rolling, blue-green, canary, feature flag)।
- Cloud এ Multi-AZ, auto-scaling, খরচ নজরদারি।
Part 10: Performance, Capacity Planning ও .NET Internals
"সাইট ধীর" শুনলে বেশিরভাগ ডেভেলপার অনুমানে কোড বদলাতে শুরু করে। পেশাদার আগে মাপে। Donald Knuth এর কথা: "অকাল optimization সব অনিষ্টের মূল।" এই Part এ শিখব কীভাবে মেপে, সঠিক জায়গায়, সঠিক উপায়ে দ্রুত করবেন।
১০.১ Performance এর সোনালি নিয়ম
- আগে মাপুন, তারপর বদলান। Profiler ছাড়া অনুমান প্রায়ই ভুল।
- Bottleneck খুঁজুন। সিস্টেমের গতি সবচেয়ে ধীর অংশ ঠিক করে। ১% অংশ ১০ গুণ দ্রুত করলে লাভ নগণ্য।
- বেশিরভাগ সময় বাধা I/O (DB, নেটওয়ার্ক), CPU নয়।
- সবচেয়ে সস্তা জয় আগে: index, N+1 ঠিক, cache, compression।
- পরিবর্তনের পরে আবার মাপুন, সত্যিই উন্নতি হয়েছে কি না।
পারফরম্যান্স সমস্যা খোঁজার ক্রম: ইউজার-অভিজ্ঞতা (ব্রাউজার, ছবি, JS) → নেটওয়ার্ক → API/অ্যাপ → ডেটাবেস।
১০.২ async/await: .NET এর সবচেয়ে বড় scalability হাতিয়ার (এবং ফাঁদ)
উপমা: ওয়েটার (thread) এক টেবিলের রান্নার জন্য রান্নাঘরের সামনে দাঁড়িয়ে অপেক্ষা করলে ১০ টেবিল সামলাতে ১০ জন ওয়েটার লাগে। async মানে ওয়েটার অর্ডার দিয়ে অন্য টেবিলে চলে যায়, খাবার তৈরি হলে ফিরে আসে। কম thread এ বেশি request।
// ❌ Thread আটকে রাখে (Thread Pool Starvation এর কারণ)
public IActionResult Get()
{
var data = _db.Orders.ToList(); // sync I/O
var x = _http.GetStringAsync(url).Result; // .Result: ডেডলক/আটকে থাকার ঝুঁকি
return Ok(data);
}
// ✅ Thread মুক্ত রাখে
public async Task<IActionResult> Get(CancellationToken ct)
{
var data = await _db.Orders.AsNoTracking().ToListAsync(ct);
var x = await _http.GetStringAsync(url, ct);
return Ok(data);
}
নিয়ম:
- I/O (DB, HTTP, ফাইল) এ সবসময় async, শেষ পর্যন্ত ("async all the way")
-
.Result/.Wait()/.GetAwaiter().GetResult()এড়ান: Thread Pool Starvation (সব thread আটকে, সাইট জমে যায়) -
async voidশুধু event handler এ; অন্যত্রasync Task - CPU-বাউন্ড ভারী কাজে async এর লাভ নেই (
Task.RunASP.NET এ সাধারণত অপ্রয়োজনীয়) - স্বাধীন কাজ সমান্তরালে:
await Task.WhenAll(a, b, c) -
ValueTaskশুধু প্রমাণিত হট পাথে (প্রায়ই সুবিধা তুচ্ছ) - লাইব্রেরি কোডে
ConfigureAwait(false)(ASP.NET Core অ্যাপ কোডে দরকার নেই)
১০.৩ Memory ও Garbage Collector (GC)
.NET স্বয়ংক্রিয় memory পরিচালনা করে, কিন্তু অনেক অকারণ allocation GC কে ব্যস্ত রাখে এবং request ধীর করে (GC pause)।
মূল ধারণা:
- Stack: ছোট, দ্রুত; value type, local variable
- Heap: বড় অবজেক্ট, GC পরিচালিত
- Generation 0/1/2: নতুন অবজেক্ট Gen0 এ (সস্তা সংগ্রহ); বেঁচে থাকলে উঁচু প্রজন্মে। Gen2 সংগ্রহ ব্যয়বহুল
- LOH (Large Object Heap): ~৮৫,০০০ বাইটের বেশি অবজেক্ট; ফ্র্যাগমেন্টেশন ও Gen2 চাপ বাড়ায়
ব্যবহারিক কৌশল:
// ❌ লুপে string জোড়া: প্রতিবার নতুন string
string s = "";
foreach (var i in items) s += i.Name + ",";
// ✅ StringBuilder
var sb = new StringBuilder();
foreach (var i in items) sb.Append(i.Name).Append(',');
// ✅ বড় array বারবার না বানিয়ে পুল
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try { /* ব্যবহার */ } finally { ArrayPool<byte>.Shared.Return(buffer); }
// ✅ Span<T> দিয়ে কপি ছাড়া slicing
ReadOnlySpan<char> part = text.AsSpan(0, 5);
-
HttpClientপ্রতি request এnewনয়;IHttpClientFactory(socket exhaustion ও DNS সমস্যা এড়ায়) - বড় ফাইল/response stream করুন, পুরোটা মেমরিতে নয়
-
IDisposable(DB connection, stream) ঠিকমতোusingদিয়ে ছাড়ুন - Memory Leak এর সাধারণ কারণ: static collection এ জমা, event handler unsubscribe না করা, বিশাল cache এ সীমা নেই
- সংগ্রহ ও লুপে LINQ এর অতিরিক্ত অ্যালোকেশন (হট পাথে সচেতন থাকুন, সাধারণ কোডে পাঠযোগ্যতাই আগে)
১০.৪ মাপার টুল
| টুল | কাজ |
|---|---|
| BenchmarkDotNet | micro-benchmark: কোডের একটা অংশের গতি ও মেমরি নির্ভরযোগ্যভাবে মাপা |
| dotnet-counters / dotnet-trace / dotnet-dump | চলমান অ্যাপের মেট্রিক, trace, মেমরি dump |
| Visual Studio / JetBrains dotTrace, dotMemory | profiler |
| Application Insights / OpenTelemetry | production এ ধীর endpoint ও dependency |
| SQL Profiler / Query Store / EXPLAIN | ধীর query |
| k6, JMeter, NBomber, Locust | load test |
Load Testing এর ধরন:
- Load test: প্রত্যাশিত ট্রাফিকে ঠিক চলে কি না
- Stress test: সীমা ছাড়িয়ে কোথায় ভাঙে
- Spike test: হঠাৎ ট্রাফিক-ঢেউ (ঈদের সেল)
- Soak test: দীর্ঘ সময় (লিক, ক্লান্তি ধরা পড়ে)
প্রোডাকশন-সদৃশ পরিবেশে ও বাস্তবসম্মত ডেটায় পরীক্ষা করুন; খালি ডেটাবেসে ফল প্রতারণাপূর্ণ।
১০.৫ Capacity Planning: Back-of-the-Envelope হিসাব
System design interview ও বাস্তব পরিকল্পনা, দুটোতেই কাজে লাগে। উদ্দেশ্য নিখুঁত সংখ্যা নয়, মাত্রা (order of magnitude) বোঝা।
কাজের সংখ্যা:
- ১ দিন ≈ ৮৬,৪০০ সেকেন্ড ≈ ~১ লক্ষ সেকেন্ড (হিসাব সহজ করতে)
- ১ কোটি request/দিন ÷ ১ লক্ষ ≈ ১০০ request/সেকেন্ড (গড়)
- পিক সাধারণত গড়ের ২-১০ গুণ → ধরুন ৫ গুণ = ৫০০ RPS
ShopEasy হিসাব (ধরে নেওয়া সংখ্যা):
- Daily Active Users (DAU): ১০ লক্ষ
- প্রতি ইউজার দিনে ৫০টি request → ৫ কোটি request/দিন
- গড় RPS = ৫ কোটি ÷ ১ লক্ষ ≈ ৫০০; পিক ৫ গুণ ≈ ২,৫০০ RPS
- যদি একটা সার্ভার ~৫০০ RPS সামলায় (মেপে দেখে), তাহলে পিকে ~৫টা + নিরাপদ মার্জিন/redundancy = ৮-১০টা সার্ভার
Storage হিসাব:
- দিনে ৫০,০০০ অর্ডার × ২ KB = ~১০০ MB/দিন → বছরে ~৩৬ GB। ছোট!
- পণ্য ছবি: ১০ লক্ষ পণ্য × ৫ ছবি × ২০০ KB = ~১ TB। ফাইল স্টোরেজ/CDN এ, DB তে নয়
Read:Write অনুপাত: ই-কমার্স সাধারণত read-heavy (১০০:১)। তাই cache ও read replica কার্যকর।
Bandwidth: ২,৫০০ RPS × ৫০ KB response = ~১২৫ MB/s ≈ ১ Gbps। CDN ও compression তখন জরুরি।
Latency Budget: মোট লক্ষ্য ৩০০ ms হলে ভাগ করুন: নেটওয়ার্ক ৫০, Gateway ১০, API ৭০, DB ১০০, বাকি মার্জিন। কোনো অংশ বাজেট ছাড়ালে বোঝা যায় কোথায় কাজ।
কয়েকটি স্মরণীয় সংখ্যা (আনুমানিক):
- একটা সাধারণ ওয়েব সার্ভার/অ্যাপ instance: শত থেকে কয়েক হাজার RPS (কাজের ওপর নির্ভর)
- একটা ভালো SQL সার্ভার: হাজার থেকে দশ হাজার সাধারণ query/সেকেন্ড
- Redis: প্রতি সেকেন্ডে লক্ষ operation
- Kafka: প্রতি সেকেন্ডে লক্ষ লক্ষ বার্তা (cluster এ)
(এগুলো মোটামুটি ধারণা; আপনার কাজের ধরনে নিজে মেপে নিন।)
১০.৬ Scaling এর ধাপ: ShopEasy এর যাত্রার সারাংশ
| ধাপ | ইউজার (আনুমানিক) | কী করবেন |
|---|---|---|
| ১ | ~১০০-১,০০০ | একটাই সার্ভার, একটাই DB। সরল ও পর্যাপ্ত। |
| ২ | ~১০,০০০ | Index, query ঠিক; DB আলাদা সার্ভারে; CDN |
| ৩ | ~১ লক্ষ | একাধিক app server + Load Balancer (stateless); Redis cache; read replica |
| ৪ | ~১০ লক্ষ | Message queue, background worker; auto-scaling; DB পারটিশনিং; search ইঞ্জিন |
| ৫ | ১০ লক্ষ+ | Microservices (যদি টিম/ডোমেইন দাবি করে); sharding; multi-region |
সংখ্যাগুলো কেবল দিকনির্দেশ। বাস্তবে ঠিক হয় ট্রাফিক ধরন ও কাজের জটিলতা দিয়ে। কোনো সংখ্যায় লাফ দেওয়ার আগে মাপুন।
Scalability নীতি:
- আগে Stateless বানান
- Database ই সাধারণত প্রথম বাধা; সেখান থেকে শুরু
- পড়া ও লেখা আলাদা করে ভাবুন
- ভারী ও ধীর কাজ অ্যাসিঙ্ক
- সম্ভব হলে কাজ ভাগ করুন (partition), যাতে সমান্তরালে চলে
- স্বয়ংক্রিয় scaling, কিন্তু সীমা ঠিক রাখুন (খরচ ও downstream নির্ভরতা বাঁচাতে)
১০.৭ ফ্রন্টএন্ড-সংলগ্ন ও নেটওয়ার্ক পারফরম্যান্স
-
Compression:
app.UseResponseCompression()(Brotli/Gzip), JSON আকার কমে - HTTP/2, HTTP/3: একই সংযোগে একাধিক request, Kestrel সমর্থন করে
- Static asset: CDN, দীর্ঘ cache + ফাইলনামে hash (cache busting), ছবি অপ্টিমাইজ (WebP/AVIF), lazy loading
-
Payload ছোট করা: শুধু দরকারি ফিল্ড; pagination;
System.Text.Json(Newtonsoft এর চেয়ে সাধারণত দ্রুত); source generator -
Connection Pooling: DB connection পুল (ADO.NET ডিফল্টে আছে); পুল ফুরালে request আটকে যায়।
Max Pool Sizeও দীর্ঘ-চলা transaction এর দিকে নজর - Keep-Alive ও HttpClient পুনর্ব্যবহার
১০.৮ সাধারণ .NET পারফরম্যান্স ফাঁদ (সংক্ষিপ্ত তালিকা)
| ফাঁদ | সমাধান |
|---|---|
| N+1 query |
Include/projection/AsSplitQuery
|
Select * ও পুরো entity |
projection |
| Pagination নেই | keyset/offset + সীমা |
.Result/.Wait()
|
async all the way |
new HttpClient() প্রতিবার |
IHttpClientFactory |
| বড় object cache এ সীমাহীন | সাইজ সীমা, মেয়াদ |
| লুপের ভেতরে DB/HTTP call | batch করুন |
| অতিরিক্ত লগ | লেভেল ঠিক রাখুন |
| Exception দিয়ে স্বাভাবিক flow |
TryParse, ফলাফল-টাইপ |
| Regex অকার্যকর (ReDoS) |
RegexOptions.Compiled/source generator, timeout |
| Reflection/dynamic হট পাথে | source generator |
Part 10 এর সারসংক্ষেপ
- আগে মাপুন (profiler, load test, trace), তারপর optimize।
- I/O তে async all the way;
.Resultনয়। - অকারণ allocation কমান;
IHttpClientFactory, stream, পুল। - Capacity planning এ order of magnitude; DAU → RPS → সার্ভার/স্টোরেজ।
- Scale করুন ধাপে ধাপে; প্রয়োজনের আগে নয়।
Part 11: Case Study: তিনটি সম্পূর্ণ System Design
তত্ত্ব শিখলেন; এখন প্রয়োগ। প্রতিটি case study তে একই কাঠামো অনুসরণ করব (Part 12 এর ফ্রেমওয়ার্ক): প্রয়োজন → হিসাব → উচ্চস্তরের নকশা → ডেটা মডেল → গভীরে সিদ্ধান্ত → ট্রেড-অফ।
Case Study ১: URL Shortener (যেমন bit.ly)
সমস্যা: লম্বা URL কে ছোট করে (sho.rt/aB3xK9), ছোটটায় ঢুকলে মূলটায় redirect।
ধাপ ১: প্রয়োজন
Functional:
- লম্বা URL দিলে ছোট URL তৈরি
- ছোট URL এ ঢুকলে redirect
- (ঐচ্ছিক) কাস্টম alias, মেয়াদ, ক্লিক-অ্যানালিটিক্স
Non-functional:
- Redirect অত্যন্ত দ্রুত (< ১০০ ms) ও উচ্চ availability
- Read-heavy: প্রতি ১ তৈরিতে ~১০০ redirect
- Short code অনুমানযোগ্য নয়, ডুপ্লিকেট নয়
ধাপ ২: হিসাব (ধরে নিচ্ছি)
- নতুন URL: ১০ কোটি/মাস ≈ ~৪০ write/সেকেন্ড
- Redirect (১০০:১): ~৪,০০০ read/সেকেন্ড গড়; পিক ~১২,০০০
- ৫ বছর সংরক্ষণ: ১০ কোটি × ১২ × ৫ = ৬০০ কোটি রেকর্ড। প্রতিটি ~৫০০ বাইট → ~৩ TB। (একটা আধুনিক ক্লাস্টারে সম্ভব)
- Short code দৈর্ঘ্য: Base62 (
a-z, A-Z, 0-9) এ ৭ অক্ষর = 62^7 ≈ ৩.৫ ট্রিলিয়ন সংমিশ্রণ। ৬০০ কোটির জন্য যথেষ্ট
ধাপ ৩: উচ্চস্তরের নকশা
Client → CDN/Load Balancer → API Servers (stateless, .NET)
├── Cache (Redis): code → longUrl
└── Database (code → longUrl)
└── (async) Queue → Analytics Worker → Analytics DB
ধাপ ৪: API
app.MapPost("/api/urls", async (CreateUrlRequest req, IUrlService svc, CancellationToken ct) =>
{
var code = await svc.CreateAsync(req.LongUrl, req.CustomAlias, ct);
return Results.Created($"/{code}", new { shortUrl = $"https://sho.rt/{code}" });
});
app.MapGet("/{code}", async (string code, IUrlService svc, CancellationToken ct) =>
{
var url = await svc.ResolveAsync(code, ct);
return url is null ? Results.NotFound() : Results.Redirect(url, permanent: false);
});
301 বনাম 302: ৩০১ (স্থায়ী) ব্রাউজার cache করে, ফলে সার্ভারে hit কমে কিন্তু ক্লিক গণনা হারায়। অ্যানালিটিক্স চাইলে ৩০২।
ধাপ ৫: Short code তৈরির কৌশল (মূল সিদ্ধান্ত)
| পদ্ধতি | কীভাবে | সমস্যা |
|---|---|---|
| Hash (MD5/SHA) এর প্রথম ৭ অক্ষর | URL এর hash | সংঘর্ষ (collision) সম্ভব; পরীক্ষা ও retry লাগে |
| Random Base62 | র্যান্ডম ৭ অক্ষর | সংঘর্ষ সম্ভব (unique constraint + retry) |
| Counter + Base62 | একটা বাড়তে থাকা সংখ্যাকে Base62 এ রূপান্তর | সংঘর্ষ নেই; কিন্তু অনুমানযোগ্য, আর কাউন্টার distributed এ জটিল |
| Pre-generated Key Service (KGS) | আগে থেকে অনেক unique key তৈরি করে রাখা; চাইলে একটা দেওয়া | সরল ও দ্রুত; KGS এর availability নিশ্চিত করতে হবে |
সিদ্ধান্ত: Counter-based ID (Snowflake-জাতীয় বা ডেটাবেস sequence ব্লক বরাদ্দ করে প্রতি সার্ভারে) + Base62 এনকোড। অনুমানযোগ্যতা সমস্যা হলে সংখ্যাকে অদলবদল (permutation/obfuscation) করুন। প্রতিটি API সার্ভার একসাথে ১০০০ ID এর ব্লক নিয়ে স্থানীয়ভাবে বিতরণ করে, প্রতিবার DB তে না গিয়ে।
public static class Base62
{
private const string Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static string Encode(long n)
{
if (n == 0) return "0";
var sb = new StringBuilder();
while (n > 0) { sb.Insert(0, Chars[(int)(n % 62)]); n /= 62; }
return sb.ToString();
}
}
ধাপ ৬: ডেটা মডেল ও ডেটাবেস
Urls(Code PK, LongUrl, UserId, CreatedAt, ExpiresAt)। সরল key-value access pattern (শুধু code দিয়ে খোঁজা), জটিল JOIN নেই। তাই SQL (PostgreSQL) বা NoSQL (DynamoDB/Cassandra) দুটোই চলে। শুরুতে SQL; বিপুল scale এ key-value store।
ধাপ ৭: Scaling ও অপ্টিমাইজেশন
- Cache (Redis): জনপ্রিয় URL সামান্য সংখ্যক; ২০% URL এ ৮০% ট্রাফিক (Pareto)। Cache-Aside, LRU eviction, TTL। ফলে DB তে খুব কম hit
- Read replica, তারপর sharding (shard key = code এর hash)
- Analytics অ্যাসিঙ্ক: ক্লিক event queue তে ফেলে আলাদা worker; redirect এর পথ ধীর হয় না
- মেয়াদোত্তীর্ণ লিংক পরিষ্কার: background job
- Rate limit ও অপব্যবহার প্রতিরোধ: ম্যালওয়্যার/ফিশিং লিংক চেক, তৈরির rate limit
ট্রেড-অফ: Cache এ মুছে ফেলা/বদলানো লিংকের পুরনো ডেটা কিছুক্ষণ থাকতে পারে (TTL ও invalidation)। Redirect এ eventual consistency গ্রহণযোগ্য।
Case Study ২: E-Commerce Order System (ShopEasy এর কেন্দ্র)
সমস্যা: ঈদের সেলে হাজারো ইউজার একসাথে কিনছে; স্টক ঠিক রাখা, পেমেন্ট নিরাপদ, কোনো অর্ডার হারানো বা দুবার প্রক্রিয়া নয়।
ধাপ ১: প্রয়োজন
- পণ্য ব্রাউজ/সার্চ, cart, checkout, পেমেন্ট, অর্ডার ট্র্যাকিং, ইমেইল/SMS
- কঠোর সঠিকতা: স্টকের বেশি বিক্রি নয়, পেমেন্ট ডুপ্লিকেট নয়
- ঈদের সময় ১০-২০ গুণ spike; ৯৯.৯% availability (checkout এ)
ধাপ ২: Bounded Context (Part 3 এর DDD)
Catalog (পণ্য), Cart, Ordering, Inventory, Payment, Shipping, Notification, Identity।
স্থাপত্য সিদ্ধান্ত: শুরুতে Modular Monolith। চাপের অংশ (Catalog পড়া, Payment) পরে আলাদা করা যাবে।
ধাপ ৩: উচ্চস্তরের প্রবাহ
Client → CDN → API Gateway (YARP: auth, rate limit)
→ Catalog (read-heavy: Redis + Elasticsearch)
→ Cart (Redis, user-keyed)
→ Ordering → [Payment, Inventory, Shipping] (Saga)
→ Outbox → Message Broker → Notification, Analytics
ধাপ ৪: Catalog (পড়া-ভারী)
- মূল ডেটা SQL এ; সার্চ ও ফিল্টারের জন্য Elasticsearch (পণ্য ইভেন্টে সিঙ্ক, eventual consistency)
- ছবি CDN এ; পণ্য পেজ HybridCache
- দাম ও স্টক সামান্য পুরনো দেখালেও চলে, কিন্তু checkout এ আবার যাচাই
ধাপ ৫: Cart
স্থায়ী নয়, দ্রুত বদলায়, ইউজার-কেন্দ্রিক। Redis Hash (cart:{userId} → পণ্য ও পরিমাণ), মেয়াদ ৭ দিন। লগইনের আগে অতিথির cart কে (cookie) লগইনের পর মার্জ।
ধাপ ৬: Checkout ও Inventory: কঠিনতম অংশ
সমস্যা: শেষ ১০টা ফোন, ১০,০০০ জন একসাথে কিনতে চায়।
কৌশল: Reservation (সাময়িক সংরক্ষণ)।
- Checkout এ স্টক Reserve (atomic, শর্তসাপেক্ষ আপডেট), মেয়াদ ১৫ মিনিট
- পেমেন্ট সফল হলে Reservation নিশ্চিত; ব্যর্থ বা মেয়াদোত্তীর্ণ হলে স্টক ফেরত
// Atomic: যথেষ্ট স্টক থাকলে তবেই কমাও (race condition নেই)
var updated = await db.Database.ExecuteSqlInterpolatedAsync($@"
UPDATE Inventory
SET Available = Available - {qty}, Reserved = Reserved + {qty}
WHERE ProductId = {productId} AND Available >= {qty}", ct);
if (updated == 0) return Result.Fail("Out of stock");
অতি-হট পণ্যে (ফ্ল্যাশ সেল) এক row এ হাজারো লেখা hot row lock contention ঘটায়। সমাধান: স্টক কয়েকটা "বালতি" (slot) এ ভাগ করা, Redis এ atomic DECR দিয়ে দ্রুত গেট, ও পরে DB তে সামঞ্জস্য; এবং waiting room/queue দিয়ে ট্রাফিক নিয়ন্ত্রণ।
ধাপ ৭: Order Saga (Part 6)
স্বাভাবিক প্রবাহ:
OrderCreated(Pending)
→ ReserveStock ✔
→ ChargePayment ✔
→ OrderConfirmed → ShipOrder → Notify
ব্যর্থতা সামলানো:
| যেখানে ব্যর্থ | ক্ষতিপূরণ |
|---|---|
| Stock নেই | Order বাতিল (কিছু ফেরত দিতে হবে না) |
| Payment ব্যর্থ | স্টক Release, Order বাতিল |
| Payment সফল কিন্তু Shipping ব্যর্থ | পুনরায় চেষ্টা; শেষে Refund + স্টক Release |
| Payment gateway timeout (সফল না ব্যর্থ অজানা) | অবস্থা অজানা: gateway এ status query/webhook এর অপেক্ষা; স্বয়ংক্রিয়ভাবে পুনর্মিলন (reconciliation) |
Orchestration পছন্দ (MassTransit State Machine বা Durable Functions): প্রবাহ স্পষ্ট, সহজে ট্র্যাক ও debug।
ধাপ ৮: Payment নিরাপত্তা ও সঠিকতা
- কার্ডের তথ্য নিজে ধরবেন না; gateway এর hosted page/tokenization
- Idempotency Key (Part 5) প্রতিটি পেমেন্ট চেষ্টায়
- gateway থেকে Webhook এর মাধ্যমে চূড়ান্ত অবস্থা (signature যাচাই, idempotent handler)
- নিয়মিত Reconciliation job (দৈনিক gateway রিপোর্টের সাথে আমাদের রেকর্ড মেলানো)
- Money এর জন্য
decimal/Money value object, একক মুদ্রা, পূর্ণসংখ্যায় (পয়সা) সংরক্ষণ বিবেচনা
ধাপ ৯: Order এর অবস্থা (State Machine)
Pending → PaymentPending → Paid → Shipped → Delivered; বিকল্প Cancelled, Refunded। কেবল বৈধ transition অনুমোদিত (Domain model এ, Order.Confirm() ইত্যাদিতে), অবৈধ ধাপে exception।
ধাপ ১০: Spike সামলানো (ঈদের সেল)
- আগে থেকে scale out (নির্ধারিত সময়ে auto-scaling) ও cache গরম (warm-up)
- Virtual Waiting Room: ট্রাফিক সীমা ছাড়ালে ইউজারকে লাইনে রাখা, সিস্টেম বাঁচানো
- Checkout ছাড়া অনাবশ্যক ফিচার সাময়িক বন্ধ (graceful degradation)
- Rate limiting, বট প্রতিরোধ (CAPTCHA/WAF)
- Load test আগে (Spike test)
- DB connection pool ও queue গভীরতা পর্যবেক্ষণ
ট্রেড-অফ: Strong consistency (স্টক, পেমেন্ট) ধীর কিন্তু প্রয়োজনীয়; Catalog, সার্চ, রিভিউ eventual consistency তে স্বচ্ছন্দ। একই সিস্টেমে বিভিন্ন অংশে ভিন্ন consistency; এটাই পরিপক্ক design।
Case Study ৩: Notification System (ইমেইল, SMS, Push)
সমস্যা: অর্ডার, OTP, প্রমোশন: কোটি কোটি বার্তা, বিভিন্ন চ্যানেলে, নির্ভরযোগ্যভাবে, ডুপ্লিকেট ছাড়া।
প্রয়োজন
- চ্যানেল: Email, SMS, Push, In-app
- OTP: সেকেন্ডে পৌঁছানো (উচ্চ অগ্রাধিকার); প্রমোশন: ধীরে চললেও চলে
- ইউজারের পছন্দ ও opt-out সম্মান করা
- ডুপ্লিকেট নয়, বার্তা হারানো নয়
- Third-party (Twilio, SendGrid ইত্যাদি) ধীর বা ডাউন হতে পারে
নকশা
সার্ভিস (Order, Auth...) → Notification API
→ Validation + Preference check + Template
→ Outbox → Message Broker
├── high-priority queue (OTP, লেনদেন)
├── normal queue (অর্ডার আপডেট)
└── bulk queue (প্রমোশন)
→ Channel Workers (Email / SMS / Push), প্রতিটি আলাদাভাবে scalable
→ Provider Adapter (Strategy: SendGrid | SES | Twilio ...)
→ Delivery status (webhook) → Notification DB
মূল সিদ্ধান্ত
- অগ্রাধিকার-ভিত্তিক আলাদা queue: প্রমোশনের বিশাল ব্যাচ OTP আটকাবে না (Noisy Neighbor প্রতিরোধ, Bulkhead)
-
Provider abstraction (Strategy/Adapter):
IEmailProviderএর পেছনে একাধিক ভেন্ডর; একটা ডাউন হলে Fallback অন্যটিতে (Circuit Breaker সহ) - Idempotency: প্রতিটি বার্তার একটা unique key; consumer ডুপ্লিকেট প্রত্যাখ্যান করে (ইউজার একই OTP দুবার পাবে না)
- Retry + backoff + DLQ: ব্যর্থ বার্তা পুনরায়, শেষে DLQ এ ও alert
- Rate limiting (provider ও ইউজার-ভিত্তিক): provider এর সীমা মানা; এক ইউজারকে স্প্যাম নয়
- Template ও লোকালাইজেশন: বাংলা/ইংরেজি; ভেরিয়েবল প্রতিস্থাপন
- Scheduled/Delayed বার্তা: "সকাল ৯টায় পাঠান" (delayed queue / scheduler)
- ট্র্যাকিং: Sent → Delivered → Opened/Failed webhook থেকে অবস্থা হালনাগাদ
- অ্যানালিটিক্স অ্যাসিঙ্ক: মূল পথ ধীর নয়
public interface ISmsProvider
{
Task<SendResult> SendAsync(string phone, string text, CancellationToken ct);
}
public class FailoverSmsSender(IEnumerable<ISmsProvider> providers, ILogger<FailoverSmsSender> log)
{
public async Task<SendResult> SendAsync(string phone, string text, CancellationToken ct)
{
foreach (var p in providers)
{
try { return await p.SendAsync(phone, text, ct); }
catch (Exception ex) { log.LogWarning(ex, "{Provider} failed, trying next", p.GetType().Name); }
}
throw new NotificationFailedException("All providers failed");
}
}
ট্রেড-অফ: exactly-once অসম্ভব, তাই At-least-once + idempotency। অত্যন্ত জরুরি OTP তে সামান্য ডুপ্লিকেট চেয়ে হারানো খারাপ, তাই ডুপ্লিকেট সহনীয়, হারানো নয়।
কেস স্টাডি থেকে শিক্ষা (সূত্র)
- প্রয়োজন ও সংখ্যা দিয়ে শুরু (অনুমান নয়)
- Read-heavy হলে cache ও replica; Write-heavy হলে queue, partition
- ধীর, ঝুঁকিপূর্ণ বা তৃতীয়-পক্ষ নির্ভর কাজ অ্যাসিঙ্ক
- সঠিকতা-সংবেদনশীল অংশে (টাকা, স্টক) strong consistency ও idempotency
- প্রতিটি নির্ভরতার জন্য ব্যর্থতার পরিকল্পনা (timeout, retry, fallback)
- সরল দিয়ে শুরু, মাপার পর জটিল করুন
Part 12: বাস্তব জীবনের পদ্ধতি: Framework, Trade-off ও Roadmap
এতক্ষণ আপনি অনেক "হাতিয়ার" শিখলেন। এখন শেখার সময় কখন কোনটা ধরবেন এবং কীভাবে সিদ্ধান্ত নেবেন ও ব্যাখ্যা করবেন। এটাই একজন সিনিয়র/আর্কিটেক্টকে আলাদা করে।
১২.১ System Design এর ৭-ধাপ ফ্রেমওয়ার্ক
যেকোনো নতুন সিস্টেম (প্রজেক্টে বা ইন্টারভিউতে) এই ক্রমে ভাবুন:
-
প্রয়োজন স্পষ্ট করুন (Requirements)
- Functional: সিস্টেম কী করবে?
- Non-functional: কতজন ইউজার? কত দ্রুত? কতটা নির্ভরযোগ্য? নিরাপত্তা? বাজেট? সময়সীমা?
- প্রশ্ন করুন। অস্পষ্টতা নিয়ে নকশা শুরু করবেন না।
- সীমা ও অনুমান (Constraints & Assumptions): টিমের আকার ও দক্ষতা, বাজেট, বিদ্যমান সিস্টেম, আইনি বিধি।
- হিসাব (Estimation): DAU, RPS, স্টোরেজ, bandwidth (Part 10)।
- উচ্চস্তরের নকশা (High-level Design): প্রধান উপাদান (ক্লায়েন্ট, Gateway, সার্ভিস, DB, Cache, Queue) ও তাদের সংযোগ। আগে সরল নকশা।
- ডেটা মডেল ও API: মূল entity, সম্পর্ক, access pattern, প্রধান endpoint।
- গভীরে যাওয়া (Deep Dive): সবচেয়ে কঠিন/ঝুঁকিপূর্ণ অংশ (স্টক রেস, ID তৈরি, ডুপ্লিকেট প্রতিরোধ)।
- Bottleneck, ব্যর্থতা ও Trade-off: কোথায় ভাঙবে? কীভাবে scale হবে? কী ছাড়লাম, কেন?
সময় ভাগ (ইন্টারভিউয়ে ৪৫ মিনিট): প্রয়োজন ৫-৮, উচ্চস্তর ১০-১৫, গভীরে ১৫-২০, শেষে ৫।
১২.২ সিদ্ধান্ত নেওয়ার প্রশ্নমালা
যেকোনো প্রযুক্তি/কৌশল বাছাইয়ের আগে নিজেকে জিজ্ঞেস করুন:
- কোন সমস্যা সমাধান করছি? (সমস্যা প্রমাণিত, নাকি কল্পিত?)
- সরলতম বিকল্প কী? সেটা কেন যথেষ্ট নয়?
- এর দাম কী? (জটিলতা, শেখার সময়, অপারেশন, খরচ, ভেন্ডর-নির্ভরতা)
- টিম কি এটা চালাতে পারবে? রাত ২টায় কে ঠিক করবে?
- উল্টানো কত কঠিন? (Two-way door বনাম one-way door সিদ্ধান্ত। সহজে ফেরানো যায় এমনটা দ্রুত নিন, ফেরানো কঠিনটা সাবধানে)
- বিকল্প কী কী ছিল, কেন বাদ দিলাম?
১২.৩ Architecture Decision Record (ADR)
ছয় মাস পরে কেউ জিজ্ঞেস করবে: "কেন আমরা MongoDB নিলাম না?" কেউ মনে রাখবে না। তাই সিদ্ধান্ত লিখে রাখুন: ছোট একটা ফাইল (docs/adr/0007-use-postgresql.md), Git এ কোডের সাথে।
# ADR-0007: অর্ডার ডেটাবেস হিসেবে PostgreSQL
## অবস্থা
গৃহীত (২০২৬-১০-২৮)
## প্রেক্ষাপট
অর্ডার ও পেমেন্টে শক্তিশালী ACID transaction ও জটিল রিপোর্টিং লাগবে। টিম SQL এ দক্ষ।
## সিদ্ধান্ত
PostgreSQL ব্যবহার করব (ম্যানেজড সার্ভিসে)।
## বিকল্প
- MongoDB: নমনীয় schema, কিন্তু multi-document transaction ও রিপোর্টিং জটিল
- SQL Server: ভালো, কিন্তু লাইসেন্স খরচ বেশি
## পরিণতি
+ ACID, পরিপক্ক ইকোসিস্টেম, কম খরচ
− অতি-বিশাল scale এ sharding জটিল (বর্তমান প্রয়োজনে প্রযোজ্য নয়)
সিদ্ধান্ত বদলালে পুরনোটা মুছবেন না; "Superseded by ADR-0015" চিহ্নিত করুন। ইতিহাসই শিক্ষা।
১২.৪ বিখ্যাত ট্রেড-অফের সারণি (স্মরণীয়)
| ট্রেড-অফ | একদিকে | অন্যদিকে |
|---|---|---|
| Consistency বনাম Availability | সঠিক কিন্তু মাঝে মাঝে অনুপলব্ধ | সবসময় চালু কিন্তু সাময়িক পুরনো |
| Latency বনাম Throughput | প্রতিটি দ্রুত | মোট বেশি কাজ (batching) |
| Normalize বনাম Denormalize | সঙ্গত ডেটা | দ্রুত পড়া |
| Monolith বনাম Microservices | সরল | স্বাধীন scaling/টিম |
| Sync বনাম Async | সরল ও তাৎক্ষণিক উত্তর | সহনশীল ও scalable |
| Build বনাম Buy | সম্পূর্ণ নিয়ন্ত্রণ | দ্রুত ও কম রক্ষণাবেক্ষণ |
| SQL বনাম NoSQL | সম্পর্ক ও ACID | নমনীয়তা ও বিপুল scale |
| Cache | গতি | পুরনো ডেটার ঝুঁকি |
| Index | দ্রুত পড়া | ধীর লেখা ও বেশি জায়গা |
| Strong বনাম Eventual consistency | সহজ যুক্তি | scalable |
১২.৫ সাধারণ ভুল (যা সবাই করে)
- শুরুতেই Microservices: বিতরিত জটিলতা কিন্তু সুবিধা নেই (Distributed Monolith)
- Resume-Driven Development: প্রযুক্তি বাছা কারণ "CV তে ভালো দেখাবে", প্রয়োজন নয়
- অকাল অপ্টিমাইজেশন ও অতিরিক্ত নকশা (Over-engineering)
- বিপরীতে: একেবারে নকশাহীন "Big Ball of Mud"
- মাপার আগেই scaling-এর চেষ্টা
- ব্যর্থতা ভুলে যাওয়া: Timeout, retry, idempotency নেই
- Shared Database একাধিক সার্ভিসের মধ্যে (লুকানো coupling)
- Observability পরে ভাবা
- নিরাপত্তা শেষে যোগ করা
- Golden Hammer: একটা প্রিয় টুল সবখানে ("যার হাতে হাতুড়ি, সবই পেরেক")
- ডকুমেন্টেশন ও সিদ্ধান্ত না লেখা
- অপারেশনাল খরচ উপেক্ষা: কে মনিটর করবে, কে on-call?
- টিমের সক্ষমতা বিবেচনা না করা
- Cache যোগ করে ডেটা সঠিকতার কথা ভুলে যাওয়া
- Big-bang rewrite: Strangler Fig ই নিরাপদ
১২.৬ Testing কৌশল (আর্কিটেকচারের অঙ্গ)
Test Pyramid:
- Unit test (বেশি, দ্রুত): একক ক্লাস/লজিক। xUnit/NUnit + Moq/NSubstitute। Domain logic এখানে সবচেয়ে ভালো (Clean Architecture এর সুবিধা)
-
Integration test (মাঝারি): আসল DB, queue সহ। Testcontainers দিয়ে Docker এ সত্যিকারের SQL/Redis/RabbitMQ চালিয়ে। ASP.NET Core
WebApplicationFactoryদিয়ে পুরো API পরীক্ষা - Contract test (মাইক্রোসার্ভিসে): সার্ভিসগুলোর চুক্তি মানা হচ্ছে কি না (Pact)
- End-to-End (কম, ধীর): মূল ব্যবহারকারী যাত্রা
-
Architecture test:
NetArchTest/ArchUnitNETদিয়ে নিয়ম প্রয়োগ (যেমন "Domain কখনো Infrastructure ব্যবহার করবে না") - Load/Performance, Security, Chaos test
public class OrdersApiTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task GetOrder_WithoutAuth_Returns401()
{
var client = factory.CreateClient();
var res = await client.GetAsync("/api/v1/orders/1");
Assert.Equal(HttpStatusCode.Unauthorized, res.StatusCode);
}
}
টেস্ট নীতি: আচরণ পরীক্ষা করুন, বাস্তবায়ন নয়; mock সীমিত রাখুন (ডেটাবেস mock করার চেয়ে Testcontainers প্রায়ই বেশি আস্থা দেয়)।
১২.৭ টেকনিক্যাল ঋণ (Technical Debt) ও Refactoring
Technical debt: আজকের শর্টকাট, কাল সুদসহ শোধ। সবসময় খারাপ নয় (ইচ্ছাকৃত, সচেতন ঋণ মাঝে মাঝে বুদ্ধিমানের কাজ), তবে দৃশ্যমান রাখুন ও পরিশোধের পরিকল্পনা করুন।
- ঋণের তালিকা রাখুন; প্রতি স্প্রিন্টে কিছু অংশ পরিশোধ
- Boy Scout Rule: কোড যেমন পেয়েছেন তার চেয়ে একটু ভালো রেখে যান
- Refactor নিরাপদে: আগে টেস্ট, ছোট ধাপ, আচরণ অপরিবর্তিত
- Strangler Fig বড় পরিবর্তনে
১২.৮ আর্কিটেক্ট হিসেবে নরম দক্ষতা
প্রযুক্তি অর্ধেক; বাকি অর্ধেক মানুষ।
- যোগাযোগ: জটিল ধারণা সহজে বোঝান (ডায়াগ্রাম আঁকুন, উপমা দিন)। C4 Model (Context, Container, Component, Code) স্তরে স্তরে ডায়াগ্রামের ভালো পদ্ধতি
- ব্যবসা বুঝুন: প্রযুক্তি ব্যবসার সেবক। "এতে ব্যবসার কী লাভ?" বলতে পারুন
- ঐক্যমত তৈরি: আদেশ নয়, বুঝিয়ে সম্মতি; RFC/ADR প্রক্রিয়া
- শুনুন ও শিখুন: যিনি প্রতিদিন কোড লেখেন, তাঁর ব্যথা বুঝুন
- "জানি না" বলতে শিখুন, তারপর খুঁজে বের করুন
- টিমকে এগিয়ে নিন: মেন্টরিং, কোড রিভিউ
- আর্কিটেক্ট হাতে-কলমে কোডও লেখে; আইভরি টাওয়ারে নয়
১২.৯ আপনার শেখার Roadmap (Beginner → Advanced)
পর্যায় ১: ভিত্তি (১-২ মাস)
- [ ] C# ও ASP.NET Core ভালোভাবে; DI, Middleware, Configuration
- [ ] HTTP, REST, SQL ও EF Core, index
[ ] SOLID ও কয়েকটি design pattern
প্রজেক্ট: একটা Layered CRUD API (ShopEasy v1)
পর্যায় ২: মাঝারি (২-৪ মাস)
- [ ] Clean Architecture/Vertical Slice, CQRS
- [ ] Authentication (JWT/OIDC), Authorization
- [ ] Caching (Redis), Docker, CI/CD
[ ] Logging/Serilog, Unit + Integration test
প্রজেক্ট: ShopEasy v2 (Clean Architecture, Docker, CI, auth, cache)
পর্যায় ৩: উন্নত (৪-৮ মাস)
- [ ] Messaging (RabbitMQ + MassTransit), Outbox, Saga
- [ ] Resilience (Polly), OpenTelemetry, Health checks
- [ ] DDD, Modular Monolith, তারপর Microservices
[ ] Kubernetes/ক্লাউড (Azure/AWS)
প্রজেক্ট: ShopEasy v3 (Modular Monolith → একটা সার্ভিস আলাদা, Event-driven)
পর্যায় ৪: বিশেষজ্ঞ (চলমান)
- [ ] System design case study নিয়মিত অনুশীলন (URL shortener, chat, feed, ride-hailing)
- [ ] পারফরম্যান্স প্রোফাইলিং, load test
- [ ] বড় সিস্টেমের ইঞ্জিনিয়ারিং ব্লগ পড়ুন (Netflix, Uber, Stripe, Discord)
- [ ] ওপেন সোর্স কোড পড়ুন (ASP.NET Core, eShop reference)
- [ ] নিজে লিখুন ও শেখান (ব্লগ, টক): শেখানোই গভীরতম শেখা
বই (প্রস্তাবিত):
- Designing Data-Intensive Applications: Martin Kleppmann (ডেটা সিস্টেমের বাইবেল)
- Clean Architecture: Robert C. Martin
- Domain-Driven Design: Eric Evans; Implementing DDD: Vaughn Vernon
- Building Microservices: Sam Newman
- System Design Interview (Vol 1 ও 2): Alex Xu
- Release It!: Michael Nygard (resilience)
- Fundamentals of Software Architecture: Mark Richards ও Neal Ford
Reference প্রজেক্ট (পড়ে শিখুন): Microsoft এর eShop (.NET microservices/Aspire), eShopOnWeb (Clean Architecture)। (সংগ্রহশালার বর্তমান অবস্থা GitHub এ দেখে নিন।)
১২.১০ সম্পূর্ণ চেকলিস্ট: নতুন সিস্টেম লঞ্চের আগে
স্থাপত্য ও কোড
- [ ] প্রয়োজন ও non-functional লক্ষ্য লিখিত (SLO সহ)
- [ ] উপযুক্ত architecture style (সরলতম যা চলে)
- [ ] কোড স্তরবদ্ধ, DI, SOLID; ADR আছে
ডেটা
- [ ] সঠিক index; N+1 নেই; pagination আছে
- [ ] Concurrency ও transaction সঠিক
- [ ] Backup ও restore পরীক্ষিত; migration নিরাপদ
API
- [ ] ভার্সনিং, validation, DTO, Problem Details
- [ ] Idempotency (POST), rate limiting
- [ ] OpenAPI ডকুমেন্ট
Resilience
- [ ] Timeout, retry (backoff+jitter), circuit breaker
- [ ] Idempotent consumer, DLQ, Outbox
- [ ] Graceful degradation, health check
নিরাপত্তা
- [ ] AuthN/AuthZ, BOLA যাচাই, HTTPS
- [ ] Secret Key Vault এ; dependency স্ক্যান
- [ ] CORS, header, input validation
Observability
- [ ] Structured log + correlation ID
- [ ] Metric (golden signals + business) ও trace (OTel)
- [ ] Alert, runbook, dashboard
Delivery
- [ ] CI/CD, স্বয়ংক্রিয় test
- [ ] Container, IaC, নিরাপদ deploy ও rollback
- [ ] Load test; খরচ বাজেট alert
উপসংহার: "বস" হওয়ার আসল রহস্য
আপনি এই গাইডে যা শিখলেন, তার সারকথা কয়েকটি বাক্যে:
- সরল দিয়ে শুরু করুন। জটিলতা অর্জন করতে হয়, উপহার নয়।
- মাপুন, অনুমান করবেন না।
- ব্যর্থতা অনিবার্য; সেই অনুযায়ী ডিজাইন করুন।
- প্রতিটি সিদ্ধান্ত একটা ট্রেড-অফ; কী পেলেন আর কী ছাড়লেন, তা জানুন ও বলুন।
- প্রয়োজনের আগে scale করবেন না, প্রয়োজনের পরে ফেলেও রাখবেন না।
- কোড অন্যের জন্য লিখুন (ছয় মাস পরের আপনিও "অন্য")।
- প্রযুক্তি টিম ও ব্যবসার সেবা করে।
সিস্টেম ডিজাইন কোনো মুখস্থ বিদ্যা নয়; এটা চিন্তার পদ্ধতি। একজন সত্যিকারের বস প্রতিটি নতুন সমস্যায় জিজ্ঞেস করেন: "প্রয়োজন কী? সীমা কী? সরলতম সমাধান কী? কোথায় ভাঙবে? এর দাম কী?" এই পাঁচটি প্রশ্ন যে অভ্যাসে পরিণত করেছে, সে নতুন যেকোনো প্রযুক্তি এক সপ্তাহে বুঝে ফেলতে পারে।
আপনার পরবর্তী পদক্ষেপ: একটা ছোট প্রজেক্ট নিন, ShopEasy হোক বা নিজের আইডিয়া। এই গাইডের ক্রমে একটা একটা করে প্রয়োগ করুন, ভুল করুন, মাপুন, শিখুন। পড়া আর করার মধ্যে যে ফাঁক, সেটাই আপনার আসল শিক্ষক।
শুভকামনা।
এই লেখার সংখ্যা, লাইব্রেরির নাম/লাইসেন্স ও সেবার ফিচার সময়ের সাথে বদলায়; প্রয়োগের আগে সংশ্লিষ্ট অফিসিয়াল ডকুমেন্ট দেখে নিন।
Top comments (0)