Nỗi ám ảnh mang tên “Spaghetti Code” và bước ngoặt thay đổi
Sau 6 tháng vận hành một dự án SaaS với hơn 50 màn hình và 10 developer, mình thực sự rơi vào bế tắc. Cấu trúc thư mục kiểu cũ (folder-by-type) như components/, hooks/, services/ ban đầu trông rất gọn nhưng nhanh chóng biến thành một đống hỗn độn. Việc tìm một logic ẩn sâu trong hàng trăm file hay sửa một tính năng mà không làm sập phần khác bỗng trở thành thử thách cực hạn.
Mình quyết định tái cấu trúc toàn bộ theo Feature-Sliced Design (FSD). Kết quả thật bất ngờ: Thời gian onboarding cho dev mới giảm từ 2 tuần xuống còn 4 ngày. Tốc độ Code Review cũng tăng gấp đôi vì mọi thứ đều nằm ở nơi nó nên thuộc về. FSD không đơn thuần là cách đặt tên folder. Nó là tư duy phân tầng (layering) giúp tách biệt các mối quan tâm một cách khoa học.
Nếu bạn từng phát điên khi sửa file A mà file B cách đó 10 tầng thư mục lăn đùng ra lỗi, FSD chính là lối thoát bạn đang tìm kiếm.
7 lớp cấu trúc: Xương sống của FSD
FSD chia dự án thành 7 tầng cố định. Quy tắc vàng cực kỳ đơn giản: Chỉ được import từ tầng thấp hơn. Tuyệt đối không import ngược lên trên hoặc import chéo giữa các module trong cùng một tầng.
Đây là sơ đồ mình đã áp dụng thành công:
src/
├── app/ # Cấu hình tổng (Providers, Styles, Routing)
├── processes/ # (Tùy chọn) Các luồng phức tạp đi qua nhiều trang
├── pages/ # Composition của toàn bộ trang
├── widgets/ # Các khối giao diện lớn (ví cả Header, Sidebar)
├── features/ # Hành động của người dùng (Login, Search, AddToCart)
├── entities/ # Business logic cốt lõi (User, Product, Order)
├── shared/ # Code dùng chung (UI Kit, API client, Utils)
Triển khai thực tế: Đừng tham lam
Bạn không cần đập đi xây lại mọi thứ ngay. Hãy bắt đầu từ tầng thấp nhất là shared.
- Shared: Tập hợp các thành phần “câm”, không mang nghiệp vụ. Ví dụ: Button, Input, Axios instance.
- Entities: Nơi định nghĩa đối tượng. Ví dụ:
entities/usersẽ chứa UI của UserCard, các hàm xử lý dữ liệu User và Redux slice tương ứng. - Features: Tập trung vào giá trị người dùng nhận được. Ví dụ:
features/auth-by-email.
Mẹo nhỏ để phân biệt: Entity trả lời cho câu hỏi “Đây là cái gì?” (Dữ liệu User), còn Feature trả lời cho câu hỏi “Người dùng làm được gì?” (Đăng nhập).
Public API: Người gác cổng nghiêm ngặt
Trong FSD, mỗi thư mục con phải có một file index.ts đóng vai trò Public API. Đây là cái phễu duy nhất cho phép thế giới bên ngoài chạm vào bên trong module đó.
Cấu trúc một Feature điển hình sẽ như thế này:
features/add-comment/
├── ui/ # Giao diện hiển thị
├── model/ # Logic (States, Selectors)
├── lib/ # Các hàm helper nội bộ
└── index.ts # Cổng xuất dữ liệu duy nhất
Trong index.ts, bạn chỉ export những gì thực sự cần thiết:
// features/add-comment/index.ts
export { AddCommentForm } from './ui/AddCommentForm';
export type { CommentSchema } from './model/types';
Để code sạch và chuyên nghiệp, hãy tận dụng path aliases trong tsconfig.json. Thay vì viết những đường dẫn dài dằng dặc, hãy dùng @/:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Khi đó, việc import tại pages/article-details trở nên cực kỳ tinh gọn: import { AddCommentForm } from '@/features/add-comment';. Lưu ý: Đừng bao giờ import xuyên qua Public API (kiểu @/features/add-comment/ui/Form). Làm vậy sẽ phá vỡ tính đóng gói và khiến hệ thống trở nên cực kỳ khó bảo trì.
Dùng ESLint làm “cảnh sát” kiến trúc
Lý thuyết rất hay, nhưng khi team đông, lỗi con người là khó tránh khỏi. Ai đó có thể lỡ tay import chéo giữa hai feature. Để ngăn chặn điều này, mình sử dụng eslint-plugin-boundaries.
Cài đặt công cụ kiểm soát ranh giới:
npm install eslint-plugin-import eslint-plugin-boundaries --save-dev
Cấu hình này sẽ giúp bạn tự động báo lỗi ngay trên IDE nếu có ai đó vi phạm quy tắc phân tầng. Ví dụ, ngăn không cho entities phụ thuộc vào features:
// Trích đoạn cấu hình rules
"boundaries/element-types": [
2,
{
"default": "disallow",
"rules": [
{ "from": "entities", "allow": ["shared"] },
{ "from": "features", "allow": ["entities", "shared"] }
]
}
]
Bài học rút ra sau 6 tháng thực chiến
FSD không phải là “viên đạn bạc” cho mọi dự án. Với các app nhỏ chỉ có 2-3 màn hình, áp dụng FSD giống như dùng dao mổ trâu giết gà, gây tốn thời gian vô ích (over-engineering).
Tuy nhiên, với dự án lớn, hãy nhớ 3 điều: Đầu tư kỹ cho tầng shared vì nó là nền móng. Luôn có file ARCHITECTURE.md để hướng dẫn dev mới. Cuối cùng, đừng quá cứng nhắc, hãy linh hoạt điều chỉnh các tầng sao cho phù hợp với đặc thù business của bạn.
Áp dụng FSD có thể khiến bạn viết code chậm hơn một chút lúc đầu. Nhưng tin mình đi, một năm sau khi quay lại bảo trì, bạn sẽ thầm cảm ơn bản thân vì đã tổ chức code một cách ngăn nắp như vậy.

