Thay vì chia theo Layer (Controller → Service → Repository), chia theo Feature. Mỗi feature là 1 slice cắt xuyên từ UI đến Database.
Tổng Quan
Khác biệt cốt lõi: Không có shared Service layer. Mỗi feature TỰ CHỨA logic, tự truy cập DB. Thay đổi feature A KHÔNG ảnh hưởng feature B.
Khi Nào Dùng / Không Dùng
| ✅ Dùng khi | ❌ Không dùng khi |
|---|---|
| Features ĐỘC LẬP nhau (ít share logic) | Business rules CHIA SẺ nhiều giữa features |
| Team muốn ship feature nhanh, ít ceremony | Cần rich Domain Model (DDD) |
| CQRS pattern (read/write tách biệt) | Team quen Clean Architecture, không muốn đổi |
| Microservices (mỗi service nhỏ, ít layers) | Cần enforce strict domain invariants |
Dự án hay dùng: API microservices, BFF (Backend for Frontend), Admin tools, MVP nhanh.
Project Structure
MyApp/
├── MyApp.sln
├── src/
│ └── MyApp.API/
│ ├── Features/
│ │ ├── Orders/
│ │ │ ├── CreateOrder.cs ← Command + Handler + Endpoint
│ │ │ ├── GetOrderById.cs ← Query + Handler + Endpoint
│ │ │ ├── CancelOrder.cs
│ │ │ └── OrderDto.cs
│ │ ├── Products/
│ │ │ ├── CreateProduct.cs
│ │ │ ├── ListProducts.cs
│ │ │ └── ProductDto.cs
│ │ └── Users/
│ │ ├── Register.cs
│ │ └── Login.cs
│ ├── Common/
│ │ ├── AppDbContext.cs
│ │ ├── Behaviors/
│ │ │ └── ValidationBehavior.cs
│ │ └── Extensions/
│ │ └── ResultExtensions.cs
│ ├── Program.cs
│ └── appsettings.json
└── tests/
└── MyApp.API.Tests/
└── Features/
└── Orders/
├── CreateOrderTests.cs
└── GetOrderByIdTests.cs
Code Blueprint - 1 Vertical Slice
csharp// File: Features/Orders/CreateOrder.cs - TẤT CẢ trong 1 file!
// === Request (Command) ===
public record CreateOrderCommand(Guid ProductId, int Quantity) : IRequest<Result<Guid>>;
// === Validator ===
public class CreateOrderValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderValidator()
{
RuleFor(x => x.ProductId).NotEmpty();
RuleFor(x => x.Quantity).GreaterThan(0);
}
}
// === Handler ===
public class CreateOrderHandler(AppDbContext db) : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
public async Task<Result<Guid>> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
var product = await db.Products.FindAsync([cmd.ProductId], ct);
if (product is null)
return Result.Failure<Guid>("Product not found");
var order = new Order
{
Id = Guid.NewGuid(),
ProductId = cmd.ProductId,
Quantity = cmd.Quantity,
Total = product.Price * cmd.Quantity
};
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return Result.Success(order.Id);
}
}
// === Endpoint ===
public static class CreateOrderEndpoint
{
public static void Map(IEndpointRouteBuilder app) =>
app.MapPost("/api/orders", async (CreateOrderCommand cmd, ISender sender) =>
{
var result = await sender.Send(cmd);
return result.IsSuccess
? Results.Created($"/api/orders/{result.Value}", result.Value)
: Results.BadRequest(result.Error);
})
.WithTags("Orders")
.RequireAuthorization();
}
So sánh Clean Architecture vs Vertical Slice
| Tiêu chí | Clean Architecture | Vertical Slice |
|---|---|---|
| Số projects | 4-5 | 1-2 |
| Shared code | Nhiều (interfaces, base classes) | Ít (copy nếu cần) |
| Feature isolation | Trung bình | CAO |
| Domain complexity | Tốt cho DDD | Tốt cho CRUD |
| Learning curve | Cao | Thấp |
| Merge conflicts | Cao (shared layers) | Thấp (isolated features) |