Là lập trình viên, chúng ta có nhiều công cụ khác nhau để xây dựng phần mềm. Nếu lấy Java làm ví dụ, chúng ta có các method, khi có nhiều method liên quan, ta nhóm chúng lại thành class, các class này có thể được gom vào package, và các package đó có thể đóng gói thành module.
Kiến trúc phần mềm là cách mà tất cả các công cụ đó được liên kết và thiết lập mối quan hệ với nhau.
Trong bài viết này, chúng ta sẽ tìm hiểu các kiểu kiến trúc phần mềm phổ biến nhất, bao gồm monolith và microservices, sau đó sẽ giới thiệu thêm một kiểu kiến trúc trung gian giữa hai kiểu này gọi là modulith (modular monolith).
1. Ứng dụng monolithic là gì?
Trong kiến trúc monolith, toàn bộ mã nguồn được đóng gói và triển khai thành một khối duy nhất.
Dữ liệu của ứng dụng monolith thường được lưu trong một cơ sở dữ liệu duy nhất.
Đối với các ứng dụng nhỏ đến vừa, xử lý khối lượng dữ liệu ở mức trung bình, thì kiểu kiến trúc này vẫn rất thực tiễn và hiệu quả, với các ưu điểm như:
- Dễ phát triển: Lập trình viên dễ hiểu luồng xử lý của toàn bộ ứng dụng.
- Dễ refactor và debug: Nhờ vào các IDE hiện đại, việc sửa lỗi và cải tiến trở nên nhanh chóng.
- Độ trễ thấp: Các hàm gọi nhau trong cùng tiến trình, không phải qua mạng.
Hạn chế của monolith khi ứng dụng phát triển:
- Khó mở rộng hiệu quả: Khi một phần nào đó nhận nhiều lưu lượng, bạn buộc phải scale toàn bộ ứng dụng.
- Khó phân chia công việc giữa các nhóm: Các domain nghiệp vụ dễ bị ràng buộc với nhau, gây xung đột khi merge code.
- Khả năng chịu lỗi kém: Nếu một phần bị lỗi, toàn bộ ứng dụng có thể ngừng hoạt động.
2. Ứng dụng microservices là gì?
Trong kiến trúc microservices, mỗi thành phần nhỏ là một dịch vụ độc lập, chịu trách nhiệm riêng, chạy trên tiến trình riêng biệt và giao tiếp với nhau qua giao tiếp mạng.
Mỗi dịch vụ sẽ có cơ sở dữ liệu riêng.
Kiểu kiến trúc này được tạo ra để giải quyết các vấn đề của monolith khi ứng dụng trở nên quá lớn. Ưu điểm gồm:
- Mở rộng linh hoạt: Chỉ cần scale dịch vụ bị quá tải, không cần scale toàn bộ hệ thống.
- Phân tách rõ ràng: Mỗi service có trách nhiệm riêng, giúp dễ bảo trì.
- Tăng khả năng chịu lỗi: Một service lỗi không ảnh hưởng đến các service khác.
Nhưng microservices cũng không miễn phí:
- Độ trễ cao hơn: Do phải gọi mạng, có thể xảy ra lỗi kết nối, timeout...
- Phức tạp khi phát triển: Phải xử lý service discovery, đảm bảo dữ liệu nhất quán, logging/phân tích phân tán…
3. Microservices có tốt hơn monolith không?
Chúng ta thường có cái nhìn tiêu cực về monolith, trong khi lại rất “tôn sùng” microservices, phần lớn là do microservices được quảng bá bởi các tập đoàn công nghệ lớn.
Tuy nhiên, góc nhìn đó mang tính chủ quan.
Thực tế, monolith vẫn rất hữu ích khi:
- Có một đội nhỏ phát triển ứng dụng đơn giản
- Cần hiệu suất cao, nhanh chóng triển khai
- Không cần đối mặt với độ phức tạp của microservices
4. Modulith – Kiến trúc trung gian
Kiến trúc modular monolith (modulith) là kiểu giữa monolith và microservices: ứng dụng vẫn là một khối, nhưng được chia thành các module nghiệp vụ độc lập.
Tuy cùng trong một mã nguồn, các module này:
- Được tách biệt rõ ràng
- Giao tiếp qua API nội bộ hoặc sự kiện
- Không bị phụ thuộc lẫn nhau
Lưu ý: "module" ở đây là module nghiệp vụ, KHÔNG phải là Java module hay module của Maven/Gradle.
Lợi ích của modulith:
- Cấu trúc ứng dụng rõ ràng, dễ hiểu luồng xử lý
- Mã nguồn nằm ở một nơi – dễ debug, dễ refactor bằng IDE
- Không có độ trễ mạng – hiệu suất cao
- Phân tách trách nhiệm rõ ràng giữa các module
Hạn chế:
- Không có khả năng chống lỗi độc lập như microservices
- Không thể scale từng module riêng biệt
5. Cách triển khai modulith trong Java
Có 3 cách phổ biến để xây dựng modulith trong Java:
▸ Dùng package cho từng module nghiệp vụ
- Đơn giản nhất
- Mỗi module nằm trong một package riêng (ví dụ: com.myapp.orders, com.myapp.billing)
- Dễ triển khai, nhưng không có kiểm soát ràng buộc giữa các module
- Cần kỷ luật trong code review để tránh rối loạn cấu trúc
▸ Dùng Maven/Gradle multi-module
- Mỗi module nghiệp vụ là một project riêng trong Maven hoặc Gradle
- Mỗi module đóng gói thành JAR riêng
- Có cách ly compile-time, tránh phụ thuộc không kiểm soát
- Tuy nhiên tại runtime, vẫn có thể truy cập lẫn nhau qua reflection
▸ Dùng Java Module System (JPMS)
- Dùng từ Java 9 trở đi, định nghĩa module qua module-info.java
- Cách ly cả lúc biên dịch và runtime
- Tuy nhiên khó tích hợp với các framework hiện đại như Spring Boot
Kết luận
Không có kiến trúc nào là tốt nhất cho mọi trường hợp. Tùy vào quy mô, nhu cầu, năng lực đội ngũ, bạn có thể chọn:
- Monolith: Nhanh, đơn giản, dễ quản lý với dự án nhỏ
- Microservices: Linh hoạt, mở rộng tốt, phù hợp dự án lớn và phân tán
- Modulith: Cân bằng giữa hai bên, tách biệt logic mà vẫn giữ mọi thứ trong một mã nguồn