Skip to content

Domain Service

Encapsulating domain operations that don't naturally belong to entities or value objects

Related Concepts: Repository | Use Cases | Clean Architecture

What is a Domain Service?

"Sometimes, it just isn't a thing." - Eric Evans, Domain-Driven Design

A Domain Service is an operation offered as an interface that stands alone in the model, without encapsulating state. When important domain operations don't naturally fit within an Entity or Value Object, a Domain Service provides a home for that behavior without compromising the clarity of your domain model.

As Eric Evans explains:

"Some concepts from the domain aren't natural to model as objects. Forcing the required domain functionality to be the responsibility of an ENTITY or VALUE either distorts the definition of a model-based object or adds meaningless artificial objects."

Domain Services exist for operations that are fundamentally activities or actionsverbs rather than nounsthat have significant meaning within your domain's Ubiquitous Language.

The Three Characteristics

According to Eric Evans, a good Domain Service has three essential characteristics:

1. The operation relates to a domain concept that is not a natural part of an Entity or Value Object

The behavior doesn't have a natural home in any single domain object. Forcing it into an entity would distort that entity's purpose and create awkward dependencies.

2. The interface is defined in terms of other elements of the domain model

Domain Services work exclusively with domain typesentities, value objects, and other domain services. They speak the language of the domain, not the language of infrastructure.

3. The operation is stateless

Any client can use any instance of a Domain Service without regard to that instance's history. As Evans clarifies: "The execution of a SERVICE will use information that is accessible globally, and may even change that global information (that is, it may have side effects). But the SERVICE does not hold state of its own that affects its own behavior."

When to Use Domain Services

Create a Domain Service When:

  • Logic spans multiple aggregates or entities - The operation naturally coordinates several domain objects
  • Complex business operations don't belong to a single entity - The behavior would create awkward responsibilities if forced into an entity
  • Preventing domain logic duplication - The operation would otherwise scatter throughout your codebase
  • Business rules require coordination - Multiple domain objects must collaborate to enforce a rule
  • The operation is intrinsically an activity - It's a verb (transfer, calculate, analyze) rather than a noun

Don't Create a Domain Service When:

  • The behavior naturally belongs to an entity - As Evans warns, "the more common mistake is to give up too easily on fitting the behavior into an appropriate object"
  • It's just infrastructure - Technical operations like sending emails belong in infrastructure services
  • It's just orchestration - Workflow coordination without business logic belongs in application services
  • You're creating "Manager" objects - Services that masquerade as meaningless "doers" often indicate poor domain modeling

Domain vs Application vs Infrastructure Services

One of the most important distinctions in service-oriented architectures is understanding which layer a service belongs to. Evans provides clear guidance:

Domain Services

  • Have business meaning in the domain language
  • Embed significant business rules and domain logic
  • Make business decisions about domain state
  • Work with domain types (entities, value objects)
  • Example: FundsTransferService - encapsulates business rules for debiting/crediting accounts, determines if transfer is allowed

Application Services

  • Lack business meaning - they don't contain domain knowledge
  • Orchestrate workflows - coordinate domain operations without making business decisions
  • Handle technical concerns - message formats, session management, transaction boundaries
  • Work with DTOs and domain types - translate between external representations and domain model
  • Example: FundsTransferApplicationService - parses XML requests, calls domain service, sends notifications

Infrastructure Services

  • Purely technical with no business meaning whatsoever
  • Provide technical capabilities - email, file storage, message queues
  • Example: NotificationService - sends emails and SMS messages

Learn more about the distinction from Enterprise Craftsmanship

Examples

Example 1: Funds Transfer (Banking Domain)

From Eric Evans' original example:

typescript
// Domain Service - contains business logic
class FundsTransferDomainService {
  constructor(
    private accountRepository: AccountRepository,
    private ledgerService: LedgerService
  ) {}

  async transfer(
    fromAccountId: AccountId,
    toAccountId: AccountId,
    amount: Money
  ): Promise<TransferResult> {
    const [fromAccount, toAccount] = await Promise.all([
      this.accountRepository.findById(fromAccountId),
      this.accountRepository.findById(toAccountId)
    ]);

    if (!fromAccount || !toAccount) {
      return TransferResult.failure('Account not found');
    }

    // Business rule: verify sufficient funds
    if (!fromAccount.hasSufficientFunds(amount)) {
      return TransferResult.failure('Insufficient funds');
    }

    // Business rule: verify accounts are active
    if (!fromAccount.isActive() || !toAccount.isActive()) {
      return TransferResult.failure('Inactive account');
    }

    // Execute the transfer using domain logic
    fromAccount.debit(amount);
    toAccount.credit(amount);

    await Promise.all([
      this.accountRepository.save(fromAccount),
      this.accountRepository.save(toAccount),
      this.ledgerService.recordTransfer(fromAccount, toAccount, amount)
    ]);

    return TransferResult.success();
  }
}

Example 2: Shipping Cost Calculator (E-commerce)

A classic example where the calculation logic doesn't naturally belong to Order or Product:

typescript
// Domain Service - encapsulates complex shipping business rules
class ShippingCostCalculator {
  calculateShippingCost(
    order: Order,
    destination: Address,
    shippingMethod: ShippingMethod
  ): Money {
    // Business rules for shipping calculation
    const baseRate = shippingMethod.getBaseRate();
    const weight = order.calculateTotalWeight();
    const dimensions = order.calculatePackageDimensions();

    // Apply business rules
    let cost = baseRate.multiply(weight);

    // Oversized package surcharge
    if (dimensions.isOversized()) {
      cost = cost.add(Money.dollars(25));
    }

    // Free shipping promotion
    if (order.qualifiesForFreeShipping()) {
      return Money.zero();
    }

    // Destination-based adjustments
    if (destination.isRemote()) {
      cost = cost.multiply(1.5);
    }

    return cost;
  }
}

More examples at DEV Community

Example 3: Library Book Lending (Library Domain)

When business logic requires coordinating multiple entities:

typescript
// Domain Service - handles complex borrowing business rules
class BookLendingService {
  constructor(
    private bookRepository: BookRepository,
    private readerRepository: ReaderRepository
  ) {}

  async canBorrowBook(
    readerId: ReaderId,
    bookId: BookId
  ): Promise<BorrowingEligibility> {
    const [reader, book] = await Promise.all([
      this.readerRepository.findById(readerId),
      this.bookRepository.findById(bookId)
    ]);

    if (!reader || !book) {
      return BorrowingEligibility.notFound();
    }

    // Business rule: check reader account status
    if (!reader.isActive()) {
      return BorrowingEligibility.ineligible('Account is not active');
    }

    // Business rule: check borrowing limit
    if (reader.hasReachedBorrowingLimit()) {
      return BorrowingEligibility.ineligible('Borrowing limit reached');
    }

    // Business rule: check book availability
    if (!book.hasAvailableCopies()) {
      return BorrowingEligibility.ineligible('No copies available');
    }

    // Business rule: reading room only restriction
    if (book.isReadingRoomOnly()) {
      return BorrowingEligibility.readingRoomOnly();
    }

    return BorrowingEligibility.eligible();
  }

  async borrowBook(readerId: ReaderId, bookId: BookId): Promise<Loan> {
    const eligibility = await this.canBorrowBook(readerId, bookId);

    if (!eligibility.isEligible()) {
      throw new BorrowingNotAllowedError(eligibility.reason);
    }

    // Execute the domain operation
    const reader = await this.readerRepository.findById(readerId);
    const book = await this.bookRepository.findById(bookId);

    const loan = book.lendTo(reader);
    reader.addLoan(loan);

    await Promise.all([
      this.bookRepository.save(book),
      this.readerRepository.save(reader)
    ]);

    return loan;
  }
}

Read more about this pattern at Developer20

Naming Domain Services

Domain Services should be named after activities in your Ubiquitous Language:

DO 

  • FundsTransferService - describes the domain activity
  • ShippingCostCalculator - names the domain calculation
  • BookLendingService - captures the domain operation
  • InventoryAllocationService - describes the business action

DON'T L

  • AccountManager - meaningless "manager" suffix
  • BookHelper - vague "helper" indicates unclear responsibility
  • DataProcessor - technical term, not domain language
  • OrderHandler - generic "handler" lacks domain meaning

As Evans warns: "Services masquerade as model objects, appearing as objects with no meaning beyond doing some operation. These 'doers' end up with names ending in 'Manager' and the like."

Granularity and Reusability

Evans emphasizes that Domain Services also serve to control granularity:

"Medium-grained, stateless SERVICES can be easier to reuse in large systems because they encapsulate significant functionality behind a simple interface."

Domain Services prevent fine-grained domain objects from leading to:

  • Knowledge leaks - domain behavior coordinated in the application layer
  • Inefficient messaging - chatty interfaces in distributed systems
  • Scattered domain logic - business rules spread across multiple layers

By consolidating related domain operations, Domain Services keep domain knowledge concentrated where it belongs.

Best Practices

DO 

  • Define services in the domain layer - they are part of your domain model
  • Name operations from Ubiquitous Language - use terms meaningful to domain experts
  • Keep services stateless - any instance should be usable by any client
  • Accept and return domain types - entities, value objects, domain events
  • Use judiciously - don't strip all behavior from entities
  • Test as pure domain logic - minimal mocking required

DON'T L

  • Create services reflexively - try to fit behavior into entities first
  • Mix domain and application concerns - keep orchestration out of domain services
  • Depend on infrastructure directly - use domain-defined repository interfaces
  • Create "Manager" or "Helper" classes - these often lack true domain meaning
  • Make services stateful - state belongs in entities and value objects
  • Let services grow too large - break up complex services by subdomain

Testing Domain Services

Domain Services are highly testable because they contain pure domain logic:

typescript
describe('FundsTransferDomainService', () => {
  let service: FundsTransferDomainService;
  let accountRepository: InMemoryAccountRepository;

  beforeEach(() => {
    accountRepository = new InMemoryAccountRepository();
    service = new FundsTransferDomainService(
      accountRepository,
      new InMemoryLedgerService()
    );
  });

  it('should reject transfer when insufficient funds', async () => {
    const fromAccount = new Account('acc-1', Money.dollars(50));
    const toAccount = new Account('acc-2', Money.dollars(100));

    await accountRepository.save(fromAccount);
    await accountRepository.save(toAccount);

    const result = await service.transfer(
      fromAccount.id,
      toAccount.id,
      Money.dollars(100) // More than available
    );

    expect(result.isFailure()).toBe(true);
    expect(result.reason).toBe('Insufficient funds');
  });

  it('should successfully transfer funds between accounts', async () => {
    const fromAccount = new Account('acc-1', Money.dollars(200));
    const toAccount = new Account('acc-2', Money.dollars(100));

    await accountRepository.save(fromAccount);
    await accountRepository.save(toAccount);

    const result = await service.transfer(
      fromAccount.id,
      toAccount.id,
      Money.dollars(50)
    );

    expect(result.isSuccess()).toBe(true);

    const updatedFrom = await accountRepository.findById('acc-1');
    const updatedTo = await accountRepository.findById('acc-2');

    expect(updatedFrom.balance).toEqual(Money.dollars(150));
    expect(updatedTo.balance).toEqual(Money.dollars(150));
  });
});

Key Takeaways

  1. Domain Services exist for operations that are fundamentally activities, not things
  2. They must have clear business meaning - not just technical helpers
  3. Statelessness is essential - any instance can serve any client
  4. They work exclusively with domain types - entities, value objects, other domain services
  5. Use them judiciously - don't give up on entity behavior too easily
  6. Avoid meaningless names - "Manager" and "Helper" often indicate poor modeling
  7. They're distinct from application and infrastructure services - keep the layers clear

Further Reading

Primary Sources

  • Domain-Driven Design: Tackling Complexity in the Heart of Software - Eric Evans (2003) - The definitive source on Domain Services
  • Domain-Driven Design - Martin Fowler's overview of DDD concepts

Supporting Articles


For implementation examples, see Application Service for the complementary orchestration pattern.