Domain logic starts as small conditionals. If it is in a certain state, do not process it. If a certain date has passed, expire it. If it is a certain plan, do not discount it. At first, a few if statements are enough.
The problem begins when the same condition repeats in many places. As conditionals increase, domain policy scatters across the code. At some point, fewer people can accurately explain “free trial eligibility” or “renewal eligibility.” Even if we search the code, the condition is not in one place.
Specification and Policy are ways to model these conditions and policies. The point is not to remove conditionals. The point is to give names to conditions and policies that matter in the domain.
A conditional can be a hidden domain concept
Look at this code:
if (subscription.getStatus() == ACTIVE
&& subscription.getNextBillingDate().isBefore(now.plusDays(1))
&& !subscription.hasOverduePayment()
&& member.isVerified()) {
subscription.renew();
}
This code seems to check whether a subscription can be renewed. But that meaning is hidden inside the conditional. The same condition is likely to repeat in batches, admin screens, and manual renewal APIs.
We can give this condition a domain name.
if (renewalEligibility.isSatisfiedBy(subscription, member, now)) {
subscription.renew();
}
Now the code says “renewal eligibility.” The detailed implementation of the condition is hidden inside. What matters is that this condition has been promoted into a meaningful domain concept.
Specification expresses conditions as objects
Specification is a pattern that judges whether an object satisfies a certain condition.
public interface Specification<T> {
boolean isSatisfiedBy(T candidate);
}
In a subscription domain, we can create a Specification like this:
public class ActiveSubscriptionSpecification implements Specification<Subscription> {
public boolean isSatisfiedBy(Subscription subscription) {
return subscription.isActive();
}
}
But if every simple condition becomes a Specification, complexity increases instead. Specification is valuable when the condition is important in the domain, reused in several places, or needs composition.
For example, free trial availability can be a good candidate.
public class TrialAvailableSpecification {
public boolean isSatisfiedBy(Member member, List<SubscriptionHistory> histories) {
return member.isVerified()
&& histories.stream().noneMatch(SubscriptionHistory::wasTrial)
&& !member.hasPaymentHistory();
}
}
Now the free trial condition is no longer an if scattered across the code. It is a named domain concept.
Policy expresses judgment and decisions
If Specification mainly asks, “Is it satisfied?”, Policy is useful for expressing broader judgments or decisions.
For example, suppose we calculate a plan change fee. We do not simply judge whether it is possible. We also decide how much to charge and whether to apply the change immediately or from the next billing date.
PlanChangeDecision decision = planChangePolicy.decide(subscription, targetPlan, now);
PlanChangePolicy creates a result as well as checking conditions.
public record PlanChangeDecision(
boolean allowed,
Money additionalCharge,
SubscriptionPeriod nextPeriod,
PlanChangeTiming timing
) {}
If this kind of policy is placed directly inside Application Service, use case flow and domain judgment become mixed. Separating it as a Policy object makes domain judgment explicit.
Naming makes conversation possible
The greatest value of Specification and Policy is in language more than code structure. Once a condition has a name, conversation with domain experts becomes easier.
What are the conditions for free trial availability?
Are overdue users excluded from renewal eligibility?
Does the plan change policy apply immediately or from the next billing date?
How does the refund policy judge usage history?
These questions are not explanations of code conditionals. They are questions that explain domain policy.
When the same names exist in code, conversation and implementation connect.
trialAvailabilityPolicy.isAvailableFor(member, histories);
renewalEligibilityPolicy.canRenew(subscription, paymentHistory, now);
refundPolicy.decide(subscription, payment, refundRequest, now);
Good names are part of design. A named policy can be documented, tested, and changed.
Not every condition needs to become an object
After learning Specification and Policy, it can be tempting to turn every if into an object. But that is not a good direction.
A simple condition like this may not need a separate object.
if (subscription.isExpired()) {
throw new CannotRenewException();
}
This condition can be sufficiently known by Subscription itself. Extracting it into a separate Specification may make the code harder to read.
When considering Specification or Policy, it is good to ask:
Is this condition repeated in several places?
Is it a policy that domain experts name and talk about?
Does the condition change often?
Do several conditions need to be composed?
Does the result go beyond a simple boolean?
Is the responsibility awkward to place inside one Entity?
If these questions do not apply, a simple method or a condition inside an Entity may be enough.
Policy objects become the center of domain tests
When policies become objects, they become easier to test. This matters especially because policies are where business rules often change.
For example, we can test a refund policy.
Full_refund_is_given_within_7_days_after_payment_if_there_is_no_usage_history
Refund_is_not_given_after_7_days_from_payment
Partial_refund_is_given_when_there_is_partial_usage_history
A_subscription_already_canceled_cannot_be_refunded_twice
These test names read like domain policy documentation. Without a policy object, these rules scatter across service methods, and tests may be written only at the use case level, making detailed rules harder to verify.
Relationship between Policy and Domain Service
Policy can be seen broadly as a type of Domain Service. It expresses domain judgment that does not naturally fit inside a specific Entity or Value Object.
However, the name Policy more clearly reveals the domain meaning of “policy.” RefundPolicy is often better than RefundService. The former looks like an object that judges refund rules, while the latter looks like a service that can do anything.
Names restrict responsibility. Names that restrict responsibility create good design.
Closing
Conditionals are not bad. Every domain rule eventually includes some condition and judgment. The problem is when important conditions scatter without names.
Specification and Policy give names to conditions and policies. They explicitly leave questions in code such as “Is this user eligible for a free trial?”, “Can this subscription be renewed?”, and “How much should this refund request return?”
But not every condition needs to become an object. Small conditions are often better placed inside Entity or Value Object. Specification and Policy should be used for judgments that are important in the domain, repeated, and likely to change.
A good model in DDD does not only find nouns well. A good model also names conditions and policies. When important judgments in the domain appear as important concepts in code, the model becomes clearer.