Khung Kiến Trúc Chuẩn — Modular Monolith (Datadict Backend)¶
Trạng thái: Active — Bắt buộc tuân thủ Ngày tạo: 2026-04-16 Áp dụng cho: Tất cả agents (backend-engineer, task-executor, code-reviewer) Tài liệu gốc: ADR-002-modular-monolith.md, docs/notes/2026-04-16-spring-modulith-shared-module.md
1. Tổng Quan¶
Datadict Backend là Modular Monolith gồm 11 modules quản lý bởi Spring Modulith 2.0.5.
vn.greendata.datadict/
├── DatadictApplication.java ← @SpringBootApplication
├── shared/ ← OPEN Module (shared kernel)
├── auth/ ← Keycloak integration
├── admin/ ← User & Org management
├── anchored/ ← Domain/SubDomain/DataElement
├── discovery/ ← File upload & field extraction
├── matching/ ← AI matching orchestration
├── mapping/ ← Bảng C, DISPUTED workflow
├── confirm/ ← Data Owner review
├── planning/ ← Bảng D, publication
├── dictionary/ ← Full-text search, portal
└── report/ ← Dashboard, reports
Quy tắc vàng:
Code không nằm trong 11 modules này = Orphan code = Vi phạm kiến trúc. Spring Modulith KHÔNG enforce boundary với code nằm ngoài 11 modules.
2. Cấu Trúc Thư Mục Chuẩn (Module Template)¶
2.1 Module thông thường (admin, anchored, discovery, ...)¶
vn.greendata.datadict.{module}/
├── package-info.java ← BẮT BUỘC: @ApplicationModule config
│
├── api/ ← PUBLIC: Exposed to other modules
│ ├── {Module}Service.java ← Interface: contracts cho operations
│ └── event/ ← Domain events (publish/subscribe)
│ └── {Entity}CreatedEvent.java
│
├── controller/ ← INTERNAL: HTTP adapters (@RestController)
│ └── {Entity}Controller.java
│
├── domain/ ← INTERNAL: Business entities
│ └── {Entity}.java ← @Entity, extends BaseEntity<Long>
│
├── repository/ ← INTERNAL: Data access
│ └── {Entity}Repository.java ← extends JpaRepository<{Entity}, Long>
│
├── service/ ← INTERNAL: Business logic
│ └── {Entity}ServiceImpl.java← implements api/{Module}Service
│
├── dto/ ← SEMI-PUBLIC: Request/Response contracts
│ ├── {Entity}Request.java
│ └── {Entity}Response.java
│
└── mapper/ ← INTERNAL: Object conversion
└── {Entity}Mapper.java
2.2 Module shared (Open Module)¶
vn.greendata.datadict.shared/
├── package-info.java ← @ApplicationModule(type = Type.OPEN)
├── domain/ ← BaseEntity<I>, Ownable, Identifiable
├── security/ ← PBAC kernel, SecurityUtils, DatadictUserPrincipal
│ UserIdentityService (interface - impl ở admin)
├── audit/ ← AuditService, AuditLogEntry, AuditAction
├── exception/ ← BaseApiException, custom exceptions, ErrorCode
├── web/ ← ApiResponse<T>, PagedResponse<T>, HealthController
├── config/ ← Spring config beans (Audit, OpenAPI, CORS)
├── middleware/ ← Servlet filters (RateLimit, RequestLogging)
└── utils/ ← UUIDv7, common utilities
3. Quy Tắc package-info.java (BẮT BUỘC)¶
3.1 shared module¶
// vn/greendata/datadict/shared/package-info.java
@org.springframework.modulith.ApplicationModule(
type = org.springframework.modulith.ApplicationModule.Type.OPEN,
displayName = "Shared"
)
package vn.greendata.datadict.shared;
3.2 Các module thông thường¶
// Ví dụ: vn/greendata/datadict/admin/package-info.java
@org.springframework.modulith.ApplicationModule(
displayName = "Admin",
allowedDependencies = { "shared", "auth" }
)
package vn.greendata.datadict.admin;
3.3 Bảng allowedDependencies đầy đủ¶
| Module | allowedDependencies |
|---|---|
shared |
— (OPEN, không khai báo allowed) |
auth |
"shared" |
admin |
"shared", "auth" |
anchored |
"shared", "auth", "admin" |
discovery |
"shared", "auth", "anchored" |
matching |
"shared", "auth", "discovery", "anchored" |
mapping |
"shared", "auth", "matching", "anchored" |
confirm |
"shared", "auth", "mapping" |
planning |
"shared", "auth", "confirm" |
dictionary |
"shared", "auth", "planning", "anchored" |
report |
"shared", "auth", "mapping", "planning", "anchored", "admin" |
4. Quy Tắc Cross-Module Import¶
4.1 ĐƯỢC PHÉP import từ module khác¶
// Được: Import từ api/ package của module B
import vn.greendata.datadict.anchored.api.AnchoredService;
import vn.greendata.datadict.anchored.api.event.DomainCreatedEvent;
// Được: Import dto/ của module B (nếu cần reference data)
import vn.greendata.datadict.anchored.dto.DataElementResponse;
// Được: Import bất kỳ class nào từ shared (vì Type.OPEN)
import vn.greendata.datadict.shared.security.DatadictUserPrincipal;
import vn.greendata.datadict.shared.domain.BaseEntity;
import vn.greendata.datadict.shared.audit.AuditService;
4.2 TUYỆT ĐỐI KHÔNG import từ module khác¶
// SAI: Import domain entity của module khác
import vn.greendata.datadict.anchored.domain.DataElement; // ❌
// SAI: Import repository của module khác
import vn.greendata.datadict.anchored.repository.DataElementRepository; // ❌
// SAI: Import service implementation của module khác
import vn.greendata.datadict.anchored.service.DataElementServiceImpl; // ❌
// SAI: Import mapper của module khác
import vn.greendata.datadict.anchored.mapper.DataElementMapper; // ❌
// SAI: Import controller của module khác
import vn.greendata.datadict.anchored.controller.DataElementController; // ❌
4.3 Quy tắc Export Sub-packages (Kỹ thuật @NamedInterface)¶
Mặc định, Spring Modulith chỉ export các class nằm trực tiếp trong package api/. Để export các sub-package (như api.event), bắt buộc sử dụng kỹ thuật gán nhãn giao diện định danh:
- Tạo file
package-info.javabên trong sub-package cần export (ví dụ:.../{module}/api/event/package-info.java). - Sử dụng annotation
@org.springframework.modulith.NamedInterfaceđể gán package này vào interface chung của module.
Ví dụ cấu hình:
@org.springframework.modulith.NamedInterface("api")
package vn.greendata.datadict.{module}.api.event;
Lợi ích: Module khác chỉ cần khai báo phụ thuộc vào {module} :: api là có thể thấy cả class trong api/ gốc và api/event/. Quy tắc này áp dụng tương tự cho dto nếu cần chia sub-package.
5. Dependency Inversion Pattern (Cross-Module)¶
Vấn đề: Module upstream cần data từ downstream¶
Ví dụ: auth cần lookup user từ admin, nhưng admin đã phụ thuộc auth.
Thêm admin vào allowedDependencies của auth = CIRCULAR DEPENDENCY = SAI.
Giải pháp: Interface ở shared, Implementation ở downstream¶
Bước 1: Định nghĩa interface trong shared module
shared/security/UserIdentityService.java
→ Interface với method signature cần thiết
Bước 2: Implement ở module downstream (admin)
admin/service/UserIdentityServiceImpl.java
→ @Service, implements UserIdentityService
→ Có thể dùng admin.repository, admin.domain
Bước 3: Module upstream (auth) inject interface
auth/security/CustomJwtAuthenticationConverter.java
→ private final UserIdentityService userIdentityService;
→ Spring DI inject UserIdentityServiceImpl tự động
Kết quả:
- auth → phụ thuộc shared (interface) ✓
- admin → phụ thuộc shared (interface để implement) ✓
- KHÔNG có circular: auth không phụ thuộc admin ✓
6. Event-Driven Communication¶
Khi nào dùng Events (thay vì direct API call)¶
- Module A cần notify Module B sau một operation, nhưng B không trong allowedDependencies của A
- Side effects không cần đồng bộ (notifications, audit, cache invalidation)
- Tránh circular dependency không giải quyết được bằng Dependency Inversion
Pattern¶
// 1. Define event record (trong module publish hoặc shared)
public record UserStatusChangedEvent(Long userId, String newStatus) {}
// 2. Publish event (trong module A)
@Service
@RequiredArgsConstructor
public class SomeServiceImpl {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void changeStatus(Long userId, String status) {
// ... business logic ...
eventPublisher.publishEvent(new UserStatusChangedEvent(userId, status));
}
}
// 3. Listen in module B (không cần phụ thuộc A)
@Service
public class NotificationService {
@ApplicationModuleListener
void on(UserStatusChangedEvent event) {
// xử lý side effect
}
}
7. BaseEntity — Quy Tắc Sử Dụng¶
Tất cả @Entity trong các module (trừ shared) phải extends BaseEntity<I>:
// Master data tables (user, org, domain, ...) — dùng Long
@Entity
@Table(name = "app_user")
public class AppUserEntity extends BaseEntity<Long> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Override public Long getId() { return id; }
@Override public void setId(Long id) { this.id = id; }
}
// Delegation/junction tables (UserRole, audit) — dùng composite PK hoặc không extends
// AuditLogEntry KHÔNG extends BaseEntity (composite PK, no updatedAt)
Hybrid PK Strategy:
- Long (BIGSERIAL) → master data tables: app_user, org_unit, domain, data_element, ...
- UUID (UUIDv7) → delegation tables, audit: user_role, audit_log, ...
- @IdClass composite PK → partitioned tables (audit_log)
8. Annotation Checklist¶
Entity¶
@Entity
@Table(name = "table_name", schema = "public")
@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
public class SomeEntity extends BaseEntity<Long> {
// @Data bị cấm — gây equals/hashCode conflict với @Entity
}
Service Implementation¶
@Slf4j
@Service // BẮT BUỘC cho ServiceImpl
@Transactional // Default transactional behavior
@RequiredArgsConstructor
public class SomeServiceImpl implements SomeService {
// inject interfaces, không inject implementations
}
Repository¶
@Repository
public interface SomeRepository extends JpaRepository<SomeEntity, Long> {
// custom queries nếu cần
}
Controller¶
@RestController
@RequestMapping("/api/v1/{module}")
@RequiredArgsConstructor
public class SomeController {
private final SomeService service; // inject interface từ api/
// KHÔNG inject Repository trực tiếp
}
9. Kiểm Thử (Testing Requirements)¶
9.1 ModulithVerificationTest (BẮT BUỘC cho mọi PR)¶
@SpringBootTest
class ModulithVerificationTest {
@Test
void verifyModularStructure() {
ApplicationModules modules = ApplicationModules.of(DatadictApplication.class);
modules.verify(); // fail = vi phạm allowedDependencies
}
}
9.2 ArchUnit Tests (Kiểm tra conventions)¶
File: ArchitectureTest.java — 3 rules hiện tại, KHÔNG thêm rule cross-module:
| Rule | Mục Đích |
|---|---|
services_should_be_annotated_with_service |
ServiceImpl phải có @Service |
controllers_should_not_access_repositories_directly |
Controller không bypass service layer |
entities_should_be_in_domain_package |
@Entity nằm trong domain/ hoặc audit/ |
LƯU Ý: KHÔNG viết ArchUnit rule
no_cross_module_internal_access— Spring Modulith làm việc này chính xác hơn, ArchUnit dễ tạo false positive.
10. Workflow Khi Tạo Module Mới¶
1. package-info.java — @ApplicationModule(allowedDependencies = {...})
Dựa vào Bảng allowedDependencies ở Mục 3.3
2. domain/{Entity}.java — @Entity extends BaseEntity<Long>
3. repository/{Entity}Repository.java — extends JpaRepository
4. api/{Module}Service.java — Interface: public service contract
5. service/{Entity}ServiceImpl.java — @Service, implements api/{Module}Service
6. dto/{Entity}Request.java, {Entity}Response.java
7. mapper/{Entity}Mapper.java — domain <-> dto conversion
8. controller/{Entity}Controller.java — HTTP REST endpoints
9. Chạy ModulithVerificationTest → xác nhận modules.verify() pass
10. Chạy ArchitectureTest → xác nhận 3 structural rules pass
11. Lỗi Thường Gặp & Cách Sửa¶
"Module X depends on Y which is not in allowedDependencies"¶
Nguyên nhân: Module X import class từ module Y không được phép
Sửa: Xóa import, dùng api/ interface của Y thay vì direct class
Hoặc thêm Y vào allowedDependencies của X nếu đúng thiết kế
"Class Z in package X.internal is not accessible from Y"¶
Nguyên nhân: Module Y import internal class của module X
Sửa: Module X phải expose class Z qua X.api/ package
Spring Modulith verify fail với shared sub-packages¶
Nguyên nhân: shared module cấu hình CLOSED (mặc định), sub-packages bị ẩn
Sửa: Đảm bảo shared/package-info.java có Type.OPEN
KHÔNG flatten code lên root shared package
Circular Dependency giữa 2 modules¶
Nguyên nhân: A phụ thuộc B, B phụ thuộc A
Sửa A: Dùng Dependency Inversion (interface ở shared, impl ở downstream)
Sửa B: Dùng Event-driven (publish event, không gọi trực tiếp)
ArchUnit false positive — "no_cross_module_internal_access"¶
Nguyên nhân: Rule ArchUnit không hiểu module boundary như Spring Modulith
Sửa: XÓA rule này khỏi ArchitectureTest, giao cho Spring Modulith verify
Tài Liệu Liên Quan¶
- ADR-002:
context/technical/03-decisions/ADR-002-modular-monolith.md - Dev Note:
docs/notes/2026-04-16-spring-modulith-shared-module.md - TASK-007:
tasks/items/TASK-007-module-skeleton.md - Spring Modulith docs: https://docs.spring.io/spring-modulith/reference/fundamentals.html
- ArchUnit docs: https://www.archunit.org/userguide/html/000_Index.html