Identify and refactor architectural and domain-model design smells in Java and Spring applications, especially volatile derived state, primitive obsession, anemic domain models, feature envy, and temporal coupling. Use when reviewing code, refactoring legacy modules, designing entities/value objects, or when service classes feel too large and domain objects too passive.
Use this skill to review Java/Spring code through a senior domain-design lens. Favor immutability, single source of truth, and objects that protect their own invariants.
init() before parse(), load() before calculate(), or open() before use() without enforcement.| Smell | Detection trigger | Risk | Better path |
|---|---|---|---|
| Volatile derivative | Fields like age, totalPrice, itemCount, isOverdue, status derived from dates or child collections | Drift and desynchronization | Calculate from source-of-truth fields |
| Primitive obsession | String/Integer represent domain concepts such as email, phone, ZIP code, money | Validation leaks across layers | Introduce value objects with self-validation |
| Anemic domain model | Getters/setters dominate domain classes; services perform business rules | God services and weak invariants | Move behavior into domain types and expose business methods |
| Feature envy | A method uses another object's getters more than its own state | Wrong ownership and brittle coupling | Move calculation/decision to the data owner |
| Temporal coupling | Methods must be called in order but API does not enforce it | Invalid runtime states and hidden NPEs | Use constructors, builders, factories, or execute-around APIs |
Flag any stored field whose value can be recomputed from another field, the current date, or a child collection.
// Smelly: age drifts every year
record User(LocalDate birthDate, int age) {}
// Better: derive on demand
record User(LocalDate birthDate) {
int age() {
return Period.between(birthDate, LocalDate.now()).getYears();
}
}
Ask:
Prefer derived methods, projections, or query-time calculations over duplicated persisted state.
Flag primitive fields when the domain concept has rules, formatting, or behavior.
public record Email(String value) {
public Email {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
}
}
Prefer value objects for:
When reviewing, ask whether the type communicates meaning and protects itself from invalid input.
Flag classes that only expose state while services implement the real rules.
// Prefer behavior-rich methods over raw status mutation
order.cancel();
subscription.renewUntil(nextBillingDate);
invoice.markPaid(paymentReference);
Move logic into the domain object when the rule depends mostly on that object's own fields. Remove broad setters in favor of meaningful business operations.
Flag methods that repeatedly traverse another object's data to calculate a result.
// Smelly
BigDecimal total = order.getItems().stream()
.map(item -> item.getPrice().multiply(item.getQuantity()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
// Better
BigDecimal total = order.calculateTotal();
Prefer moving calculations, state transitions, and invariant checks to the type that owns the data being inspected.
Flag APIs that can be misused by calling methods in the wrong order.
// Smelly
parser.init();
parser.parse(input);
parser.cleanup();
Prefer:
Email, Money, and DateRange.if/switch blocks that vary behavior by type or status.new HeavyResource() inside domain or service constructors.When reporting findings:
Use concise findings in this form:
Smell: Volatile derivative
Why it matters: totalPrice can drift from line items during partial updates.
Better path: remove the field, compute from items, or centralize recalculation behind one business method.
jpa-patterns for fetch strategies, lazy loading, and query tuning.architecture-review for package boundaries, module direction, and layering.solid-principles or clean-code for broader OO refactoring beyond these domain smells.