One of the most confusing names when applying DDD is Service. Application Service, Domain Service, and Infrastructure Service all have the word Service. So over time, all logic gathers in SomethingService.
Application Service is especially prone to accumulating business logic. It receives requests from controllers, calls Repositories, checks state with conditionals, calls external APIs, and saves results. At first, this feels natural. But as domain rules increase, Application Service becomes a giant procedural script.
The role of Application Service is not to directly judge domain logic. Application Service is the layer that coordinates the flow of a use case.
It handles the flow of a use case
Think about a feature that changes a subscription plan. This use case has several steps.
1. Load the subscription of the requesting user.
2. Load the target plan.
3. Ask the subscription to change its plan.
4. If additional payment is needed, create a payment request.
5. Save the changed subscription.
6. Publish necessary events.
This entire flow fits well in an Application Service.
public void changePlan(ChangePlanCommand command) {
Subscription subscription = subscriptionRepository.findById(command.subscriptionId());
Plan targetPlan = planRepository.findById(command.targetPlanId());
subscription.changePlan(targetPlan, planChangePolicy, clock.now());
subscriptionRepository.save(subscription);
eventPublisher.publishAll(subscription.pullEvents());
}
Here, the Application Service does important work. It loads objects, prepares collaborators needed for the use case, asks the domain object to work, saves it, and publishes events.
But it does not directly judge “whether the plan can be changed,” “how to calculate the remaining period,” or “what state changes happen.” Those judgments belong to domain objects or domain policy objects.
What can belong in Application Service
It is a misunderstanding to think Application Service should contain no logic at all. Application Service contains logic related to application flow.
For example:
Starting and ending transactions
Authorization checks
Converting request DTOs into Commands
Loading Aggregates through Repositories
Calling domain object methods
Coordinating the order of external system calls
Publishing Domain Events
Composing response models
This logic is hard for one specific domain object to own, because it needs to know the progress of the whole use case.
On the other hand, rules like these are dangerous if they stay in Application Service for long:
An expired subscription cannot change plans.
A free trial is allowed only once per account.
After payment failure, there is a three-day grace period.
An enterprise plan must satisfy the minimum seat count.
These are domain rules. If Application Service judges them directly, the same rules can scatter across several use cases.
Smells of a bad Application Service
The signs that an Application Service is becoming bloated are fairly clear.
The first sign is when conditionals directly express domain rules.
if (subscription.getStatus() == EXPIRED) {
throw new IllegalStateException("An expired subscription cannot be changed.");
}
if (targetPlan.isEnterprise() && command.seatCount() < 10) {
throw new IllegalStateException("Enterprise plans require at least 10 seats.");
}
When many conditions like this appear in Application Service, domain objects become anemic.
The second sign is many setter calls.
subscription.setPlanId(targetPlan.getId());
subscription.setStatus(ACTIVE);
subscription.setNextBillingDate(nextBillingDate);
This code is not coordinating a use case. It is manipulating the internal state of a domain object.
The third sign is the same rule repeating across several Application Services. If similar state validation repeats in user cancellation, admin cancellation, and payment-failure termination, we need to reconsider where the rule belongs.
A good Application Service is thin but meaningful
People often say Application Service should be thin. The direction is right, but the word “thin” is not enough. A good Application Service is not simply short. It delegates domain judgment to domain objects.
@Transactional
public void cancelSubscription(CancelSubscriptionCommand command) {
Subscription subscription = subscriptionRepository.findById(command.subscriptionId());
subscription.cancelByMember(command.reason(), clock.now());
subscriptionRepository.save(subscription);
eventPublisher.publishAll(subscription.pullEvents());
}
This code is short. But it is good not merely because it is short. Subscription judges whether cancellation is allowed, how state changes when canceled, and which domain events occur. The Application Service clearly shows the flow of the use case.
This kind of code lets readers quickly understand “in what order this feature proceeds.” At the same time, detailed rules can be found inside the domain object.
Transaction boundaries and Application Service
Application Service is a good place to define transaction boundaries. It knows which Aggregates a single use case loads and changes.
@Transactional
public void renewSubscription(RenewSubscriptionCommand command) {
Subscription subscription = subscriptionRepository.findById(command.subscriptionId());
PaymentResult paymentResult = paymentGateway.pay(command.paymentRequest());
subscription.renew(paymentResult, clock.now());
subscriptionRepository.save(subscription);
}
But there are still things to consider. Can the external system call be placed inside the transaction? What should happen if saving fails after payment succeeds? Should event publishing happen before or after transaction commit?
Application Service coordinates this kind of application-level consistency. But if it also takes on domain judgments such as “is this subscription renewable?”, responsibilities become mixed.
Transaction boundaries are technical issues, but they connect to domain issues. Aggregate boundaries and Application Service transaction boundaries should be considered together.
Difference from Domain Service
Application Service and Domain Service have similar names but different roles.
Application Service coordinates use case flow. It receives a user’s request, decides which objects to load, in what order to call them, and how to save the result.
Domain Service expresses domain logic that is a domain concept but does not naturally belong to a specific Entity or Value Object. Examples include calculating plan change fees or judging free trial availability based on several subscription histories.
PlanChangeResult result = planChangePolicy.calculate(subscription, targetPlan, now);
planChangePolicy contains domain rules. Application Service prepares this policy object and passes it to the domain behavior.
If we do not distinguish roles, everything gathers in Application Service. Then DDD becomes procedural code with separated layers.
Closing
Application Service is an important layer. But its importance is not in containing all business logic. The value of Application Service is in clearly coordinating use case flow.
A good Application Service makes domain objects work. It retrieves Aggregates from Repositories, prepares needed policy objects, calls domain behavior, saves, and publishes events. It does not judge everything directly.
When conditionals and setters start increasing in Application Service, we should treat it as a boundary signal. Should this condition be known by a domain object? Should it be separated into a Domain Service or Policy? Or does it truly belong to application flow?
In DDD, Application Service is not the main character. It is closer to a director who coordinates the order so that domain objects can play their roles on stage.