ValidX in DDD: Layered Validation for Domain Models
This article demonstrates how ValidX, a JSR-380-compliant validation library with 100+ Chinese business validators, integrates into DDD layered architecture, showing annotation-based DTO validation, chain-style API for value objects, aggregate roots, and domain services, with code examples for each layer and best practices for avoiding common pitfalls.
Why Validation Is Hard in DDD
In traditional three-layer architecture (Controller → Service → DAO), validation logic is scattered: DTO format validation in the controller with @Valid, manual if-else business rules in the service, and database constraints in the DAO. This leads to duplicated rules, anemic domain models, repeated boilerplate for Chinese-specific formats (ID card, phone, bank card), and high maintenance cost.
Key Principle: Validation logic should stay as close to the domain model as possible. Format validation (phone, ID card) belongs in value objects; business rules (amount > 0) in aggregate roots; input format in DTOs; cross-aggregate rules in domain services.
Why Choose ValidX?
ValidX is built on JSR-380 (Bean Validation 2.0) and provides:
100+ ready-to-use Chinese business validators (ID card, phone, bank card, unified social credit code, license plate, etc.)
Two usage styles: annotation-based (for DTOs/value objects) and chain-style API (for aggregate roots/domain services)
Built-in Chinese error messages (8 languages supported)
Zero-configuration Spring Boot integration
Compared to standard Bean Validation (only generic annotations) and custom validation (reinventing wheels), ValidX combines standard compatibility with domain-specific validators.
DDD Layered Validation Architecture
Classic DDD Layers
┌─────────────────────────────────────┐
│ User Interface Layer (Interfaces) │
│ Controller, DTO, @Valid validation │
├─────────────────────────────────────┤
│ Application Layer │
│ Application services, commands, tx │
├─────────────────────────────────────┤
│ Domain Layer │
│ Aggregate roots, entities, VOs, │
│ domain services ★ business rules ★ │
├─────────────────────────────────────┤
│ Infrastructure Layer │
│ Repository impl, MQ, external svcs │
└─────────────────────────────────────┘Validation Responsibilities per Layer
User Interface Layer: DTO format validation (required, format) – annotation style ( @ChinesePhone, @Valid)
Application Layer: Command object validation, cross-aggregate pre-checks – chain API ( ValidX.init())
Domain Layer – Value Objects: Self-contained invariants (phone format, amount > 0) – constructor chain validation
Domain Layer – Aggregate Roots: Aggregate business rules (state transitions, invariants) – method chain validation
Domain Layer – Domain Services: Cross-aggregate rules – chain API
Infrastructure Layer: Generally no validation (database constraints as last resort)
Core Principles: Format validation (phone, ID card) sinks into value objects Business rules (amount > 0, state legality) cohere in aggregate roots Input format (DTO required fields) handled at interface layer as first line of defense Cross-aggregate rules handled in domain services
Value Objects: Enforcing Invariants with ValidX
Traditional Problem
Value objects like PhoneNumber often embed hand-written regex, leading to copy-paste errors, inconsistent messages, and no support for international formats.
ValidX Solution
Use chain-style API in the constructor:
public class PhoneNumber {
private final String value;
public PhoneNumber(String value) {
ValidX validator = ValidX.init()
.config(ValidXConfig.GLOBAL_NOT_EMPTY)
.field("手机号")
.isChinesePhone(value);
if (!validator.passed()) {
throw new DomainValidationException(validator.getErrors());
}
this.value = value;
}
// equals/hashCode based on value
}More examples:
IdCard: .isChineseIdCard(value) – automatically validates checksum
Money: combines isTrue(amount > 0) and currency code length check
Email: .isEmail(value) Benefits: invariants encapsulated, illegal instances impossible, reusable across project, unified error messages.
Aggregate Roots: Business Rule Validation with ValidX
Order Aggregate Example
An Order aggregate root contains PhoneNumber, IdCard, Money, and a list of OrderItem. Business methods create(), pay(), cancel() each validate preconditions:
public void create() {
ValidX validator = ValidX.init()
.config(ValidXConfig.GLOBAL_NOT_NULL)
.field("订单项")
.isTrue(items != null && !items.isEmpty(), "订单项不能为空")
.field("总金额")
.isTrue(totalAmount != null, "总金额不能为空")
.field("金额一致性")
.isTrue(isAmountConsistent(), "总金额必须等于各订单项之和");
if (!validator.passed()) {
throw new DomainValidationException(validator.getErrors());
}
this.status = OrderStatus.UNPAID;
}
public void pay(Money payAmount) {
ValidX validator = ValidX.init()
.config(ValidXConfig.GLOBAL_NOT_NULL)
.field("订单状态")
.isTrue(status == OrderStatus.UNPAID, "只有待支付订单才能支付")
.field("支付金额")
.isTrue(payAmount != null, "支付金额不能为空")
.field("金额一致性")
.isTrue(payAmount.equals(totalAmount), "支付金额必须等于订单金额");
if (!validator.passed()) {
throw new DomainValidationException(validator.getErrors());
}
this.status = OrderStatus.PAID;
}
private boolean isAmountConsistent() {
if (items == null || items.isEmpty() || totalAmount == null) return false;
BigDecimal sum = items.stream()
.map(item -> item.getPrice().getAmount()
.multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.compareTo(totalAmount.getAmount()) == 0;
}Advantages: business rules co-located, clear state machine, illegal states prevented, highly testable.
Application Layer: DTO Annotation Validation + Command Validation
DTO Validation (Interface Layer)
Use ValidX annotations on request DTOs:
public class CreateOrderRequest {
@NotBlank(message = "客户手机号不能为空")
@ChinesePhone(message = "客户手机号格式不正确")
private String customerPhone;
@ChineseIdCard(message = "身份证号格式不正确")
private String customerIdCard;
@NotEmpty(message = "订单项不能为空")
private List<OrderItemRequest> items;
}Controller uses @Valid to trigger automatic validation and return 400 on failure.
Command Validation (Application Layer)
Command objects encapsulate use-case input and can perform more complex checks via chain API:
public class CreateOrderCommand {
private String customerPhone;
private String customerIdCard;
private List<OrderItemCommand> items;
public void validate() {
ValidX validator = ValidX.init()
.config(ValidXConfig.GLOBAL_NOT_EMPTY)
.field("客户手机号").isChinesePhone(customerPhone)
.field("身份证号").isChineseIdCard(customerIdCard)
.field("订单项").isTrue(items != null && !items.isEmpty(), "订单项不能为空");
if (!validator.passed()) {
throw new ApplicationValidationException(validator.getErrors());
}
// validate each item
for (int i = 0; i < items.size(); i++) {
OrderItemCommand item = items.get(i);
ValidX itemValidator = ValidX.init()
.config(ValidXConfig.GLOBAL_NOT_NULL)
.field("订单项[" + i + "]商品ID").isTrue(item.getProductId() != null, "商品ID不能为空")
.field("订单项[" + i + "]数量").isTrue(item.getQuantity() != null && item.getQuantity() > 0, "数量必须大于0");
if (!itemValidator.passed()) {
throw new ApplicationValidationException(itemValidator.getErrors());
}
}
}
}Three-Layer Validation Synergy
Frontend Input
↓
[Layer 1] DTO Annotation Validation (@Valid)
Format, required, length
↓
[Layer 2] Command Validation (command.validate())
Complex use-case pre-checks
↓
[Layer 3] Value Object + Aggregate Root Validation (Domain)
Invariants, business rules, state transitions
↓
PersistenceWhy Three Layers? DTO validation: fast fail, dirty requests never reach application layer Command validation: application-level use-case preconditions Domain validation: final guard for domain model integrity Layers do not duplicate; each has distinct focus
Domain Services: Cross-Aggregate Validation
When a rule spans multiple aggregates (e.g., order placement checks product stock and user status), place validation in a domain service:
@DomainService
public class OrderDomainService {
@Autowired private ProductRepository productRepository;
@Autowired private UserRepository userRepository;
public void validateOrder(Order order, Long userId) {
ValidX validator = ValidX.init();
// 1. Check user exists and active
User user = userRepository.findById(userId);
validator.field("用户")
.isTrue(user != null, "用户不存在")
.isTrue(user != null && user.isActive(), "用户已被禁用");
// 2. Check each product stock
for (OrderItem item : order.getItems()) {
Product product = productRepository.findById(item.getProductId());
validator.field("商品[" + item.getProductId() + "]")
.isTrue(product != null, "商品不存在")
.isTrue(product != null && product.getStock() >= item.getQuantity(),
"商品库存不足");
}
if (!validator.passed()) {
throw new DomainValidationException(validator.getErrors());
}
}
}Summary & Best Practices
ValidX Overview: JSR-380 based, 100+ Chinese validators, annotation + chain API, zero-config Spring Boot.
DDD Layered Validation: Interface (DTO annotations), Application (command chain), Domain VOs (constructor chain), Domain Aggregates (method chain), Domain Services (cross-aggregate chain).
Value Object Validation: Constructor validation, fail-fast, reusable PhoneNumber, IdCard, Email.
Aggregate Root Validation: Business rules in methods, state machine enforcement, aggregate invariants (total = sum of items).
Common Pitfalls:
ValidX instances are not thread-safe – create new instance per validation
Chain API allows null by default – explicitly configure required fields
Three-layer validation should not duplicate; each layer has its own concern
Define custom exception hierarchy to distinguish validation errors from business exceptions
DDD's essence is "keeping code structure aligned with the business model". Validation logic is no exception. Business rules should not wander in service-layer if-else chains; they belong in the domain model. ValidX provides elegant tooling to make domain validation simple, consistent, and reusable. With ValidX, value object invariants and aggregate root business rules can be cohesively encapsulated.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
