শুরুর কথা
প্রজেক্ট শুরু করলে ফাইল থাকে হাতে গোনা ৫-৬টা। দুই মাস পর সেটা হয়ে যায় ২০০টা। তখন হঠাৎ payment-এ একটা bug ধরতে হবে, আর তুমি বুঝতেই পারছ না কোন ফোল্ডারে ঢুকবে।
অনেকেই এই জায়গায় ১০ মিনিট শুধু ফাইল খুঁজে সময় নষ্ট করে। টিমে নতুন কেউ এলে তো তার অবস্থা আরও খারাপ।
তাই আমি folder structure-কে বলি প্রজেক্টের map। map ঠিক থাকলে যেকোনো জায়গায় ২ সেকেন্ডে পৌঁছে যাওয়া যায়।
১. Clean Architecture-এর মূল ধারণা
Clean Architecture-এ ৪টা layer থাকে। পেঁয়াজের খোসার মতো ভাবো, একটার ভেতরে আরেকটা।
┌─────────────────────────────────┐
│ Presentation (API) │
│ ┌───────────────────────────┐ │
│ │ Infrastructure │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ Application │ │ │
│ │ │ ┌───────────────┐ │ │ │
│ │ │ │ Domain │ │ │ │
│ │ │ └───────────────┘ │ │ │
│ │ └─────────────────────┘ │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
মূল নিয়ম: বাইরের layer ভেতরের layer-কে চেনে। ভেতরের layer বাইরের কাউকে চেনে না।
মানে Domain জানেই না SQL Server কী জিনিস। Application জানে না Controller কী। কিন্তু API সবাইকে চেনে।
এভাবে করার কারণ সোজা। Database বদলাবে, UI বদলাবে, কিন্তু business logic মোটামুটি একই থাকবে। তাই সেটাকে আলাদা করে সুরক্ষিত রাখি।
২. চারটা Layer, একে একে
Domain: বাড়ির ইট
প্রজেক্টের কেন্দ্র। Business-এর আসল ধারণাগুলো এখানে থাকে।
থাকে:
-
Entities: যেমন
Student,Course,Enrollment। দেখতে table-এর মতো, কিন্তু business rule সহ -
Value Objects: যেগুলোর আলাদা id লাগে না, যেমন
Money,Address -
Enums:
EnrollmentStatus.Active,PaymentStatus.Paid -
Domain Events:
StudentEnrolled,PaymentCompleted
থাকে না: database, API, DbContext, HttpContext, আর প্রায় কোনো NuGet package-ই না।
Application: বাড়ির নকশা
এখানে ঠিক হয় কী হবে আর কীভাবে হবে।
থাকে:
-
Services:
EnrollmentService,PaymentService -
Interfaces:
IEnrollmentRepository,IEmailSender -
DTOs:
EnrollmentRequestDto,PaymentResponseDto - Validators (FluentValidation) আর Mappings (AutoMapper)
থাকে না: database connection, HTTP call, কোনো concrete implementation।
একটা প্রশ্ন সবাই করে: IEnrollmentRepository এখানে, তাহলে implementation কোথায়? উত্তর, Infrastructure-এ। এটাই Dependency Inversion।
Infrastructure: বাড়ির প্লাম্বিং
বাইরের জগতের সাথে সব যোগাযোগ এখানে। Database, payment gateway, email, file system।
থাকে: AppDbContext, repository-র implementation, AdyenPaymentService, SendGridEmailService, EF configuration, migrations।
একটা মজার টেস্ট: পুরো Infrastructure project মুছে দিলেও Application layer টের পাবে না। কারণ সে শুধু interface চেনে।
Presentation (API): বাড়ির দরজা
HTTP request এখান দিয়েই ঢোকে।
থাকে: Controllers, Middleware, Filters, আর Program.cs (DI setup আর pipeline)।
⚠️ Controller-এ business logic লিখো না। Controller শুধু HTTP সামলাবে, বাকি কাজ service-এর।
| Layer | বাড়ির সাথে তুলনা | কাজ |
|---|---|---|
| Domain | ইট, সিমেন্ট | মূল উপাদান |
| Application | নকশা | কী হবে, কীভাবে হবে |
| Infrastructure | প্লাম্বিং, বিদ্যুৎ | বাইরের সাথে যোগাযোগ |
| Presentation | দরজা | সবার প্রবেশপথ |
৩. বাস্তব Folder Structure
SchoolApp/
├── src/
│ ├── SchoolApp.Domain/
│ │ ├── Entities/ (Student, Course, Enrollment)
│ │ ├── ValueObjects/ (Money, Address)
│ │ ├── Enums/
│ │ ├── Events/
│ │ └── Common/ (BaseEntity)
│ │
│ ├── SchoolApp.Application/
│ │ ├── Services/
│ │ ├── Interfaces/
│ │ ├── DTOs/
│ │ ├── Validators/
│ │ └── Mappings/
│ │
│ ├── SchoolApp.Infrastructure/
│ │ ├── Persistence/ (AppDbContext, Repositories, Configurations)
│ │ ├── Services/ (AdyenPaymentService, SendGridEmailService)
│ │ └── Migrations/
│ │
│ └── SchoolApp.API/
│ ├── Controllers/
│ ├── Middleware/
│ ├── Program.cs
│ └── appsettings.json
│
└── tests/
├── SchoolApp.UnitTests/
└── SchoolApp.IntegrationTests/
৪. Request-এর যাত্রা
HTTP Request
↓
Controller (API)
↓
Service (Application)
↓
Repository Interface (Application)
↓
Repository Impl (Infrastructure)
↓
DbContext → Database
উপর থেকে নিচে যায় request, নিচ থেকে উপরে আসে response। প্রতিটা layer শুধু তার ঠিক পরের layer-কে ডাকে। Controller সরাসরি DbContext ধরবে না।
৫. ছোট একটা উদাহরণ: Enrollment
Controller (API)
[HttpPost("enroll")]
public async Task<IActionResult> Enroll(EnrollmentRequestDto request)
{
var result = await _enrollmentService.EnrollAsync(request);
return Ok(result);
}
Service (Application)
public async Task<EnrollmentResponseDto> EnrollAsync(EnrollmentRequestDto request)
{
var course = await _courseRepository.GetByIdAsync(request.CourseId);
if (course.Seats <= 0)
throw new BusinessException("No seats available");
var enrollment = new Enrollment(request.StudentId, course.Id);
await _enrollmentRepository.AddAsync(enrollment);
return _mapper.Map<EnrollmentResponseDto>(enrollment);
}
Repository (Infrastructure)
public async Task<Course?> GetByIdAsync(int id)
{
return await _dbContext.Courses.FirstOrDefaultAsync(c => c.Id == id);
}
খেয়াল করো, Service-এ business rule (Seats <= 0) আছে কিন্তু database-এর নাম নেই। Repository-তে database আছে কিন্তু কোনো business rule নেই। এটাই পুরো ব্যাপারটার সারকথা।
৬. নতুনদের ৬টা কমন ভুল
- Controller-এ business logic লেখা। সবচেয়ে বেশি দেখা যায়।
-
Domain-এ
DbContextব্যবহার। Domain database চেনে না। - Application-এ concrete class ব্যবহার। সবসময় interface নাও।
- Repository-তে business logic ঢোকানো। Repository শুধু data আনবে-নেবে।
- সব কিছু এক project-এ ফেলে রাখা। অন্তত আলাদা folder তো করো।
- DTO ছাড়া Entity return করা। Entity সরাসরি বাইরে পাঠিও না।
৭. কখন এই structure লাগবে না?
সত্যি কথা, ছোট প্রজেক্টে এটা overkill। ৫-১০টা endpoint-এর জন্য ৪টা project আর ২০টা folder বানালে দেখবে মাত্র ৩টা class-এর জন্য এত ঝামেলা।
আমার নিয়ম: কোনো pattern তখনই আনো, যখন সেটা না থাকার কষ্ট, রক্ষণাবেক্ষণের কষ্টের চেয়ে বেশি।
| প্রজেক্টের সাইজ | যা করবে |
|---|---|
| ৫-১০ endpoint | এক project, simple folder |
| ২০-৫০ endpoint | এক project, আলাদা folder (Domain, Services, Data) |
| ৫০+ endpoint বা টিমে ২+ জন | Clean Architecture |
শেষ কথা
Folder structure আসলে দায়িত্ব ভাগ করে দেওয়ার ব্যাপার। Database-এর কাজ database করবে, business-এর কাজ business, আর presentation-এর কাজ presentation।
প্রতিটা class যখন শুধু নিজের কাজটাই করে, তখন পুরো প্রজেক্ট বোঝা সহজ হয়, test করা সহজ হয়, আর বদলানোও সহজ হয়।
এটা কোনো কঠিন নিয়ম না। এটা common sense।
Top comments (1)
Thú nhận thật là quen thuộc với cái cảm giác này — mỗi dự án như một cuộc phiêu lưu, bắt đầu bằng vài tệp nhỏ và dần biến thành một "rừng" mà chính ta tạo ra. Mình đã từng vô số lần dừng lại, nhìn vào cấu trúc thư mục và tự hỏi: "Tại sao mình lại để nó vĩ đại đến vậy?"
Một điều mình học được là: không có "cấu trúc hoàn hảo" nào tồn tại — nhưng có những nguyên tắc vững vàng giúp giữ cho dự án không sa đẫm vào bếo lông. Ví dụ như việc tách rõ rệt giữa business logic và infrastructure, hay dùng tên thư mục mang ý nghĩa (ví dụ:
Featuresthay vìModules) để mỗi người trong team đều "đọc được" cấu trúc chỉ bằng nhìn.Mình cũng hay nhấn mạnh việc đặt tên nhất quán — một thói quen nhỏ nhưng cứng như sắt khi dự án bắt đầu phình ra. Và đôi khi, chỉ cần một developer cẩn thận với sự đồng nhất từ ngày đầu tiên, cả nhóm sẽ cảm ơn sau nửa năm sau.
Cảm ơn tác giả vì đã chia sẻ — bài viết này như một lời nhắc nhở nhẹ nhàng (site: labagent .tech)