Vertical Slice Architecture trên .NET 10 — Tổ chức code theo tính năng
Posted on: 4/23/2026 6:14:23 AM
Table of contents
- Vấn đề với kiến trúc phân lớp truyền thống
- Vertical Slice Architecture là gì?
- So sánh kiến trúc: VSA vs Clean Architecture
- Cấu trúc thư mục thực tế trên .NET 10
- Triển khai với MediatR + Minimal API
- Pipeline Behaviors — Cross-cutting concerns
- Đăng ký trong Program.cs
- Khi nào KHÔNG nên dùng Vertical Slice?
- VSA kết hợp CQRS
- Testing trong VSA
- Migrate từ Clean Architecture sang VSA
- Thư viện hỗ trợ VSA trên .NET 10
- FastEndpoints — Giải pháp thay thế MediatR
- Kết luận
Vấn đề với kiến trúc phân lớp truyền thống
Nếu bạn đã từng làm việc với các dự án .NET lớn, hẳn bạn quen thuộc với kiến trúc phân lớp (N-Layer): Controller → Service → Repository → Database. Mô hình này trực quan, dễ hiểu, và gần như là "mặc định" cho mọi dự án ASP.NET Core. Nhưng khi dự án phát triển đến hàng trăm endpoint, bạn bắt đầu nhận ra một loạt vấn đề.
Với kiến trúc Clean Architecture hay Onion Architecture, bạn tổ chức code theo layer ngang — tất cả Controller ở một folder, tất cả Service ở folder khác, tất cả Repository ở nơi khác nữa. Khi cần thêm tính năng "Tạo đơn hàng", bạn phải tạo/sửa file ở tối thiểu 3-4 thư mục khác nhau.
graph TB
subgraph "Kiến trúc phân lớp truyền thống"
A["Controllers/"] --> B["OrderController"]
A --> C["ProductController"]
A --> D["UserController"]
E["Services/"] --> F["OrderService"]
E --> G["ProductService"]
E --> H["UserService"]
I["Repositories/"] --> J["OrderRepository"]
I --> K["ProductRepository"]
I --> L["UserRepository"]
end
style A fill:#e94560,stroke:#fff,color:#fff
style E fill:#e94560,stroke:#fff,color:#fff
style I fill:#e94560,stroke:#fff,color:#fff
style B fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style C fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style D fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style F fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style G fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style H fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style J fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style K fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style L fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
Kiến trúc phân lớp — code phân tán theo layer, không theo tính năng
Hậu quả? Khi team có 5 dev cùng làm 5 tính năng khác nhau, tất cả đều sửa vào cùng folder Services/ và Repositories/, tạo ra xung đột merge liên tục. Đây chính là coupling theo chiều ngang — các tính năng không liên quan nhưng lại chia sẻ cùng một không gian code.
Vertical Slice Architecture là gì?
Vertical Slice Architecture (VSA) đảo ngược hoàn toàn cách tổ chức code: thay vì nhóm theo layer, bạn nhóm theo tính năng (feature). Mỗi tính năng là một "lát cắt dọc" xuyên suốt từ request đến database, tự chứa toàn bộ logic cần thiết.
💡 Nguyên tắc cốt lõi
Mỗi use case (tạo đơn hàng, lấy danh sách sản phẩm, cập nhật profile...) là một slice độc lập với request model, handler, validation, và response model riêng. Không chia sẻ abstraction trừ khi thực sự cần thiết.
graph TB
subgraph "Vertical Slice Architecture"
subgraph "Feature: CreateOrder"
A1["Request"] --> A2["Validator"]
A2 --> A3["Handler"]
A3 --> A4["DB Access"]
end
subgraph "Feature: GetProducts"
B1["Request"] --> B2["Handler"]
B2 --> B3["DB Access"]
end
subgraph "Feature: UpdateProfile"
C1["Request"] --> C2["Validator"]
C2 --> C3["Handler"]
C3 --> C4["DB Access"]
end
end
style A1 fill:#e94560,stroke:#fff,color:#fff
style A2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style A3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style A4 fill:#2c3e50,stroke:#fff,color:#fff
style B1 fill:#4CAF50,stroke:#fff,color:#fff
style B2 fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
style B3 fill:#2c3e50,stroke:#fff,color:#fff
style C1 fill:#ff9800,stroke:#fff,color:#fff
style C2 fill:#f8f9fa,stroke:#ff9800,color:#2c3e50
style C3 fill:#f8f9fa,stroke:#ff9800,color:#2c3e50
style C4 fill:#2c3e50,stroke:#fff,color:#fff
Vertical Slice — mỗi tính năng là một đơn vị độc lập, tự chứa toàn bộ logic
Ý tưởng này được Jimmy Bogard (tác giả MediatR, AutoMapper) phổ biến hóa từ năm 2018, nhưng phải đến 2025-2026 với sự trưởng thành của Minimal API trong .NET và các thư viện hỗ trợ, VSA mới thực sự bùng nổ trong cộng đồng .NET.
So sánh kiến trúc: VSA vs Clean Architecture
| Tiêu chí | Clean Architecture | Vertical Slice |
|---|---|---|
| Tổ chức code | Theo layer (Controllers, Services, Repos) | Theo feature (CreateOrder, GetProducts...) |
| Coupling | Coupling ngang giữa các tính năng cùng layer | Mỗi slice độc lập, ít coupling |
| Thêm tính năng mới | Tạo/sửa 3-5 file ở nhiều folder | Thêm 1 file/folder duy nhất |
| Xóa tính năng | Phải tìm và xóa ở nhiều layer | Xóa 1 file/folder là xong |
| Testing | Unit test mỗi layer riêng, mock nhiều | Integration test theo use case, ít mock |
| Merge conflict | Cao — nhiều dev sửa cùng folder | Thấp — mỗi dev làm feature riêng |
| Phù hợp với | Domain phức tạp, team lớn cần chuẩn hóa | API-first, CRUD+logic, team muốn tốc độ |
Cấu trúc thư mục thực tế trên .NET 10
├── 📁 Features/
│ ├── 📁 Orders/
│ │ ├── CreateOrder.cs
│ │ ├── GetOrderById.cs
│ │ ├── ListOrders.cs
│ │ ├── CancelOrder.cs
│ │ └── OrderMappings.cs
│ ├── 📁 Products/
│ │ ├── CreateProduct.cs
│ │ ├── GetProductById.cs
│ │ ├── SearchProducts.cs
│ │ └── UpdateProduct.cs
│ └── 📁 Users/
│ ├── Register.cs
│ ├── Login.cs
│ ├── GetProfile.cs
│ └── UpdateProfile.cs
├── 📁 Shared/
│ ├── AppDbContext.cs
│ ├── PipelineBehaviors/
│ └── Extensions/
├── Program.cs
└── appsettings.json
✅ Quy tắc vàng
Mỗi file trong Features/ chứa trọn vẹn: Request → Validator → Handler → Response → Endpoint cho một use case duy nhất. Folder Shared/ chỉ chứa infrastructure thực sự dùng chung (DbContext, middleware, pipeline behaviors).
Triển khai với MediatR + Minimal API
Combo phổ biến nhất cho VSA trên .NET 10 là MediatR (mediator pattern) + Minimal API (thay cho Controller). MediatR đóng vai trò dispatcher — nhận request, tìm handler tương ứng, và trả về response.
Cài đặt package
dotnet add package MediatR
dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions
dotnet add package Carter
Ví dụ: Feature CreateOrder
Toàn bộ logic của use case "Tạo đơn hàng" nằm trong một file duy nhất:
// Features/Orders/CreateOrder.cs
using Carter;
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace MyApp.Features.Orders;
// 1. Request & Response
public record CreateOrderRequest(
int CustomerId,
List<OrderItemDto> Items,
string? Note
) : IRequest<CreateOrderResponse>;
public record OrderItemDto(int ProductId, int Quantity);
public record CreateOrderResponse(
int OrderId,
decimal TotalAmount,
DateTime CreatedAt
);
// 2. Validation
public class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId).GreaterThan(0);
RuleFor(x => x.Items).NotEmpty()
.WithMessage("Đơn hàng phải có ít nhất 1 sản phẩm");
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(i => i.ProductId).GreaterThan(0);
item.RuleFor(i => i.Quantity).InclusiveBetween(1, 100);
});
}
}
// 3. Handler — toàn bộ business logic
public class CreateOrderHandler(AppDbContext db)
: IRequestHandler<CreateOrderRequest, CreateOrderResponse>
{
public async Task<CreateOrderResponse> Handle(
CreateOrderRequest request,
CancellationToken ct)
{
var products = await db.Products
.Where(p => request.Items.Select(i => i.ProductId).Contains(p.Id))
.ToDictionaryAsync(p => p.Id, ct);
var order = new Order
{
CustomerId = request.CustomerId,
Note = request.Note,
Items = request.Items.Select(i => new OrderItem
{
ProductId = i.ProductId,
Quantity = i.Quantity,
UnitPrice = products[i.ProductId].Price
}).ToList()
};
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return new CreateOrderResponse(
order.Id,
order.Items.Sum(i => i.UnitPrice * i.Quantity),
order.CreatedAt
);
}
}
// 4. Endpoint — Minimal API qua Carter
public class CreateOrderEndpoint : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapPost("/api/orders", async (
CreateOrderRequest request,
ISender sender) =>
{
var result = await sender.Send(request);
return Results.Created($"/api/orders/{result.OrderId}", result);
})
.WithName("CreateOrder")
.WithTags("Orders")
.Produces<CreateOrderResponse>(201)
.ProducesValidationProblem();
}
}
📋 Phân tích cấu trúc
Toàn bộ 4 thành phần (Request/Response model, Validation, Business logic, Endpoint routing) nằm trong 1 file duy nhất. Khi cần sửa logic tạo đơn hàng, bạn chỉ mở đúng file CreateOrder.cs — không cần nhảy qua lại giữa Controller, Service, và Repository.
Ví dụ: Feature GetOrderById (Query)
// Features/Orders/GetOrderById.cs
namespace MyApp.Features.Orders;
public record GetOrderByIdRequest(int OrderId) : IRequest<OrderDetailResponse?>;
public record OrderDetailResponse(
int Id,
string CustomerName,
List<OrderItemResponse> Items,
decimal Total,
string Status,
DateTime CreatedAt
);
public record OrderItemResponse(
string ProductName,
int Quantity,
decimal UnitPrice,
decimal Subtotal
);
public class GetOrderByIdHandler(AppDbContext db)
: IRequestHandler<GetOrderByIdRequest, OrderDetailResponse?>
{
public async Task<OrderDetailResponse?> Handle(
GetOrderByIdRequest request,
CancellationToken ct)
{
return await db.Orders
.Where(o => o.Id == request.OrderId)
.Select(o => new OrderDetailResponse(
o.Id,
o.Customer.Name,
o.Items.Select(i => new OrderItemResponse(
i.Product.Name,
i.Quantity,
i.UnitPrice,
i.Quantity * i.UnitPrice
)).ToList(),
o.Items.Sum(i => i.Quantity * i.UnitPrice),
o.Status.ToString(),
o.CreatedAt
))
.FirstOrDefaultAsync(ct);
}
}
public class GetOrderByIdEndpoint : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("/api/orders/{orderId:int}", async (
int orderId,
ISender sender) =>
{
var result = await sender.Send(new GetOrderByIdRequest(orderId));
return result is not null
? Results.Ok(result)
: Results.NotFound();
})
.WithName("GetOrderById")
.WithTags("Orders");
}
}
Pipeline Behaviors — Cross-cutting concerns
Một câu hỏi thường gặp: "Nếu mỗi slice độc lập, làm sao xử lý các concern xuyên suốt như validation, logging, caching?" Câu trả lời là Pipeline Behaviors của MediatR — hoạt động như middleware cho mỗi request.
graph LR
A["HTTP Request"] --> B["Logging Behavior"]
B --> C["Validation Behavior"]
C --> D["Caching Behavior"]
D --> E["Handler"]
E --> F["Response"]
style A fill:#2c3e50,stroke:#fff,color:#fff
style B fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style D fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style E fill:#e94560,stroke:#fff,color:#fff
style F fill:#4CAF50,stroke:#fff,color:#fff
Pipeline Behaviors — middleware pattern cho MediatR requests
Validation Behavior tự động
// Shared/PipelineBehaviors/ValidationBehavior.cs
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)
{
if (!validators.Any()) return await next(ct);
var context = new ValidationContext<TRequest>(request);
var failures = (await Task.WhenAll(
validators.Select(v => v.ValidateAsync(context, ct))))
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
return await next(ct);
}
}
Logging Behavior
// Shared/PipelineBehaviors/LoggingBehavior.cs
public class LoggingBehavior<TRequest, TResponse>(
ILogger<LoggingBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var requestName = typeof(TRequest).Name;
logger.LogInformation("Handling {RequestName}", requestName);
var sw = Stopwatch.StartNew();
var response = await next(ct);
sw.Stop();
logger.LogInformation(
"Handled {RequestName} in {ElapsedMs}ms",
requestName, sw.ElapsedMilliseconds);
return response;
}
}
Đăng ký trong Program.cs
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// EF Core
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
// MediatR + Pipeline Behaviors
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(LoggingBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(ValidationBehavior<,>));
});
// FluentValidation
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
// Carter (auto-discover endpoints)
builder.Services.AddCarter();
var app = builder.Build();
app.MapCarter(); // Tự động đăng ký tất cả ICarterModule
app.Run();
✅ Carter tự động scan
MapCarter() tự quét assembly tìm tất cả class implement ICarterModule và đăng ký endpoint. Không cần app.MapGet/MapPost thủ công trong Program.cs — mỗi feature tự đăng ký route của mình.
Khi nào KHÔNG nên dùng Vertical Slice?
VSA không phải viên đạn bạc. Có những trường hợp kiến trúc phân lớp truyền thống hoặc Clean Architecture vẫn phù hợp hơn:
| Scenario | Nên dùng VSA? | Lý do |
|---|---|---|
| API với 50+ endpoints, logic CRUD chiếm 70% | ✅ Rất phù hợp | Mỗi endpoint là slice đơn giản, dễ maintain |
| Domain phức tạp với nhiều business rule liên kết | ⚠️ Cân nhắc | Domain logic cần DDD, aggregate roots — có thể kết hợp |
| Team mới, cần chuẩn hóa code style | ❌ Không nên | Clean Architecture cung cấp guideline rõ ràng hơn |
| Microservice nhỏ, single-purpose | ✅ Lý tưởng | Mỗi service chỉ có vài slice, rất gọn |
| Monolith đang refactor dần sang microservices | ✅ Phù hợp | Mỗi feature group có thể tách thành service riêng sau |
⚠️ Cạm bẫy phổ biến
Đừng tạo abstraction quá sớm. Khi mới bắt đầu, hai handler có logic tương tự nhau — hãy để chúng duplicate. Chỉ khi pattern lặp lại ≥3 lần VÀ logic thực sự giống hệt, bạn mới nên extract thành shared service. Premature abstraction trong VSA sẽ biến nó trở lại thành kiến trúc phân lớp.
VSA kết hợp CQRS
VSA kết hợp rất tự nhiên với CQRS (Command Query Responsibility Segregation). Trong thực tế, mỗi slice đã là một command hoặc query riêng biệt:
graph TB
subgraph "Commands (Write)"
C1["CreateOrder"]
C2["CancelOrder"]
C3["UpdateProduct"]
end
subgraph "Queries (Read)"
Q1["GetOrderById"]
Q2["ListOrders"]
Q3["SearchProducts"]
end
C1 --> DB1["Write DB"]
C2 --> DB1
C3 --> DB1
Q1 --> DB2["Read Replica"]
Q2 --> DB2
Q3 --> DB2
style C1 fill:#e94560,stroke:#fff,color:#fff
style C2 fill:#e94560,stroke:#fff,color:#fff
style C3 fill:#e94560,stroke:#fff,color:#fff
style Q1 fill:#4CAF50,stroke:#fff,color:#fff
style Q2 fill:#4CAF50,stroke:#fff,color:#fff
style Q3 fill:#4CAF50,stroke:#fff,color:#fff
style DB1 fill:#2c3e50,stroke:#fff,color:#fff
style DB2 fill:#2c3e50,stroke:#fff,color:#fff
VSA + CQRS — commands và queries tự nhiên tách biệt thành các slice riêng
Với cách tiếp cận này, query handler có thể sử dụng Dapper hoặc raw SQL cho performance tối ưu, trong khi command handler vẫn dùng EF Core với change tracking đầy đủ. Mỗi slice tự quyết định data access strategy phù hợp nhất.
// Features/Orders/ListOrders.cs — Query dùng Dapper
public class ListOrdersHandler(IDbConnection db)
: IRequestHandler<ListOrdersRequest, PagedResult<OrderSummary>>
{
public async Task<PagedResult<OrderSummary>> Handle(
ListOrdersRequest request,
CancellationToken ct)
{
const string sql = """
SELECT o.Id, c.Name AS CustomerName,
COUNT(oi.Id) AS ItemCount,
SUM(oi.Quantity * oi.UnitPrice) AS Total,
o.Status, o.CreatedAt
FROM Orders o
JOIN Customers c ON o.CustomerId = c.Id
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
WHERE (@Status IS NULL OR o.Status = @Status)
GROUP BY o.Id, c.Name, o.Status, o.CreatedAt
ORDER BY o.CreatedAt DESC
OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY
""";
var results = await db.QueryAsync<OrderSummary>(sql, new
{
request.Status,
Skip = (request.Page - 1) * request.PageSize,
Take = request.PageSize
});
return new PagedResult<OrderSummary>(results.ToList(), totalCount);
}
}
Testing trong VSA
Một lợi thế lớn của VSA là testing trở nên đơn giản và có ý nghĩa hơn. Thay vì unit test từng layer riêng lẻ với hàng chục mock, bạn viết integration test cho từng slice:
// Tests/Features/Orders/CreateOrderTests.cs
public class CreateOrderTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task CreateOrder_WithValidItems_ReturnsCreated()
{
// Arrange
var client = factory.CreateClient();
var request = new CreateOrderRequest(
CustomerId: 1,
Items: [new(ProductId: 1, Quantity: 2)],
Note: "Giao buổi sáng"
);
// Act
var response = await client.PostAsJsonAsync("/api/orders", request);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var result = await response.Content
.ReadFromJsonAsync<CreateOrderResponse>();
result!.OrderId.Should().BeGreaterThan(0);
result.TotalAmount.Should().BeGreaterThan(0);
}
[Fact]
public async Task CreateOrder_WithEmptyItems_ReturnsBadRequest()
{
var client = factory.CreateClient();
var request = new CreateOrderRequest(
CustomerId: 1, Items: [], Note: null);
var response = await client.PostAsJsonAsync("/api/orders", request);
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
}
✅ Test theo behavior, không theo implementation
Integration test kiểm tra "gửi request hợp lệ → nhận response đúng" thay vì "Service gọi Repository đúng method". Khi refactor internal code, test vẫn pass miễn là behavior không đổi. Đây chính là test có giá trị thực sự.
Migrate từ Clean Architecture sang VSA
Nếu dự án hiện tại đang dùng Clean Architecture và bạn muốn chuyển sang VSA, đây là lộ trình thực tế:
Thư viện hỗ trợ VSA trên .NET 10
| Thư viện | Vai trò | Ghi chú |
|---|---|---|
| MediatR | Mediator / CQRS dispatcher | De facto standard, pipeline behaviors mạnh mẽ |
| Carter | Auto-discovery cho Minimal API | Thay thế Controller, tự scan endpoint modules |
| FluentValidation | Request validation | Tích hợp tốt với MediatR qua pipeline behavior |
| Wolverine | Thay thế MediatR + message bus | All-in-one: mediator + message queue + saga |
| FastEndpoints | Endpoint framework kiểu VSA | Không cần MediatR, tự cung cấp request/handler/validator |
| Immediate.Handlers | Source-generated mediator | Zero reflection, AOT-friendly, nhanh hơn MediatR |
FastEndpoints — Giải pháp thay thế MediatR
Nếu bạn không muốn sử dụng MediatR, FastEndpoints là một framework được thiết kế riêng cho VSA pattern trên .NET. Nó cung cấp sẵn request binding, validation, và endpoint routing trong một package duy nhất:
// Features/Orders/CreateOrder.cs — với FastEndpoints
public class CreateOrderEndpoint
: Endpoint<CreateOrderRequest, CreateOrderResponse>
{
public override void Configure()
{
Post("/api/orders");
AllowAnonymous();
}
public override async Task HandleAsync(
CreateOrderRequest req,
CancellationToken ct)
{
var order = new Order
{
CustomerId = req.CustomerId,
Items = req.Items.Select(i => new OrderItem
{
ProductId = i.ProductId,
Quantity = i.Quantity
}).ToList()
};
await DbContext.Orders.AddAsync(order, ct);
await DbContext.SaveChangesAsync(ct);
await SendCreatedAtAsync<GetOrderByIdEndpoint>(
new { order.Id },
new CreateOrderResponse(order.Id, order.Total, order.CreatedAt),
cancellation: ct);
}
}
Kết luận
Vertical Slice Architecture không phải là "kẻ thay thế" Clean Architecture — đó là một cách tiếp cận khác cho một loại bài toán khác. Khi dự án của bạn chủ yếu là API endpoints với logic business không quá phức tạp, VSA giúp bạn:
- Giảm coupling — mỗi tính năng độc lập, sửa một chỗ không ảnh hưởng chỗ khác
- Tăng tốc phát triển — thêm feature mới chỉ cần tạo 1 file
- Giảm merge conflict — dev làm feature riêng, không đụng vào code của nhau
- Test có ý nghĩa hơn — integration test theo use case thay vì unit test từng layer
- Dễ onboard — dev mới chỉ cần đọc 1 file để hiểu trọn vẹn 1 tính năng
Với sự trưởng thành của Minimal API trên .NET 10, kết hợp MediatR (hoặc FastEndpoints, Wolverine), việc triển khai VSA chưa bao giờ dễ dàng đến vậy. Hãy bắt đầu bằng cách áp dụng cho tính năng mới tiếp theo trong dự án của bạn — không cần rewrite toàn bộ.
Tham khảo
Rate Limiting — Kiểm Soát Lưu Lượng API Trong Hệ Thống Phân Tán
Vue Vapor Mode — Loại Bỏ Virtual DOM, Tăng Hiệu Năng Gấp 3 Lần
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.