ভূমিকা: কেন এই লেখা
তিন বছর আগের একটা ইনসিডেন্ট থেকে এই লেখার শুরু। প্রোডাকশন সার্ভিসের ডেটাবেজ প্রোভাইডার চেঞ্জ করতে গিয়ে দেখলাম আমার কোডবেসে ডেটা অ্যাক্সেস লজিক ছড়িয়ে আছে — কন্ট্রোলারে DbContext, সার্ভিসে raw SQL, কোথাও DbSet-এর উপর LINQ। প্রোভাইডার চেঞ্জ করতে গিয়ে প্রায় পুরো Application লেয়ার টাচ করতে হলো। সেটা ছিল আমার জন্য বড় শিক্ষা — abstraction-এর অভাবে ছোট একটা change কত বড় হয়ে যায়।
সেই দিন থেকে আমি ডিজাইন প্যাটার্নগুলো সিরিয়াসলি পড়তে শুরু করি। পরের প্রজেক্টে যখন Repository প্যাটার্ন ঠিকভাবে প্রয়োগ করলাম, তখন ঠিক উল্টো অভিজ্ঞতা হলো — মাইগ্রেশন Infrastructure লেয়ারেই সীমাবদ্ধ থাকল। সেটাই এই লেখার মূল সূত্র। এটা কোনো টিউটোরিয়াল না — প্রোডাকশন এক্সপেরিয়েন্স থেকে আমার future self-এর জন্য লেখা নোট, আপনাদেরও কাজে লাগবে।
১. Repository Pattern: Abstraction-এর লাভ এবং খরচ
প্যাটার্নটা কী করে
Repository প্যাটার্ন ডেটা অ্যাক্সেস লজিককে ডোমেইন লজিক থেকে আলাদা করে। ডোমেইন লেয়ার জানে না ডেটা কোথা থেকে আসছে — SQL Server, MySQL, Redis, নাকি in-memory। এটাই persistence ignorance।
Production-এ কী হয়েছিল
পরের প্রজেক্টে — একটা ব্যাংকিং API — Repository প্যাটার্ন ঠিকভাবে প্রয়োগ করেছিলাম। যখন MSSQL থেকে MySQL-এ মাইগ্রেট করার দরকার পড়ল, শুধু Infrastructure লেয়ার চেঞ্জ করতে হয়েছিল। Controller, Handler, Validator — কিছুই টাচ করতে হয়নি। প্রথম প্রজেক্টে যেটা প্রায় পুরো Application লেয়ার ছুঁয়ে ফেলেছিল, এখানে সেটা এক লেয়ারে আটকে গেল। এটাই Repository প্যাটার্নের সবচেয়ে বড় প্রমাণ।
কিন্তু এর খারাপ দিক
-
EF Core-এর পাওয়ার লুকিয়ে ফেলে: Repository মেথডের ভেতরে
Include(),AsNoTracking(), complex projection লুকিয়ে ফেললে পারফরম্যান্স টিউনিং কঠিন হয়ে যায়। Specific query optimization লাগলে অ্যাবস্ট্রাকশনটাই বাধা হয়ে দাঁড়ায়। - Repository Explosion: প্রতিটা এনটিটির জন্য আলাদা Repository বানালে ২০টা এনটিটি = ২০টা ইন্টারফেস + ২০টা ক্লাস। Maintain করা কষ্টকর।
- Complex query-তে rigid: মাল্টি-টেবিল join বা dynamic filter-এ Repository মেথড ফুলে যায়। তখন Query Object বা Specification pattern দরকার হয়।
আমি কী করেছি
শুধু Aggregate Root-এর জন্য Repository বানিয়েছি। বাকি read-heavy query-গুলো Dapper বা raw SQL দিয়ে সরাসরি করেছি। ফলাফল: write side-এ abstraction, read side-এ performance।
// শুধু Aggregate Root-এর জন্য, domain-এর ভাষায়
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct);
Task AddAsync(Order order, CancellationToken ct);
}
"Repository is an anti-pattern" বিতর্ক
একটা পরিচিত যুক্তি আছে: EF Core-এর DbContext নিজেই Unit of Work, আর DbSet অনেকটা Repository-র মতোই কাজ করে। তাই তার উপর আরেকটা Repository অনেকের কাছে অপ্রয়োজনীয় লেয়ার। আমি এটা পুরোপুরি অস্বীকার করি না — সেজন্যই সব এনটিটির জন্য Repository বানাইনি। কিন্তু provider বদলানোর ঝুঁকি বা domain purity যেখানে সত্যিই দরকার, সেখানে এই লেয়ারের দাম উসুল হয়।
Alternatives
-
Generic Repository (
IRepository<T>): একটাই Repository, CRUD মেথড। কিন্তু domain-specific মেথডের অভাব। -
No Repository at all: সরাসরি Handler-এ
DbContextinject। Small project-এ সবচেয়ে simple। -
Query Object Pattern: Complex query আলাদা ক্লাসে encapsulate করা। Repository-র চেয়ে flexible। .NET-এ
Ardalis.Specificationলাইব্রেরি এর জনপ্রিয় implementation।
২. MediatR + CQRS: Pipeline-এর শক্তি
CQRS আসলে কী
CQRS = Command Query Responsibility Segregation। মানে: Command state পরিবর্তন করে, Query state পড়ে। এর বেশি কিছু না। Event Sourcing বা আলাদা ডেটাবেজ লাগে না।
বেশিরভাগ .NET অ্যাপে CQRS মানে: "read আর write operation-এর জন্য আলাদা ক্লাস থাকা।"
MediatR কীভাবে এলো
MediatR হলো Mediator pattern-এর একটা in-process implementation। Controller থেকে mediator.Send(command) কল করলে MediatR সঠিক handler খুঁজে বের করে execute করে।
আসল পাওয়ার: Pipeline Behaviors
MediatR-এর সবচেয়ে বড় advantage হলো Pipeline Behaviors। এগুলো handler-এর আগে বা পরে execute হয়। আমি FluentValidation-কে pipeline behavior হিসেবে সেট করেছি — ফলে invalid input কখনোই handler-এ পৌঁছায় না।
// Controller: thin, just dispatches
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderCommand command)
{
var orderId = await _mediator.Send(command);
return Ok(orderId);
}
Controller-এ কোনো business logic নেই। শুধু command dispatch। Logic থাকে handler-এ:
public class CreateOrderHandler(IOrderRepository repo)
: IRequestHandler<CreateOrderCommand, Guid>
{
public async Task<Guid> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.CustomerId, cmd.Items);
await repo.AddAsync(order, ct);
return order.Id;
}
}
আর validation pipeline behavior হিসেবে একবারই লেখা হয়, সব command-এর জন্য কাজ করে:
public class ValidationBehavior<TRequest, TResponse>(
IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
{
public async Task<TResponse> Handle(
TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
var results = await Task.WhenAll(
validators.Select(v => v.ValidateAsync(request, ct)));
var failures = results.SelectMany(r => r.Errors).ToList();
if (failures.Count > 0)
throw new ValidationException(failures);
return await next();
}
}
কিন্তু এর সমস্যা
-
Navigation Issue:
Sendমেথডে "Go to Definition" চাপলে MediatR interface-এ যায়, আসল handler-এ না। ম্যানুয়ালি handler খুঁজতে হয়। IDE plugin ছাড়া frustrating। - Reflection Overhead: MediatR startup-এ handler খুঁজতে reflection ব্যবহার করে। Standard app-এ negligible, high-performance system-এ matter করে।
- Boilerplate: ৩টা টেবিলের CRUD API-তে MediatR ব্যবহার করলে ৪৭টা extra ক্লাস তৈরি হয়, real benefit ছাড়াই।
আমার Rule of Thumb
- 10+ distinct operations → MediatR-এর value আছে
- 3-4 endpoints → plain service class-ই best
- Complex cross-cutting concerns (validation, logging, caching) → MediatR pipeline দুর্দান্ত
- Simple CRUD → MediatR overkill
Alternatives
- Plain DI Services: সবচেয়ে simple। Controller সরাসরি সার্ভিস কল করে।
- Wolverine: MediatR-এর lightweight alternative। Command/Query dispatch-এ কম overhead।
- Custom Dispatcher: নিজে একটা in-process dispatcher বানানো। Control বেশি, boilerplate কম।
৩. CQRS-এর Silent Killer: Overengineering
যে ভুলটা আমি দেখেছি
একটা প্রজেক্ট দেখেছি — ৫০০ ইউজারের একটা অ্যাপ। অথচ CQRS, event bus, mediator, ৫টা সার্ভিস লেয়ার। একটা ফিল্ড ডেটাবেজ থেকে return করতে ৩টা ইন্টারফেস, ১৭টা ফাইল, ৫টা প্রজেক্ট পার হতে হয়।
Complexity তখনই ভালো যখন real problem solve করে। নাহলে complexity নিজেই একটা problem।
কখন CQRS ব্যবহার করবে
- Read এবং Write-এর requirements significantly আলাদা হলে
- Read path-এ heavy caching বা projection দরকার হলে
- Write path-এ complex business rules enforce করতে হলে
- Team বড় হলে — ownership আর clarity-র জন্য
কখন করবে না
- Simple CRUD অ্যাপ
- Read এবং Write-এর logic প্রায় same
- Small team, যেখানে extra layer maintain করার bandwidth নেই
- Startup-এ যেখানে speed matter করে
এক বাক্যে: "If your domain models are being modified primarily to satisfy a specific UI screen, it is time to consider CQRS." অন্যথায় simple-ই থাকো।
৪. ডিজাইন প্যাটার্ন: কখন আর কখন না
| প্যাটার্ন | কখন ব্যবহার করবে | কখন এড়িয়ে যাবে |
|---|---|---|
| Repository | Multiple data source, migration risk, domain purity দরকার | Simple CRUD, EF Core-এর full power দরকার |
| MediatR | 10+ operations, cross-cutting concerns, team collaboration | 3-4 endpoints, simple CRUD |
| CQRS | Read/write asymmetry, complex domain, scaling needs | Simple CRUD, same logic both sides |
Alternatives এক নজরে
| Alternative | কোথায় ভালো | Trade-off |
|---|---|---|
| Generic Repository | Simple CRUD | Domain-specific মেথড থাকে না |
DbContext সরাসরি |
Small project, কম boilerplate | Data access logic handler-এ ঢুকে যায়, provider বদলানো কঠিন |
| Query Object / Specification | Complex read query | আরেকটা abstraction |
| Wolverine | MediatR-এর lightweight বিকল্প | MediatR-এর চেয়ে ছোট কমিউনিটি |
| Vertical Slice (feature folder) | Feature অনুযায়ী কোড সাজানো, layer-hopping কম | টিমকে convention মানতে হয় |
| Plain DI Services | সবচেয়ে simple | Cross-cutting concern নিজেকে সামলাতে হয় |
আমার সবচেয়ে বড় শিক্ষা
"Can I explain why this pattern exists in my codebase in one sentence?"
উত্তর যদি হয় "কারণ এটা best practice" — আবার ভাবো। উত্তর যদি হয় "কারণ আমাদের read path-এ heavy caching লাগে এবং write path-এ complex validation" — তাহলে রাখো।
উপসংহার
- Repository মানে abstraction, কিন্তু abstraction-এর খরচ আছে। যেখানে migration risk বা multiple data source আছে, শুধু সেখানে ব্যবহার করো।
- MediatR মানে pipeline, কিন্তু pipeline মানে indirection। 10+ operations থাকলে value আছে, নাহলে boilerplate।
- CQRS মানে read আর write আলাদা করা — event sourcing বা separate database না। Simple-এ simple রাখো।
এগুলো টুল, ধর্ম না। সঠিক সময়ে সঠিক টুল ব্যবহার করাই আসল engineering।
আপনার প্রজেক্টে কোন প্যাটার্নটা সবচেয়ে বেশি কাজে লেগেছে, বা কোনটা overkill মনে হয়েছে? কমেন্টে জানান।
Top comments (0)