Spring Modulith: Xây dựng Modular Monolith chuẩn chỉ trước khi nghĩ đến Microservices
- Published on
- /8 mins read/
Đừng vội chia nhỏ hệ thống thành Microservices chỉ vì thấy người ta làm vậy. Một Monolith có cấu trúc mô-đun vững chắc (Modular Monolith) luôn vượt trội hơn một hệ thống Distributed Monolith phân mảnh và hỗn loạn.
1. Nỗi đau muôn thuở: "Big Ball of Mud" và cám dỗ Microservices
Trong hầu hết các dự án Spring Boot lâu năm, kịch bản kinh điển luôn lặp lại:
- Dự án bắt đầu dưới dạng Monolith gọn gàng, team dev push tính năng nhanh chóng.
- Theo thời gian, code phình to. Các package bắt đầu import chéo lẫn nhau vô tội vạ (
OrderServicegọiInventoryRepository,PaymentServicequery thẳng bảngCustomer,...). - Ranh giới miền nghiệp vụ (Domain Boundaries) bị xóa nhòa. Một thay đổi nhỏ ở mô-đun khuyến mãi có thể làm sập quy trình thanh toán. Hệ thống biến thành một "Big Ball of Mud" (Cục bùn khổng lồ).
Đứng trước sự hỗn loạn này, phản xạ thường thấy của các kỹ sư là: "Hãy đập Monolith ra làm Microservices!". Nhưng thực tế tàn khốc là: Nếu bạn không thể giữ ranh giới sạch sẽ trong cùng một repository và một process, thì khi tách thành network services, bạn chỉ đang tạo ra một Distributed Monolith tệ hại hơn gấp 10 lần, kèm theo chi phí vận hành khổng lồ (k8s, network latency, distributed transaction, observability,...).
Đó chính là lý do Modular Monolith trở thành xu hướng kiến trúc chủ đạo trong những năm gần đây, và Spring Modulith chính là dự án chính thức từ VMware/Spring team để biến điều này thành hiện thực.
2. Spring Modulith là gì?
Spring Modulith (trước đây là dự án Moduliths của Oliver Drotbohm) là một extension chính thức cho Spring Boot 3.x, cung cấp:
- Xác định ranh giới mô-đun (Module Boundaries) thông qua cấu trúc package convention.
- Kiểm tra tính đóng gói (Encapsulation Verification): Chặn đứng tình trạng các class nội bộ của mô-đun này bị mô-đun khác import trái phép (thông qua ArchUnit).
- Giao tiếp bất đồng bộ qua Sự kiện miền (Domain Events): Tích hợp sẵn Event Publication Registry để đảm bảo transactional event giữa các mô-đun mà không sợ mất message.
- Tự động sinh tài liệu kiến trúc (Living Documentation): Export sơ đồ C4 Component, Mermaid diagrams và bảng phân tích phụ thuộc tự động từ mã nguồn.
3. Kiến trúc package chuẩn mực trong Spring Modulith
Spring Modulith quy ước mỗi package con nằm trực tiếp dưới root application package sẽ được coi là một Application Module.
com.tungdadev.ecommerce
├── EcommerceApplication.java
│
├── order <-- Module "order"
│ ├── Order.java (Public API của module)
│ ├── OrderService.java (Public API của module)
│ ├── OrderPlacedEvent.java (Public Domain Event)
│ └── internal <-- Package nội bộ (Mô-đun khác KHÔNG ĐƯỢC PHÉP gọi)
│ ├── OrderRepository.java
│ └── OrderCalculator.java
│
├── inventory <-- Module "inventory"
│ ├── InventoryService.java
│ └── internal
│ └── StockRepository.java
│
└── payment <-- Module "payment"
├── PaymentService.java
└── internal
└── PaymentGatewayClient.javaMặc định, các class nằm ở root của module (com.tungdadev.ecommerce.order) được coi là Public API. Mọi class nằm trong package con (internal hoặc bất kỳ sub-package nào khác) sẽ là Private Implementations và bị cấm truy cập từ các mô-đun khác!
4. Kiểm tra vi phạm kiến trúc bằng Unit Test
Điểm ăn tiền nhất của Spring Modulith là khả năng tự động fail CI/CD build ngay khi một developer vô tình import một class nội bộ từ mô-đun khác.
4.1. Thêm dependency
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>4.2. Viết Architecture Verification Test
Chỉ với đúng 5 dòng code test:
package com.tungdadev.ecommerce;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.docs.Documenter;
class ApplicationModularityTests {
ApplicationModules modules = ApplicationModules.of(EcommerceApplication.class);
@Test
void verifyModularity() {
// Tự động kiểm tra ranh giới, chu trình phụ thuộc (cyclic dependencies)
modules.verify();
}
@Test
void generateDocumentation() {
// Tự động xuất file Mermaid và PlantUML vào thư mục target
new Documenter(modules)
.writeDocumentation()
.writeModulesAsPlantUml();
}
}Nếu một kỹ sư trong team viết:
// Trong PaymentService (thuộc module payment):
import com.tungdadev.ecommerce.order.internal.OrderRepository; // <-- VI PHẠM!Khi chạy mvn test hoặc push lên Git:
org.springframework.modulith.core.Violations:
Module 'payment' depends on non-exposed type 'com.tungdadev.ecommerce.order.internal.OrderRepository' within module 'order'!Build sẽ fail ngay lập tức! Điều này giải quyết triệt để vấn đề kiến trúc bị thoái hóa theo thời gian.
5. Tách rời khớp nối bằng In-Process Domain Events
Thay vì OrderService tiêm trực tiếp PaymentService và InventoryService để gọi đồng bộ (tight coupling):
5.1. Phát sinh sự kiện miền trong Order Module
package com.tungdadev.ecommerce.order;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final ApplicationEventPublisher events;
public OrderService(ApplicationEventPublisher events) {
this.events = events;
}
@Transactional
public void placeOrder(CreateOrderRequest req) {
// 1. Logic nghiệp vụ tạo đơn hàng
Order order = new Order(req.customerId(), req.totalAmount());
// 2. Publish Domain Event
events.publishEvent(new OrderPlacedEvent(order.getId(), order.getTotalAmount()));
}
}5.2. Lắng nghe sự kiện ở Module khác với @ApplicationModuleListener
Spring Modulith cung cấp annotation @ApplicationModuleListener (kết hợp của @Async, @Transactional(propagation = REQUIRES_NEW), và @TransactionalEventListener(phase = AFTER_COMMIT)):
package com.tungdadev.ecommerce.payment;
import com.tungdadev.ecommerce.order.OrderPlacedEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;
@Component
public class PaymentEventListener {
@ApplicationModuleListener
public void on(OrderPlacedEvent event) {
// Xử lý tạo bill thanh toán bất đồng bộ
// Chỉ chạy sau khi transaction của Order đã COMMIT thành công!
System.out.println("Processing payment for order: " + event.orderId());
}
}6. Giải bài toán Transactional Event Publication Registry
Vấn đề muôn thuở của Event-Driven trong Monolith: Nếu Order đã commit vào Database, nhưng Event Listener xử lý thất bại (do crash máy bay, restart container, network out), thì message có bị mất không?
Spring Modulith tích hợp sẵn Event Publication Registry theo cơ chế Outbox:
- Khi event được publish, Spring Modulith lưu trạng thái event vào một bảng nội bộ trong DB (
event_publication). - Event listener xử lý xong thành công thì record được đánh dấu hoàn thành (
completion_date). - Nếu container crash giữa chừng, khi khởi động lại hoặc qua scheduled job, các event chưa hoàn thành sẽ được retry tự động!
Để bật tính năng này với JDBC/JPA, chỉ cần thêm dependency:
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-jdbc</artifactId>
<version>1.2.3</version>
</dependency>7. Tổng kết: Con đường chuẩn mực đi đến Microservices
| Tiêu chí | Big Monolith truyền thống | Modular Monolith (Spring Modulith) | Microservices |
|---|---|---|---|
| Ranh giới mô-đun | Mơ hồ, phụ thuộc chéo | Rõ ràng, kiểm duyệt tự động qua test | Rất nghiêm ngặt (Network boundary) |
| Độ trễ (Latency) | Cực thấp (In-memory calls) | Cực thấp (In-memory calls) | Cao (Network calls, Serializing JSON/gRPC) |
| Chi phí vận hành | Thấp | Thấp (1 CI/CD, 1 Deployment unit) | Rất cao (K8s, Istio, Tracing, Consul,...) |
| Tính toàn vẹn dữ liệu | ACID Transaction | ACID + Event Publication Registry | BASE, Eventual Consistency, Saga |
| Khả năng tách Microservice | Cực kỳ khó khăn | Rất dễ dàng (chỉ việc bóc module ra) | Đã là Microservices |
Lời khuyên thực chiến: Hãy luôn bắt đầu với Modular Monolith. Bằng cách áp dụng Spring Modulith, bạn có được sự linh hoạt và tốc độ phát triển của một Monolith, đồng thời sở hữu tính kỷ luật và ranh giới kiến trúc sạch sẽ của Microservices. Khi doanh nghiệp thực sự lớn mạnh và một mô-đun cần scale độc lập, việc tách nó thành một service riêng sẽ chỉ mất vài ngày thay vì hàng tháng trời đập đi xây lại.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.
On this page
- 1. Nỗi đau muôn thuở: "Big Ball of Mud" và cám dỗ Microservices
- 2. Spring Modulith là gì?
- 3. Kiến trúc package chuẩn mực trong Spring Modulith
- 4. Kiểm tra vi phạm kiến trúc bằng Unit Test
- 4.1. Thêm dependency
- 4.2. Viết Architecture Verification Test
- 5. Tách rời khớp nối bằng In-Process Domain Events
- 5.1. Phát sinh sự kiện miền trong Order Module
- 5.2. Lắng nghe sự kiện ở Module khác với @ApplicationModuleListener
- 6. Giải bài toán Transactional Event Publication Registry
- 7. Tổng kết: Con đường chuẩn mực đi đến Microservices