Từ MVC đến N-Tier Architecture: Cách tiếp cận kiến trúc phần mềm cho sinh viên mới vào nghề.
MVC, N-Tier Architecture ưu nhược điểm và cách tiếp cận tối ưu.

Có một câu hỏi tôi vẫn nhớ từ thời còn đi học.
Thầy hỏi tôi: “Business Logic nằm ở đâu trong các tầng của ứng dụng?”
Tôi trả lời: “Ở người dùng.”
Thầy chắc cũng phải bất ngờ với câu trả lời đó.
Thực ra, lúc ấy tôi không hiểu Business Logic là gì. Tôi dịch sát nghĩa thành “logic kinh doanh”, rồi liên tưởng đến những hoạt động kinh doanh ngoài đời thực. Tôi nghĩ người dùng là người thực hiện nghiệp vụ, nên logic đó phải nằm ở phía người dùng.
Sau này đi làm, tiếp xúc với nhiều hệ thống hơn, tôi mới hiểu Business Logic là những quy tắc nghiệp vụ mà phần mềm phải thực hiện. Và trong một kiến trúc backend phổ biến, những quy tắc đó thường được tổ chức ở tầng Service hoặc các thành phần nghiệp vụ tương ứng.
Tôi kể chuyện này không phải để nói về một câu trả lời sai ngày xưa. Tôi muốn nói rằng có những thứ khi còn đi học, chúng ta tưởng mình đã hiểu vì thuộc được định nghĩa, nhưng phải đến khi thực sự xây dựng phần mềm, chúng ta mới hiểu nó giải quyết vấn đề gì.
Câu chuyện về MVC và N-Tier của tôi cũng bắt đầu như vậy.
1. Tôi bắt đầu bằng MVC, Express.js và Handlebars
Thời gian đầu làm web, tôi sử dụng Express.js kết hợp với Handlebars (HBS). Tôi tiếp cận MVC và xây dựng ứng dụng theo cách khá trực quan: có route, có controller, có model, có view.
Người dùng gửi request. Express nhận request. Controller xử lý. Model làm việc với dữ liệu. Handlebars render giao diện trả về trình duyệt.
Cứ thế mà làm.
Với một người mới học, tôi thấy cách này rất ổn. Nó giúp mình hiểu được một ứng dụng web thực sự vận hành như thế nào, thay vì chỉ biết viết một vài hàm JavaScript rồi chạy thử.
Tôi có thể xây dựng chức năng đăng nhập, quản lý sản phẩm, CRUD dữ liệu, rồi bổ sung những tính năng phức tạp hơn. Mỗi lần làm được một chức năng, tôi hiểu thêm một chút về cách các thành phần kết nối với nhau.
Ở giai đoạn đó, tôi không nghĩ quá nhiều về Clean Architecture, Dependency Injection hay những kiến trúc có nhiều tầng. Thực tế, nếu lúc ấy phải dựng một dự án với hàng loạt interface, repository, service và các lớp trừu tượng, có lẽ tôi chỉ biết làm theo hướng dẫn mà chưa chắc hiểu tại sao phải làm như vậy.
Nhìn lại, tôi nghĩ bắt đầu từ MVC là một lựa chọn hợp lý.
Khi mới học lập trình, điều quan trọng nhất không phải là viết code giống một dự án doanh nghiệp lớn. Điều quan trọng là hiểu được mình đang viết gì, tại sao nó hoạt động và dữ liệu đi qua hệ thống như thế nào.
Đừng để những thuật ngữ kiến trúc làm mình có cảm giác đang học những thứ cao siêu, trong khi một request HTTP đi từ trình duyệt đến database ra sao vẫn chưa thực sự nắm chắc.
2. Rồi đến lúc tôi nhận ra code chạy được chưa có nghĩa là code dễ phát triển
Khi ứng dụng còn nhỏ, việc đặt logic trong controller có thể chưa gây ra vấn đề lớn.
Nhưng thử tưởng tượng bạn đang làm một website bán hàng.
Ban đầu chỉ có chức năng hiển thị sản phẩm. Sau đó, khách hàng yêu cầu thêm giỏ hàng, đặt hàng, tính chiết khấu, kiểm tra tồn kho, thanh toán, hoàn tiền và quản lý trạng thái đơn hàng.
Mỗi lần thêm một tính năng, controller lại có thêm một ít logic.
Một thời gian sau, bạn mở lại file controller và thấy nó vừa kiểm tra dữ liệu đầu vào, vừa tính toán tiền, vừa kiểm tra điều kiện nghiệp vụ, vừa truy vấn database, vừa gửi email. Các nhánh điều kiện cứ nối tiếp nhau.
Lúc đầu, bạn vẫn có thể xử lý được. Nhưng khi cần sửa một quy tắc, bạn phải đọc lại một đống code để xác định thay đổi ở đâu, ảnh hưởng đến phần nào.
Đó là lúc vấn đề không còn nằm ở chuyện biết thêm cú pháp JavaScript hay sử dụng framework nào.
Vấn đề nằm ở cách tổ chức code.
Tôi nghĩ nhiều bạn mới vào nghề sẽ trải qua giai đoạn này. Ban đầu, chúng ta chỉ quan tâm làm sao để chức năng chạy được. Sau đó, khi phải sửa code cũ, thêm tính năng mới hoặc phối hợp với người khác, chúng ta mới nhận ra rằng cách tổ chức code cũng quan trọng không kém việc viết ra nó.
Đó là một trong những lý do tôi dần chuyển sang cách tổ chức backend theo N-Tier.
3. N-Tier giúp tôi nhìn code theo trách nhiệm, không chỉ theo chức năng
N-Tier Architecture chia ứng dụng thành các tầng có trách nhiệm tương đối rõ ràng. Với một backend thông thường, chúng ta có thể bắt đầu bằng ba thành phần chính:
- Controller: Nhận request, xử lý giao tiếp HTTP và điều phối yêu cầu.
- Service: Thực hiện hoặc điều phối nghiệp vụ của ứng dụng.
- Repository: Chịu trách nhiệm truy cập và thao tác với dữ liệu.
Database nằm phía dưới, còn frontend là nơi người dùng tương tác với hệ thống.
Ví dụ, khi khách hàng đặt hàng, controller không cần tự mình tính toán tất cả giá trị đơn hàng hay quyết định mọi điều kiện nghiệp vụ. Nó nhận yêu cầu và gọi OrderService.
OrderService xử lý quy trình đặt hàng, kiểm tra các điều kiện cần thiết và phối hợp những thành phần liên quan. Khi cần đọc hoặc ghi dữ liệu, nó sử dụng repository theo cách tổ chức của ứng dụng.
Có thể hình dung đơn giản như sau:
Request từ người dùng
|
v
Controller
|
v
OrderService
|
+------ Kiểm tra nghiệp vụ
|
+------ Tính toán đơn hàng
|
v
OrderRepository
|
v
Database
Đây là ví dụ minh họa, không phải một cấu trúc bắt buộc cho mọi dự án. Một ứng dụng nhỏ có thể không cần tách Repository riêng. Nhưng khi các trách nhiệm bắt đầu chồng chéo, việc phân tách đúng chỗ sẽ giúp code dễ hiểu và dễ thay đổi hơn.
Điều tôi thấy tiện nhất ở cách tổ chức này là khi quay lại đọc code, mình dễ xác định trách nhiệm của từng thành phần hơn.
Muốn xem API xử lý request thế nào, tìm controller. Muốn hiểu quy tắc đặt hàng, tìm service. Muốn kiểm tra cách truy vấn dữ liệu, tìm phần truy cập dữ liệu.
Không cần đọc toàn bộ hệ thống chỉ để hiểu một chức năng đang làm gì.
Và khi cần thay đổi nghiệp vụ, chúng ta có cơ hội giới hạn phạm vi thay đổi tốt hơn, thay vì để logic nằm rải rác khắp nơi.
Đối với tôi, đó mới là giá trị thực tế của việc phân tầng.
4. Business Logic không nằm ở người dùng như tôi từng nghĩ
Quay lại câu chuyện thời đi học.
Business Logic không phải là người dùng, cũng không đơn giản là bất cứ đoạn code nào chạy ở backend. Nó là những quy tắc xác định hệ thống phải hành xử như thế nào theo yêu cầu nghiệp vụ.
Lấy ví dụ một đơn hàng.
Người dùng nhấn nút đặt hàng. Đó là hành động khởi tạo yêu cầu.
Frontend có thể kiểm tra một số dữ liệu để phản hồi nhanh hơn, chẳng hạn trường bắt buộc không được để trống. Nhưng frontend không nên là nơi duy nhất quyết định đơn hàng có hợp lệ hay không, bởi request có thể được gửi trực tiếp đến backend mà không thông qua giao diện.
Backend phải thực hiện những kiểm tra có thẩm quyền, chẳng hạn:
- Sản phẩm còn đủ số lượng để bán không?
- Mức giảm giá có hợp lệ không?
- Tổng tiền cần thanh toán được tính như thế nào?
- Đơn hàng có đủ điều kiện chuyển sang trạng thái tiếp theo không?
Những quy tắc đó thuộc về logic nghiệp vụ.
Trong cách tổ chức N-Tier phổ biến, chúng thường được đặt trong Service hoặc các thành phần nghiệp vụ chuyên trách. Với những kiến trúc khác, chúng có thể được tổ chức ở Application Layer hoặc Domain Layer.
Không cần nhớ hết tên gọi ngay lập tức. Hãy nhớ bản chất: người dùng đưa ra yêu cầu, còn hệ thống phải thực thi những quy tắc đã được xác định để quyết định yêu cầu đó có hợp lệ và được xử lý như thế nào.
Khi hiểu được điều này, bạn sẽ bắt đầu đặt câu hỏi khác khi viết code: đoạn logic này thực sự thuộc trách nhiệm của thành phần nào?
Đó là một bước tiến trong tư duy thiết kế phần mềm.
5. Nhưng đừng hiểu rằng cứ nhiều tầng hơn là tốt hơn
Đến đây, có thể một số bạn sẽ nghĩ rằng nên áp dụng N-Tier cho tất cả dự án ngay từ đầu.
Tôi không nghĩ như vậy.
Nếu làm một website nhỏ, chỉ có vài chức năng CRUD, dữ liệu đơn giản và phạm vi gần như không thay đổi, việc tạo quá nhiều lớp có thể khiến dự án nặng nề hơn mức cần thiết.
Một hàm chỉ có vài dòng nhưng phải đi qua nhiều lớp trung gian, mỗi lớp chỉ gọi tiếp một lớp khác mà không tạo thêm giá trị, chưa chắc là thiết kế tốt.
Thêm một tầng đồng nghĩa với thêm một ranh giới cần hiểu, thêm cách tổ chức cần duy trì và đôi khi thêm cả chi phí kiểm thử, cấu hình.
Trong dự án nhỏ, tốc độ triển khai cũng là một yếu tố quan trọng. Không nên dành quá nhiều thời gian để xây dựng kiến trúc cho những vấn đề mà dự án chưa thực sự gặp phải.
Ngược lại, nếu ứng dụng có nhiều tính năng, quy tắc nghiệp vụ phức tạp, nhiều người cùng phát triển hoặc cần duy trì lâu dài, việc phân tách trách nhiệm sẽ ngày càng có giá trị.
Đặc biệt, khi nhiều chức năng cùng sử dụng một quy tắc nghiệp vụ, việc đặt quy tắc đó ở một nơi phù hợp giúp giảm lặp code và hạn chế tình trạng mỗi nơi triển khai một kiểu.
Vì vậy, tôi không xem MVC là thứ cần bỏ đi để chuyển sang N-Tier. MVC vẫn có thể được sử dụng cùng với Service và Repository. Hai khái niệm này không loại trừ nhau.
Điều cần thay đổi không nhất thiết là tên kiến trúc. Điều cần thay đổi là cách chúng ta tổ chức trách nhiệm khi hệ thống bắt đầu phức tạp hơn.
Đừng xây dựng kiến trúc phức tạp chỉ để chứng minh mình biết kiến trúc. Hãy xây dựng nó khi bạn hiểu vấn đề mà nó cần giải quyết.
6. Nếu đang là sinh viên, tôi khuyên bạn học theo cách này
Nếu mới học lập trình backend, tôi không khuyên bạn bắt đầu bằng một dự án có hàng chục tầng, interface và các mẫu thiết kế mà bản thân chưa hiểu rõ.
Hãy làm theo một lộ trình thực tế hơn.
Đầu tiên, xây dựng một ứng dụng MVC bằng Express.js.
Chọn một bài toán nhỏ: quản lý công việc, quản lý sản phẩm hoặc website đăng bài. Làm đầy đủ CRUD, đăng nhập, phân quyền cơ bản và kết nối database.
Đừng chỉ dừng lại ở việc chạy được. Hãy tự giải thích được request đi qua những thành phần nào, dữ liệu được lấy ở đâu và lỗi được xử lý ra sao.
Tiếp theo, cố tình làm một ứng dụng có nghiệp vụ phức tạp hơn.
Ví dụ, thêm chức năng đặt hàng, tính giá, áp dụng mã giảm giá và kiểm tra tồn kho. Khi controller bắt đầu phình to, hãy xem phần nào đang làm quá nhiều việc.
Đây là thời điểm thích hợp để tìm hiểu Service và Repository, bởi bạn đã có vấn đề thực tế để đối chiếu với kiến thức mới.
Cuối cùng, thử thay đổi một yêu cầu nghiệp vụ.
Chẳng hạn, thay đổi cách tính chiết khấu hoặc bổ sung một điều kiện khi hủy đơn hàng. Quan sát xem bạn phải sửa bao nhiêu nơi, việc kiểm thử khó đến mức nào và có làm ảnh hưởng đến các chức năng khác không.
Sau đó, thử tổ chức lại code và so sánh.
Bạn sẽ hiểu giá trị của kiến trúc tốt hơn nhiều so với việc chỉ đọc một bài viết liệt kê ưu điểm của N-Tier.
Khi đã vững những nền tảng này, bạn có thể tiếp tục học Dependency Injection, SOLID, Clean Architecture và những kỹ thuật thiết kế khác. Lúc đó, bạn sẽ hiểu chúng đang giải quyết vấn đề gì, thay vì chỉ biết rằng dự án chuyên nghiệp thường có những thứ đó.
Kết luận
Từ MVC với Express.js và Handlebars đến N-Tier, điều tôi học được không chỉ là cách chia code thành nhiều tầng.
Đó là quá trình chuyển từ suy nghĩ “làm sao để chức năng chạy được?” sang câu hỏi “làm sao để hệ thống có thể tiếp tục phát triển mà không trở nên khó kiểm soát?”.
Tôi từng không hiểu Business Logic là gì, từng trả lời sai một câu hỏi của thầy. Nhưng sau này, khi tiếp xúc với code và những bài toán thực tế, tôi mới dần hiểu được ý nghĩa của các khái niệm mình đã học.
Nếu bạn đang là sinh viên, đừng ngại việc mình chưa biết nhiều. Hãy làm những dự án nhỏ, tự giải quyết vấn đề và học từ chính những giới hạn mà bạn gặp phải.
Đừng vội chạy theo kiến trúc phức tạp. Nhưng cũng đừng chỉ dừng lại ở việc viết code miễn sao chạy được.
Học để viết được code. Làm dự án để hiểu vấn đề. Và khi hệ thống bắt đầu phức tạp, hãy học cách thiết kế để kiểm soát nó.
Đó là con đường tôi nghĩ có ích hơn cho một người muốn đi xa với nghề phát triển phần mềm.