Anemic Domain Model — Junior¶
What? An Anemic Domain Model is a design where the classes that represent your domain —
User,Order,Account,Invoice— are reduced to bags of fields with public getters and setters, and all the behaviour that should live on those classes is pushed out into separate "service" classes (UserService,OrderService,AccountService). The data and the logic that operates on the data are physically separated. Martin Fowler named this pattern in 2003 and called it an antipattern in object-oriented design. How? Spot it by looking at any class in your domain folder. If every field has agetX()andsetX(...)pair, and there is no method on the class that does anything with those fields beyond returning or replacing them, that class is anemic. The behaviour that should belong to it — validation, state transitions, calculations — is somewhere else, usually in a class namedXxxService,XxxManager, orXxxHelper.
1. The shape of the antipattern¶
Look at this domain class. It is the canonical anemic shape — the kind of thing you'll find in tutorials, generated by IDE wizards, and lurking in production codebases everywhere.
// User.java — a bag of fields. No behaviour.
public class User {
private Long id;
private String email;
private String passwordHash;
private boolean active;
private Instant createdAt;
private Instant lastLoginAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPasswordHash() { return passwordHash; }
public void setPasswordHash(String h) { this.passwordHash = h; }
public boolean isActive() { return active; }
public void setActive(boolean a) { this.active = a; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant t) { this.createdAt = t; }
public Instant getLastLoginAt() { return lastLoginAt; }
public void setLastLoginAt(Instant t) { this.lastLoginAt = t; }
}
Every behaviour about a user is then dragged into a "service":
// UserService.java — where all the actual logic lives
public class UserService {
private final UserRepository repo;
private final PasswordHasher hasher;
private final Clock clock;
public UserService(UserRepository repo, PasswordHasher hasher, Clock clock) {
this.repo = repo;
this.hasher = hasher;
this.clock = clock;
}
public User register(String email, String rawPassword) {
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("invalid email");
}
if (rawPassword == null || rawPassword.length() < 8) {
throw new IllegalArgumentException("password too short");
}
User u = new User();
u.setEmail(email.toLowerCase().trim());
u.setPasswordHash(hasher.hash(rawPassword));
u.setActive(true);
u.setCreatedAt(clock.instant());
return repo.save(u);
}
public void recordLogin(User u) {
if (!u.isActive()) {
throw new IllegalStateException("user disabled");
}
u.setLastLoginAt(clock.instant());
repo.save(u);
}
public void deactivate(User u) {
u.setActive(false);
repo.save(u);
}
}
The User class doesn't know how to register itself, doesn't know what makes its email valid, doesn't know what it means to be active or to record a login. Everything that defines what a user is, as a concept, lives somewhere else.
2. Why this is an antipattern in OOP¶
Object-oriented design has one defining promise: encapsulation. An object hides its data and exposes meaningful operations on that data. The point of putting email, passwordHash, and active together in one class is so that the rules about those fields — what makes them valid, how they change together — also live there. The anemic model breaks that promise:
- It is procedural code wearing OO clothes.
UserService.register(...)reads exactly like a function in a C program that takes aUserstruct, manipulates its fields, and returns it. Theclasskeyword onUseradds nothing —Useris a struct. - Invariants are unenforceable. Any caller that holds a
Userreference can callsetActive(true)on a user who was just deleted, or setemailtonull, or pushlastLoginAtinto the future. The class has no opinion about its own state. - Behaviour scatters.
User-related rules end up inUserService,UserValidator,UserMapper,UserActivator,UserAuditor. To understand what a user is, you read six files. To add a rule (say, "a user can't log in within 5 seconds of registration"), you have to find every place that touches login. - Tell, don't ask, is reversed. Code constantly asks a
Userfor its fields and then makes decisions outside. The OO style is to tell theUserto do something and let it decide.
Fowler's original wording (bliki: AnemicDomainModel, 2003) is blunt:
"The fundamental horror of this anti-pattern is that it's so contrary to the basic idea of object-oriented design; which is to combine data and process together."
Read his article in full — it's short. The pattern is so widespread that many developers have only ever seen it and don't realise an alternative exists.
3. The rich-model alternative, briefly¶
The fix is to move the behaviour back onto the class. The same User, written richly:
public final class User {
private final UserId id;
private Email email;
private PasswordHash passwordHash;
private boolean active;
private final Instant createdAt;
private Instant lastLoginAt;
private User(UserId id, Email email, PasswordHash hash, Instant createdAt) {
this.id = id;
this.email = email;
this.passwordHash = hash;
this.active = true;
this.createdAt = createdAt;
}
public static User register(Email email, PasswordHash hash, Clock clock) {
return new User(UserId.newId(), email, hash, clock.instant());
}
public void recordLogin(Clock clock) {
if (!active) throw new IllegalStateException("user disabled");
this.lastLoginAt = clock.instant();
}
public void deactivate() {
this.active = false;
}
public void changeEmail(Email newEmail) {
if (!active) throw new IllegalStateException("disabled user cannot change email");
this.email = newEmail;
}
public UserId id() { return id; }
public Email email() { return email; }
public boolean isActive() { return active; }
public Instant createdAt() { return createdAt; }
public Optional<Instant> lastLoginAt() { return Optional.ofNullable(lastLoginAt); }
}
Notice the differences:
- The constructor is
private. AUseronly enters the world viaregister(...), which guarantees a valid initial state. - There are no setters. State transitions happen through
recordLogin,deactivate,changeEmail— methods whose names say what they do in the domain. - The invariant "disabled users cannot change email" lives on the class. Any caller that holds a
Usercannot bypass it. EmailandPasswordHashare value objects — small immutable types that own their own validity (we'll meet them inmiddle.md).
The UserService shrinks dramatically — it becomes a thin coordinator between the repository and the rich User, not the place where user logic lives.
4. Where anemic shapes are not an antipattern¶
This is the part most tutorials skip. Anemic shapes are a problem in the core domain. Elsewhere, they are often correct.
- DTOs at the network boundary. A class whose only job is to be serialised to JSON for an HTTP response is supposed to be a bag of fields.
UserResponseDtohas no business logic — that's correct. - JPA/Hibernate persistence shapes in CRUD-heavy applications. If your "domain" is genuinely a CRUD form (e.g., an admin screen that edits a row), modelling it as an entity with rich behaviour adds noise. The smell scales with the complexity of the rules.
- Database read models / projections. A
CustomerSummaryrow used for a dashboard query is data — no behaviour belongs there. - Event payloads. A Kafka message is data in flight. Fields-and-getters is its job.
- Configuration objects. A
ServerPropertiescarrier loaded fromapplication.ymlis anemic by design.
The rule of thumb: anemic is correct at the edges (I/O, serialization, transport) and wrong at the centre (the core domain that holds the business rules). If you can't articulate a single domain rule the class enforces, that class either should be anemic (it's a DTO) or is missing the behaviour that justifies its existence (it's an anemic domain model).
5. How to spot it in a code review¶
Five questions to ask of any class in a domain/, model/, or entity/ package:
- Does this class have any method that isn't a getter or setter? If no, it's anemic.
- Could I change a field through a setter into an invalid value? If yes, the class isn't protecting its own invariants.
- Is there a class named
<ThisClass>Servicethat contains all the rules about this class's data? If yes, the rules are in the wrong place. - Does the constructor leave the object in a valid state? If you can
new User()and get an object with a null email, the answer is no. - If I read this class in isolation, can I understand what it does in the business? If you have to open the service file to find out, the class is anemic.
A "yes" to question 1 plus "no" to questions 4 and 5 is the signature shape. Three out of five is already a smell.
6. Common newcomer mistakes¶
Mistake 1: thinking the antipattern is about having getters. Getters are fine. The problem is having only getters and setters, with no behaviour. A rich Account class probably still has balance() (a getter); it just also has deposit(amount) and withdraw(amount).
Mistake 2: writing a "rich" model that exposes a setter for every field.
public class Account {
private BigDecimal balance;
public void setBalance(BigDecimal b) { this.balance = b; }
public void deposit(BigDecimal amt) { setBalance(balance.add(amt)); }
}
The setBalance exposes the same hole the anemic version had — any caller can bypass deposit and set balance to -1,000,000. Setters that mutate invariant-carrying state must be private or absent.
Mistake 3: treating "no setters" as the whole fix. Removing setters but keeping the logic in UserService doesn't help — the service still mutates User via its constructor or other escape hatches. The fix is both hiding mutation and moving the logic onto the class.
Mistake 4: applying rich-model thinking to a CRUD admin form. If the entire job of your Tag table is "create/edit/delete a label", a rich domain model is overkill. Choose the depth of modelling that matches the depth of the rules.
7. Quick rules¶
- Domain classes own their invariants. No setter that lets a caller put the object into an invalid state.
- Construct in a valid state. A constructor or static factory enforces every invariant before the object exists.
- Methods are domain verbs, not field assignments.
deposit(amount), notsetBalance(b). - Services orchestrate; they don't decide. The "what" lives on the entity; the service decides "when" and "in what order".
- Anemic shapes at boundaries are fine. DTOs, JSON payloads, event bodies, read projections — those are data, not domain.
8. What's next¶
| Topic | File |
|---|---|
| Fowler's full critique, JPA pressure to go anemic, first refactor | middle.md |
| Justified-vs-unjustified anemia, Tell-Don't-Ask, full rich example | senior.md |
| DDD aggregates, MapStruct, ArchUnit rules, CQRS read models | professional.md |
| Metrics, definitions, ArchUnit policy code | specification.md |
| 10 buggy snippets with diagnosis and fix | find-bug.md |
| JIT, escape analysis, dirty checking, value-object grouping | optimize.md |
| 8 refactoring exercises | tasks.md |
| 20 interview questions | interview.md |
Memorize this: Anemic Domain Model is when data lives in User and behaviour lives in UserService. That is procedural code in OO syntax. The cure is to put the behaviour back where the data already is — and to forbid every setter that would let a caller put the object into a state it should never be in.