Skip to content
Leo Jung
Go back

[DDD Intro #16] Domain Service: Domain logic that is hard to force into an object

Edit page

When studying DDD, we often hear that “domain logic should be inside domain objects.” This is important. If domain rules scatter into Application Services or Controllers, the model becomes anemic.

But not every piece of domain logic can be placed inside an Entity or Value Object. Some rules are hard to see as the responsibility of one specific object. Sometimes several objects must be considered together, or the calculation itself is an independent domain concept.

This is where Domain Service can be used.

Domain Service is not a trash bin for domain logic. It is a way to express a domain concept that does not naturally belong to a specific Entity or Value Object.

Why Domain Service is needed

Think about changing a plan in a subscription service. A user wants to change from a monthly Basic plan to an annual Pro plan. We need to calculate the additional payment amount.

This calculation may need several pieces of information.

Remaining period of the current subscription
Price of the current plan
Price of the target plan
Monthly or annual billing cycle
Promotion discount
User grade
Minimum seat count for enterprise plan

We could put all this logic into Subscription. But if the subscription object knows price policy, discount policy, and plan comparison rules, it has too many responsibilities.

Putting it into Plan is also awkward. The calculation needs to consider the current subscription, the target plan, the current time, and policy together.

In this case, a domain service or policy object like this is natural:

PlanChangeResult result = planChangePolicy.calculate(subscription, targetPlan, now);
subscription.applyPlanChange(result);

Here, PlanChangePolicy is not a simple technical service. It is the domain concept of “plan change policy” expressed in code.

Domain Service names should be domain language

A common reason Domain Service fails is that its name is too general.

SubscriptionService
OrderService
PaymentService

These names are too broad. It is hard to know what the service does, and over time they become places where every kind of logic enters.

Domain Service should have a concrete domain concept as its name whenever possible.

PlanChangePolicy
RenewalEligibilityChecker
RefundPolicy
SubscriptionPricingService
TrialAvailabilityPolicy

These names reveal domain questions.

By what policy is a plan change calculated?
Is this subscription renewable?
How are refund eligibility and amount decided?
Can this user use a free trial again?

A good Domain Service name shows not merely that “a service exists,” but that “a domain concept exists.”

Difference between Application Service and Domain Service

The easiest way to distinguish the two Services is to ask different questions.

Application Service asks about use case flow.

What should be called, and in what order, to handle this request?
Which Aggregates should be loaded?
Inside which transaction should they be saved?
Which events should be published?

Domain Service asks about domain judgment.

Is this subscription renewable?
How much should be charged for this plan change?
Is this user eligible for a free trial?
By which policy should this refund request be handled?

For example, look at this code:

public void changePlan(ChangePlanCommand command) {
    Subscription subscription = subscriptionRepository.findById(command.subscriptionId());
    Plan targetPlan = planRepository.findById(command.targetPlanId());

    PlanChangeResult result = planChangePolicy.calculate(subscription, targetPlan, clock.now());
    subscription.changePlan(result);

    subscriptionRepository.save(subscription);
}

This method is an Application Service. It coordinates the flow. On the other hand, planChangePolicy.calculate() is a domain service or policy object. It calculates the change result according to domain rules.

It is also different from Infrastructure Service

Domain Service is also different from Infrastructure Service. Infrastructure Service provides technical functions.

Sending email
Calling payment APIs
Uploading files
Publishing to a message broker
Encryption
Calling an external address validation API

These may be needed by the domain, but they are not themselves domain rules.

For example, PaymentGateway can be an infrastructure service that calls an external payment system. On the other hand, RefundPolicy is a domain service that judges refund eligibility and amount.

RefundDecision decision = refundPolicy.decide(subscription, payment, requestDate);
paymentGateway.refund(decision.amount());

The first line is domain judgment. The second line is an external system call. Distinguishing them makes testing easier and the model clearer.

The moment Domain Service is overused

Domain Service is useful, but easy to overuse. If all logic that should be in Entities is extracted into Services, the model becomes anemic.

Suppose there is a service like this:

public class SubscriptionDomainService {
    public void cancel(Subscription subscription, CancelReason reason) {
        if (subscription.getStatus() == EXPIRED) {
            throw new IllegalStateException();
        }
        subscription.setStatus(CANCELED);
    }
}

This logic does not need to be in a Domain Service. Judging whether a subscription can be canceled based on its own state is more naturally the responsibility of Subscription.

subscription.cancelByMember(reason, now);

Before creating a Domain Service, we should ask:

Can a specific Entity judge this logic by itself?
Can this logic be expressed as behavior of a Value Object?
Does it need to look at several objects together?
Can it be called an independent policy or calculation in the domain?

If the answer to the first two questions is “yes,” it may be better to place the logic inside the object before extracting it into a Domain Service.

Services that express judgment rather than changing state

A good Domain Service often expresses judgment or calculation rather than directly changing state.

RenewalDecision decision = renewalPolicy.decide(subscription, paymentHistory, now);
subscription.applyRenewalDecision(decision);

In this structure, the policy object creates a decision, and the Entity reflects that decision as its own state change. This clarifies roles.

The policy handles calculation and judgment. The Entity changes its own state and protects invariants.

If a Domain Service starts directly manipulating fields of an Entity, responsibilities become mixed. If the Domain Service knows too much internal state, we should reconsider whether that logic should be inside the Entity.

Domain Service is also a good target for tests

Domain Service explicitly contains domain rules, so it is a good target for testing.

For example, we can test a policy that judges free trial availability.

A_member_who_already_used_a_free_trial_cannot_start_another_free_trial
A_member_with_payment_history_is_not_eligible_for_a_new_free_trial
An_enterprise_member_invited_by_an_admin_may_not_be_eligible_for_a_free_trial

These tests go beyond simple code verification and become documentation of domain policy. The more concrete the Domain Service name is, the closer test names become to domain language.

Closing

Domain Service is not a convenient space for domain logic that is hard to place anywhere. It is a tool for expressing a domain concept that does not naturally belong to a specific Entity or Value Object.

A good Domain Service uses domain language from its name. Concrete names such as PlanChangePolicy, RefundPolicy, and TrialAvailabilityPolicy are better than broad and vague names like SubscriptionService.

When using Domain Service, we must always ask where responsibility belongs. Is this logic a rule that an Entity should protect by itself? Is it behavior of a Value Object? Is it an independent policy that needs to look at several objects together?

Using Domain Service carefully through these questions keeps the domain model from becoming anemic while also preventing it from becoming excessively bloated. What matters is not putting all logic in one place, but placing domain meaning where it appears most naturally.


Edit page
Share this post:

Previous Post
[DDD Intro #17] Factory: Creation also contains domain knowledge
Next Post
[DDD Intro #15] Application Service: The layer that coordinates use cases