Vertical Slice Architecture: Tổ Chức Code Theo Feature

Rất nhiều codebase bắt đầu với cấu trúc quen thuộc: controllers/, services/, repositories/, models/. Ban đầu nhìn khá gọn, nhưng khi ứng dụng lớn dần, một feature lại bị rải ra năm sáu thư mục khác nhau. Muốn hiểu "publish post hoạt động thế nào" hay "cancel order hoạt động ra sao" là phải nhảy ngang khắp codebase.
Vertical Slice Architecture giải quyết chuyện đó bằng cách tổ chức code quanh feature và use case, thay vì quanh technical layers. Thay vì gom tất cả controllers vào một chỗ và tất cả repositories vào một chỗ, mỗi feature sẽ sở hữu request handler, validation, business logic và data access của riêng nó trong một slice.
Kết quả thường là codebase dễ điều hướng hơn, dễ sửa hơn và phù hợp hơn với các team liên tục ship feature.
Series: Software Architecture Patterns Roadmap
Bài liên quan: Kiến Trúc Layered (N-Tier), Hexagonal Architecture, Clean Architecture
Bạn Sẽ Học Được Gì
✅ Vertical Slice Architecture là gì và nó giải quyết vấn đề nào
✅ Tại sao tổ chức theo feature thường scale tốt hơn tổ chức theo horizontal layers
✅ Một cấu trúc thư mục thực tế cho project thật
✅ Request đi qua một slice như thế nào
✅ Khi nào vertical slices phù hợp và khi nào không
✅ Những lỗi phổ biến: fake slices, shared-kernel lan tràn, và over-abstraction
✅ Vertical Slice Architecture liên hệ với Clean và Hexagonal ra sao
Vertical Slice Architecture Là Gì
Vertical Slice Architecture tổ chức hệ thống quanh những hành vi hoàn chỉnh mà người dùng nhìn thấy được.
Một slice thường là những thứ như:
CreateOrderCancelSubscriptionPublishPostResetPassword
Mỗi slice chứa mọi thứ cần thiết cho use case đó:
- request/command model,
- validation,
- handler hoặc application logic,
- data access cho use case đó,
- response mapping,
- và đôi khi cả tests.
Ý tưởng cốt lõi rất đơn giản:
Code nào thay đổi cùng nhau thì nên nằm gần nhau.
Nghe có vẻ hiển nhiên, nhưng rất nhiều codebase phân lớp lại vi phạm điều này mỗi ngày.
Vấn Đề Của Horizontal Layers
Trong cấu trúc layered truyền thống, một feature bị trải ra theo chiều ngang:
src/
├── controllers/
│ └── order-controller.ts
├── services/
│ └── order-service.ts
├── repositories/
│ └── order-repository.ts
├── validators/
│ └── order-validator.ts
└── dtos/
└── order-dto.tsNhìn thì có vẻ ngăn nắp, nhưng nó tối ưu cho việc gom code theo vai trò kỹ thuật, không tối ưu cho việc hiểu một feature từ đầu đến cuối.
Bây giờ tưởng tượng bạn phải đổi hành vi "cancel order":
- tìm controller,
- tìm method trong service,
- tìm validation,
- tìm repository logic,
- tìm DTO mapping,
- rồi hy vọng business rule không bị copy ở chỗ khác.
Chi phí nhận thức đó tăng dần theo mỗi feature mới.
Vertical slices cố gắng giảm khoản "thuế nhận thức" này.
Ý Tưởng Cốt Lõi
Thay vì tổ chức code như thế này:
Bạn tổ chức như thế này:
Mỗi slice là một mini-module cho một use case.
Một slice điển hình có thể trông như sau:
src/features/orders/create-order/
├── create-order.route.ts
├── create-order.request.ts
├── create-order.validator.ts
├── create-order.handler.ts
├── create-order.repository.ts
└── create-order.response.tsĐiều này không có nghĩa là mỗi slice phải duplicate mọi thứ. Ý nghĩa thực sự là mỗi slice nên sở hữu phần code đặc thù của hành vi đó.
Ví Dụ Thực Tế
Hãy tưởng tượng một nền tảng blog có use case "Publish Post".
src/
├── features/
│ └── posts/
│ ├── publish-post/
│ │ ├── publish-post.route.ts
│ │ ├── publish-post.request.ts
│ │ ├── publish-post.handler.ts
│ │ └── publish-post.repository.ts
│ └── get-post-details/
│ ├── get-post-details.route.ts
│ ├── get-post-details.query.ts
│ ├── get-post-details.handler.ts
│ └── get-post-details.repository.ts
└── shared/
├── db/
├── auth/
└── kernel/Điểm đáng chú ý là không còn:
services/khổng lồ,controllers/khổng lồ,repositories/khổng lồ.
Bạn có thể mở một thư mục feature và hiểu một hành vi mà không cần tự ghép toàn bộ ứng dụng trong đầu.
Request Đi Qua Slice Như Thế Nào
Trong Vertical Slice Architecture, request thường đi qua một feature module với rất ít đường vòng:
Handler thường là trung tâm của slice. Nó điều phối use case. Nó có thể gọi domain objects hoặc shared infrastructure, nhưng hành vi của feature vẫn được giữ cục bộ.
Dưới đây là ví dụ TypeScript thực tế:
// src/features/posts/publish-post/publish-post.handler.ts
type PublishPostCommand = {
authorId: string;
title: string;
content: string;
};
type PostRecord = {
id: string;
author_id: string;
title: string;
content: string;
status: "draft" | "published";
published_at: Date | null;
};
export class PublishPostHandler {
constructor(private readonly repository: PublishPostRepository) {}
async execute(command: PublishPostCommand): Promise<PostRecord> {
if (command.title.trim().length < 5) {
throw new Error("Title must be at least 5 characters");
}
const draft = await this.repository.findDraftByAuthorAndTitle(
command.authorId,
command.title
);
if (draft) {
throw new Error("A draft with this title already exists");
}
return this.repository.insertPublishedPost({
authorId: command.authorId,
title: command.title.trim(),
content: command.content.trim(),
publishedAt: new Date(),
});
}
}Và repository chỉ phục vụ cho slice đó:
// src/features/posts/publish-post/publish-post.repository.ts
export class PublishPostRepository {
constructor(private readonly db: DbClient) {}
findDraftByAuthorAndTitle(authorId: string, title: string) {
return this.db.post.findFirst({
where: { authorId, title, status: "draft" },
});
}
insertPublishedPost(input: {
authorId: string;
title: string;
content: string;
publishedAt: Date;
}) {
return this.db.post.create({
data: {
authorId: input.authorId,
title: input.title,
content: input.content,
status: "published",
publishedAt: input.publishedAt,
},
});
}
}Repository này không cố trở thành abstraction tái sử dụng khổng lồ cho toàn bộ ứng dụng. Nó phục vụ tốt cho đúng một use case.
Vì Sao Nhiều Team Thích Vertical Slices
1. Ownership Theo Feature Tốt Hơn
Mỗi feature có một "nhà" rõ ràng. Khi đồng đội hỏi "logic publish post nằm ở đâu?", câu trả lời là một thư mục, không phải năm thư mục.
2. Cô Lập Thay Đổi Dễ Hơn
Khi requirement đổi, bạn thường sửa một slice thay vì chạm vào nhiều horizontal layers dùng chung cho toàn app.
3. Giảm Cognitive Load
Developer có thể hiểu một use case mà không phải tải quá nhiều phần không liên quan vào working memory.
4. Phù Hợp Với Product Work
Phần lớn team ship feature, không ship layers. Tổ chức theo feature thường khớp hơn với cách công việc được lên kế hoạch, review và test.
5. Reuse Trung Thực Hơn
Các hệ thống phân lớp thường tạo "shared service" quá sớm. Vertical slices buộc bạn giữ code cục bộ cho đến khi reuse thực sự được chứng minh.
Làm Rõ Một Điểm Quan Trọng: Nó Không Chống Lại Layers
Vertical Slice Architecture không có nghĩa là architecture biến mất.
Bên trong một slice, bạn vẫn có thể có:
- route/controller làm điểm vào,
- application logic,
- domain rules,
- và data access.
Khác biệt là các concerns này được gom trong phạm vi feature, thay vì bị tách ra thành những thư mục dùng chung khổng lồ cho cả ứng dụng.
Đó là lý do Vertical Slice Architecture thường kết hợp rất tốt với:
- Clean Architecture
- Hexagonal Architecture
- handler kiểu CQRS
- DDD aggregates và value objects
Bạn hoàn toàn có thể có một vertical slice mà handler nói chuyện với ports, domain model vẫn clean, còn infrastructure được ẩn sau adapters.
Vertical slices trả lời câu hỏi "nên tổ chức features như thế nào?"
Clean/Hexagonal trả lời câu hỏi "dependencies nên chảy như thế nào?"
Đây là các câu hỏi bổ sung cho nhau, không loại trừ nhau.
Khi Nào Vertical Slice Architecture Phù Hợp
Vertical Slice Architecture đặc biệt hiệu quả khi:
- sản phẩm có nhiều use case khác nhau,
- team ship thay đổi theo từng feature,
- codebase đang khổ sở vì các shared service class quá lớn,
- onboarding chậm vì logic bị rải rác,
- và team muốn ownership theo feature rõ hơn.
Nó rất hợp với:
- business applications,
- internal tools,
- SaaS products,
- hệ thống CRUD-heavy nhưng có workflow có ý nghĩa,
- và API nơi use case quan trọng hơn abstraction tái sử dụng quanh entity.
Khi Nó Có Thể Chưa Phải Lựa Chọn Tốt Nhất
Vertical slices không tự động là câu trả lời tốt nhất cho mọi project.
Hãy cẩn thận nếu:
- hệ thống vẫn còn rất nhỏ,
- team còn ít kinh nghiệm và đang cần convention đơn giản trước,
- domain phần lớn chỉ là một lớp bọc dữ liệu mỏng với ít hành vi,
- hoặc bạn đang ép mọi thứ thành slices trong khi độ phức tạp thực sự nằm ở shared infrastructure.
Với ứng dụng rất nhỏ, một cấu trúc layered đơn giản có thể đã đủ. Architecture nên giải quyết pain thật, không nên tạo thêm ceremony.
Những Sai Lầm Phổ Biến
1. Fake Slices
Một số team tạo feature folders, nhưng mọi logic thật vẫn chảy vào services/ và repositories/ dùng chung khổng lồ.
Đó không phải vertical slicing. Đó là layered architecture mặc áo feature-folder.
2. Shared Folder Bùng Nổ
Nếu cứ ba file lại có một file bị chuyển vào shared/, các slices sẽ thành những cái vỏ rỗng. Shared code nên được "kiếm được" bằng reuse lặp lại, không nên tạo ra trước.
3. Ám Ảnh Generic Repository
Cố ép một generic repository cho mọi slice thường làm mất đi ý nghĩa business hữu ích. Nhiều khi một query riêng cho đúng use case mới là thiết kế hợp lý.
4. Copy-Paste Không Có Ranh Giới
Giữ code cục bộ là tốt. Duplicate mù quáng thì không. Nếu ba slices cùng chia sẻ một policy hoặc domain concept, hãy tách khái niệm đó ra một cách có chủ đích.
5. Nhầm "Feature" Với "Database Table"
Slice nên đại diện cho một hành vi hoặc use case, không chỉ là một table. orders/ thường quá rộng. create-order/, cancel-order/, và ship-order/ thường rõ ràng hơn.
Vertical Slice vs Layered vs Clean vs Hexagonal
| Architecture | Ý tưởng tổ chức chính | Câu hỏi nó trả lời tốt nhất |
|---|---|---|
| Layered | Các technical layers theo chiều ngang | Làm sao tách các trách nhiệm lớn? |
| Vertical Slice | Các module theo feature/use case | Làm sao để mỗi hành vi dễ tìm và dễ sửa? |
| Clean | Dependency rule, inner vs outer policies | Làm sao bảo vệ business logic khỏi frameworks? |
| Hexagonal | Ports và adapters ở biên hệ thống | Làm sao cô lập core khỏi external systems? |
Kết luận thực dụng là:
- dùng Layered khi hệ thống còn nhỏ hoặc team muốn convention đơn giản,
- dùng Vertical Slice khi feature sprawl và cognitive load bắt đầu đau,
- dùng Clean/Hexagonal khi dependency boundaries và việc bảo vệ core dài hạn là cực kỳ quan trọng,
- và nhớ rằng Vertical Slice hoàn toàn có thể sống cùng Clean hoặc Hexagonal.
Một Lộ Trình Áp Dụng Thực Dụng
Bạn không cần big-bang rewrite.
Một cách làm thực tế là:
- Giữ nguyên các feature cũ ở chỗ cũ.
- Xây feature mới dưới dạng slices.
- Chỉ trích xuất phần code thực sự shared.
- Từ từ di chuyển những workflow cũ đau đầu nhất sang slices.
Cách này giữ được đà phát triển và tránh việc refactor architecture tự biến thành một project riêng.
Kết Luận
Vertical Slice Architecture được ưa chuộng vì nó khớp với cách team thực sự làm việc: giao feature và thay đổi use case.
Nó sẽ không tự động sửa được bad domain modeling hay boundary không rõ ràng. Nhưng nó giải quyết một vấn đề rất thật: trong nhiều codebase phân lớp, code của một feature bị rải khắp nơi.
Nếu team của bạn liên tục phải hỏi "logic này nằm ở đâu?" hoặc mỗi thay đổi đều phải chạm controller, service, repository và mapper ở những góc khác nhau của ứng dụng, vertical slices rất đáng để cân nhắc nghiêm túc.
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.