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

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 đề.

70% Thay đổi 1 tính năng phải sửa ≥3 file
5-8 Layer phải đi qua cho 1 request đơn giản
40% Code trong Service layer chỉ pass-through

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/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

📁 src/MyApp.Api/
├── 📁 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ế:

Phase 1 — Tính năng mới dùng VSA
Giữ nguyên code cũ. Mọi endpoint mới viết theo pattern VSA (file-per-feature). Hai kiến trúc cùng tồn tại trong 1 project — hoàn toàn khả thi vì MediatR handler và Controller/Service không xung đột.
Phase 2 — Migrate endpoint nóng
Chọn 5-10 endpoint được sửa thường xuyên nhất, chuyển từ Controller+Service+Repository thành file VSA duy nhất. Đảm bảo integration test cover trước khi migrate.
Phase 3 — Dọn dẹp Service layer
Khi đã migrate đủ nhiều endpoint, các service class bắt đầu chỉ còn 1-2 method. Lúc này xóa service/repository không còn sử dụng, gộp DbContext access trực tiếp vào handler.
Phase 4 — Loại bỏ Controller
Chuyển toàn bộ endpoint từ Controller sang Minimal API + Carter. Xóa folder Controllers/, Services/, Repositories/. Project giờ chỉ còn Features/ + Shared/.

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