# Application Services vs Domain Services in DDD

> Application services coordinate use cases. Domain services hold business rules that do not fit one entity or value object. These .NET examples show where loading, transactions, and decisions belong, including the edge cases around repositories and concurrency.

Published: 2026-09-22. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/application-services-vs-domain-services-ddd

An **application service** coordinates a use case by loading state, invoking domain behavior, and saving changes.
A **domain service** expresses business rules that do not fit one entity or value object.
Put transaction boundaries and external side effects in application orchestration, and keep business decisions in domain types so multiple use cases can reuse them.

## The One-Line Distinction

**An application service orchestrates a use case. A domain service makes a business decision.**

Slightly longer:

- The **application service** is the entry point for "a thing the user (or a message) wants done". It loads aggregates, invokes domain behavior, persists changes, and coordinates side effects. It knows about transactions, repositories, and the outside world. It contains no business rules.
- The **domain service** holds business logic that doesn't naturally belong to one entity, usually because it spans multiple aggregates or expresses a calculation over several domain concepts. It speaks only in domain types and makes no decisions about persistence, messaging, or transactions.

In [**Clean Architecture terms**](https://milanjovanovic.tech/blog/clean-architecture-dotnet): application services live in the application layer, domain services live in the domain layer, and the dependency arrow points inward, never out.

| Responsibility | Application Service | Domain Service |
| --- | --- | --- |
| Purpose | Coordinate a use case | Express a business rule |
| Typical inputs | Commands, identifiers, DTOs | Entities, value objects, domain facts |
| Persistence | Load and save through abstractions | May query a domain-defined interface for a rule |
| Transactions and external messages | Coordinate the boundary and delivery | Leave delivery and transaction mechanics outside |
| Testing | Verify orchestration and integration | Verify decisions, often with plain objects |

## A Use Case, Sorted Correctly

Money transfer illustrates the distinction because its rule spans two accounts.

The following service uses your application's `Account`, `Money`, and `Result` types.
`CanDeposit` checks all destination rejection conditions, including currency and balance limits; `Deposit` then succeeds while those in-memory objects remain unchanged.
`Withdraw` performs its own validation and returns a failure before changing the source:

```csharp
// Domain layer
public sealed class MoneyTransferService
{
    public Result Transfer(Account source, Account destination, Money amount)
    {
        if (source.Id == destination.Id)
        {
            return Result.Failure(TransferErrors.SameAccount);
        }

        if (amount.Amount <= 0 || !destination.CanDeposit(amount))
        {
            return Result.Failure(TransferErrors.InvalidTransfer);
        }

        var withdrawal = source.Withdraw(amount);
        if (withdrawal.IsFailure)
        {
            return withdrawal;
        }

        destination.Deposit(amount);

        return Result.Success();
    }
}
```

It receives fully-loaded aggregates, applies the rules, and mutates domain state.
You can unit test it with two in-memory `Account` objects and nothing else.
Checking the destination before withdrawal avoids partially changing the in-memory accounts when the deposit would be rejected.

The application service (here as a [**MediatR command handler**](https://milanjovanovic.tech/blog/cqrs-pattern-with-mediatr), which is an application service for exactly one use case) does everything around that decision:

```csharp
// Application layer
public sealed class TransferMoneyCommandHandler(
    IAccountRepository accountRepository,
    MoneyTransferService transferService,
    IUnitOfWork unitOfWork)
    : IRequestHandler<TransferMoneyCommand, Result>
{
    public async Task<Result> Handle(TransferMoneyCommand command, CancellationToken ct)
    {
        var source = await accountRepository.GetByIdAsync(command.SourceId, ct);
        var destination = await accountRepository.GetByIdAsync(command.DestinationId, ct);

        if (source is null || destination is null)
        {
            return Result.Failure(TransferErrors.AccountNotFound);
        }

        // The request boundary has constructed a validated Money value.
        var result = transferService.Transfer(source, destination, command.Amount);

        if (result.IsFailure)
        {
            return result;
        }

        await unitOfWork.SaveChangesAsync(ct);

        return Result.Success();
    }
}
```

Here `TransferMoneyCommand.Amount` is a validated `Money` value; its construction belongs at the request boundary.
The repository and unit of work must share a tracked context, and one save must atomically persist both account changes.
With [**EF Core transactions**](https://learn.microsoft.com/en-us/ef/core/saving/transactions), a single `SaveChanges` is transactional when the provider supports transactions.

Atomic writes do not prevent two requests from spending the same balance they both read earlier.
Configure [**concurrency tokens**](https://learn.microsoft.com/en-us/ef/core/saving/concurrency) and handle a conflict by reloading and re-evaluating the whole transfer, or choose an appropriate locking strategy.
Separate account stores require a durable multi-step workflow; this sample assumes one database and does not implement a banking ledger or retry idempotency.

![A sequence showing the application service loading both accounts from the repository, delegating the transfer decision to the domain service, and then saving through the unit of work](https://milanjovanovic.tech/blogs/articles/application-services-vs-domain-services-ddd/money-transfer-flow.png)

The handler decides nothing about money; it doesn't know what "insufficient funds" means.
If you deleted the domain service and inlined its logic here, the code would still run, and that's exactly how business rules end up trapped in one use case, unreachable by the next one that needs them.

## How Do You Tell Which Service You Need?

When a service class crosses your desk in review, ask three questions:

1. **Does it coordinate infrastructure for a use case?** Loading, saving, opening transactions, and delivering external messages belong in application orchestration. A domain-defined lookup used by a business rule can still belong to a domain service.
2. **Would the business recognize the logic?** Read the method body aloud, minus the plumbing. "If the source can't cover the amount, refuse the transfer" is domain. "Begin transaction, load two accounts, save, publish event" is application. If the domain expert would nod along, it's domain logic.
3. **What does its test verify?** Checking a decision against domain inputs suggests domain logic. Checking the order of repository calls and message delivery suggests application orchestration. Dependencies are useful evidence, but the presence of a mock does not decide the layer.

The most common finding: an "application service" with an `if` chain implementing a pricing rule or an eligibility check.
The fix is mechanical: extract the decision into the domain (an entity method if it fits one aggregate, a domain service if it spans several), and leave the orchestration behind.

## Edge Cases That Cause Arguments

**Can a domain service use a repository?**
The purist answer is no; the practical answer is "a read-only interface, sparingly, when the rule itself needs data".
The uniqueness check is the classic case: "a user's email must be unique" is a business rule, but only the database knows the answer.
Defining `IUserRepository` in the domain layer and letting a `UserUniquenessChecker` depend on it is a defensible pattern.
My preference is to keep even that in the application layer when possible (check first, then create), because every dependency you add to the domain makes it harder to test and reason about.
What's never acceptable: a domain service that loads and saves aggregates as part of a use-case flow. That's orchestration wearing the wrong uniform.

A uniqueness lookup can race with another request.
Use a unique database constraint on the normalized value as the final guard, and handle conflicts when saving.

**Where do transactions live?**
The application layer coordinates the transaction boundary; infrastructure implements the transaction.
A domain service expresses the rule, while the caller must provide the isolation and atomicity that rule requires.
That's also why [**transaction management in Clean Architecture**](https://milanjovanovic.tech/blog/transactions-clean-architecture) is a pipeline or handler concern.

**Is a `PricingService` with no state really a service?**
If it's pure calculation over domain types, it could equally be a static method or an entity method.
Reach for a domain service only after asking whether the logic belongs on an entity or value object.
The [**domain service**](https://milanjovanovic.tech/blog/domain-services-ddd) is a fallback position, not the default; a codebase where all logic lives in services has rebuilt the [**anemic domain model**](https://milanjovanovic.tech/blog/rich-vs-anemic-domain-model) with extra steps.

**What about "manager", "helper", "processor" classes?**
Same test. The name doesn't matter; the dependencies and the vocabulary do.

## What Does This Separation Improve?

Separating decisions from orchestration has practical benefits:

- **Reuse without fear.** The `MoneyTransferService` works for the API endpoint, the batch job, and the admin tool, because it has no idea which one is calling. Rules trapped in application services get copy-pasted instead.
- **Focused tests.** Pure domain services can test decisions with plain objects. Application-service tests verify orchestration and failure handling. This split helps with [**unit testing use cases**](https://milanjovanovic.tech/blog/unit-testing-clean-architecture-use-cases).
- **A domain layer that stays portable.** When domain services depend only on domain types, swapping infrastructure (a new bus, a new ORM) never touches business rules.

## Summary

- **Application service**: loads, delegates, saves, notifies. Knows infrastructure, contains no rules. Command handlers are this.
- **Domain service**: business decisions that do not fit one entity or value object, expressed in domain terms.
- Review the responsibility, business vocabulary, and what the test verifies; dependencies alone do not classify a service.
- Prefer entities first, domain services second, and keep transactions and side effects in the application layer.

The cost is an extra boundary and occasionally more types.
Use it where shared rules or complex decisions justify that separation.

## Frequently asked questions

### What is the difference between an application service and a domain service?

An application service orchestrates a use case: it loads aggregates, calls domain logic, saves changes, and triggers side effects like emails. A domain service holds business logic that spans multiple aggregates or does not fit a single entity. One coordinates, the other decides.

### Can a domain service call a repository?

It can depend on repository interfaces defined in the domain layer when a rule needs data, like a uniqueness check. Loading and saving aggregates for a use case is orchestration, which belongs in the application service.

### Is a command handler an application service?

Yes. A MediatR command handler is an application service for exactly one use case. The pattern splits a wide service class into focused handlers, but the responsibilities are identical: orchestrate, do not decide.

### Should a domain service send emails or publish messages?

Sending emails and publishing external messages are side effects coordinated by the application layer through infrastructure abstractions. Domain services express business rules in domain terms. Pure calculations are easy to unit test, while rules using domain-defined data interfaces may need test substitutes.

### Where should business logic go if it fits on an entity?

On the entity. Domain services are the fallback for logic that spans aggregates or requires external information, not the default home for business rules. If everything lives in services, you have an anemic domain model.
