Even when a team says it applies DDD, domain objects often do almost nothing in real code. Class names look domain-like, such as Subscription, Order, and Payment, but inside they only have fields, getters, and setters. Most important rules live in service classes such as SubscriptionService, OrderService, and PaymentService.
From the outside, the layers are separated, there is a domain package, and Entities exist. But domain objects do not judge anything by themselves. They only hold state.
This kind of model is often called an anemic domain model.
Putting behavior into domain objects in DDD does not simply mean creating many methods. It means letting domain objects know their own rules and express meaningful behavior.
State change is not behavior
Think about a feature that cancels a subscription. A common implementation looks like this:
public void cancelSubscription(Long subscriptionId) {
Subscription subscription = repository.findById(subscriptionId);
if (subscription.getStatus() == SubscriptionStatus.EXPIRED) {
throw new IllegalStateException("The subscription is already expired.");
}
subscription.setStatus(SubscriptionStatus.CANCELED);
subscription.setCanceledAt(Instant.now());
repository.save(subscription);
}
This code works. But the domain rule is inside the Application Service. Subscription does not know whether its state is expired, whether it can be canceled, or what values should change when it is canceled.
In this structure, the same rule is likely to be repeated in many places. A feature where the user cancels directly, a feature where an admin terminates a subscription, and a batch that automatically ends subscriptions after payment failure may each have similar state-changing code.
A better way is to ask the subscription object to perform the behavior.
public void cancelSubscription(SubscriptionId subscriptionId, CancelReason reason) {
Subscription subscription = repository.findById(subscriptionId);
subscription.cancelByMember(reason, Instant.now());
repository.save(subscription);
}
Here, the Application Service coordinates the flow. It loads the subscription, asks the subscription to cancel itself, and saves it. The subscription object is responsible for judging whether cancellation is allowed and what state it should change to.
Method names should be domain language
When putting behavior into domain objects, method names matter. Hiding setters alone does not create a good model.
subscription.updateStatus(CANCELED);
This code may look a little better than setStatus, but it is still not domain behavior. Updating status is a technical expression. Domain experts do not say, “Update the status to CANCELED.” They say, “Cancel the subscription,” “End the free trial,” or “Enter a grace period because payment failed.”
Method names should follow the words actually used in the domain.
subscription.cancelByMember(reason, now);
subscription.startGracePeriod(paymentFailure, now);
subscription.expire(now);
subscription.renew(payment, now);
subscription.changePlan(targetPlan, policy, now);
These names contain more meaning than state changes. They reveal “who,” “why,” and “under what condition” the event happened.
A good domain method makes the calling code read like a domain scenario.
subscription.changePlan(targetPlan, planChangePolicy, now);
subscription.publishEvents(eventPublisher);
Of course, this does not mean every method must read beautifully. But important domain behavior should look important in code too.
Why objects should protect rules
When domain rules exist only in the service layer, bypass paths appear. The more code modifies the same Entity, the greater the risk becomes.
Suppose there is a rule that an expired subscription cannot change plans. If this rule exists only in an Application Service, it can be missed in another use case.
if (subscription.getStatus() != EXPIRED) {
subscription.setPlanId(targetPlanId);
}
But if the subscription object protects the rule by itself, the same rule applies no matter which path calls it.
public void changePlan(Plan targetPlan, PlanChangePolicy policy, Instant now) {
if (this.isExpired()) {
throw new CannotChangePlanException("An expired subscription cannot change plans.");
}
PlanChangeResult result = policy.calculate(this, targetPlan, now);
this.planId = targetPlan.id();
this.period = result.nextPeriod();
}
This code does not mean the Entity must know everything alone. A complex policy such as calculating the payment amount for a plan change can be handled by PlanChangePolicy. But a core rule about the subscription’s own state, such as “an expired subscription cannot be changed,” is naturally protected by the subscription.
The model becomes stable when domain objects protect themselves.
Anemic models produce procedural code
When domain objects have no behavior, the service layer grows larger and larger. Every use case repeats this pattern:
1. Load data.
2. Check state.
3. Judge rules with conditionals.
4. Change values with setters.
5. Save.
At first, this is simple. But as domain rules increase, service methods become procedural scripts. Objects become structures that hold data, and services become giant functions responsible for every judgment.
In this structure, reading the model does not help us understand the domain. Even if we open the Subscription class, we cannot tell what a subscription can do. The real rules are scattered across services, batches, and event handlers.
DDD wants the opposite direction. By looking at a domain object, we should be able to roughly understand what responsibility it has, what behavior it can perform, and what rules it protects.
subscription.renew(paymentResult, now);
subscription.cancelByMember(reason, now);
subscription.startGracePeriod(failureReason, now);
subscription.expire(now);
This list of methods explains the lifecycle of a subscription. That is what it means to put behavior into a domain object.
This does not mean putting all logic into Entity
Putting behavior into domain objects does not mean pushing every piece of logic into one Entity. That creates another problem. The Entity becomes too large, gains many external dependencies, and becomes hard to test.
What matters is where responsibility belongs.
Rules like these fit well inside Entity:
An expired subscription cannot change plans.
A canceled subscription cannot be canceled again.
A free trial subscription must have a trial end date.
The end date of a subscription period cannot be before the start date.
On the other hand, logic like this may be more natural in separate objects:
Calculate the additional payment amount for a plan change.
Apply discount policy based on user grade and promotions.
Judge whether rejoining is allowed based on several subscriptions and payment histories.
Request payment from an external payment system.
In these cases, we can split responsibilities into Domain Service, Policy, Specification, Application Service, Infrastructure Adapter, and so on. The key is not scattering domain rules carelessly into technical services or procedural code.
Good behavior protects invariants
Behavior in a domain object is not a simple convenience method. That behavior should protect the object’s invariants.
For example, SubscriptionPeriod protects the rule that the end date cannot be before the start date. Subscription protects the rule that an expired subscription cannot become active without renewal. Plan protects the rule that the minimum seat count cannot be less than 1.
Behavior changes state in a way that does not break these invariants.
public void expire(Instant now) {
if (this.status == SubscriptionStatus.CANCELED) {
return;
}
if (this.period.endsAfter(now)) {
throw new CannotExpireSubscriptionException("The expiration date has not passed yet.");
}
this.status = SubscriptionStatus.EXPIRED;
this.expiredAt = now;
}
This method does not simply change the state to EXPIRED. It checks the conditions under which expiration is allowed, and permits only a valid state change.
Good domain behavior keeps objects from falling into strange states.
Closing
Putting behavior into domain objects is not an abstract argument for writing object-oriented code. It is a practical proposal to keep domain rules from scattering inside code.
A setter-centered model is easy to create quickly. But as domain rules grow, conditionals pile up in the service layer, and domain objects become empty shells. Eventually, reading the code no longer helps us understand the domain.
When domain objects have meaningful behavior, the code starts speaking the language of the domain. It can say cancelByMember(reason, now) instead of setStatus(CANCELED). This difference may look small, but over time it greatly changes the understandability and maintainability of the model.
What matters in DDD is not having more objects. What matters is that domain rules and behavior are where they should be. When domain objects can tell their own story, the model finally becomes a living model.