Skip to content

Application Service

Orchestrating domain operations to fulfill business use cases (DDD perspective)

Related Concepts: Use Cases | Domain Service | Clean Architecture

Note: At Synapse, we prefer the term Use Case over Application Service. This document explains the Domain-Driven Design perspective on application services for teams working in DDD contexts. For our standard approach, see Use Cases.

What is an Application Service?

In Domain-Driven Design terminology, an Application Service is a component in the application layer that orchestrates domain operations to fulfill business use cases. If you're familiar with Clean Architecture, this is what Uncle Bob Martin calls a Use Case or Use Case Interactor.

From Eric Evans' Domain-Driven Design:

"The application layer is responsible for ordering the notification. The domain layer is responsible for determining if a threshold was met... [Application services] are built on top of the populations of ENTITIES and VALUES, behaving like scripts that organize the potential of the domain to actually get something done."

Application Services are the thin orchestration layer that sits between your presentation/API layer and your domain model, coordinating domain objects to fulfill business operations without containing business logic themselves.

Use Cases vs Application Services

These are essentially the same pattern with different names from different architectural schools:

AspectUse Cases (Clean Architecture)Application Services (DDD)
LayerApplication/Use Case layerApplication layer
PurposeOrchestrate business operationsOrchestrate domain objects
DependenciesInterfaces (ports)Repositories, domain services
Business LogicMay contain application-specific rulesStrictly no business logic
Terminology OriginRobert C. Martin (Uncle Bob)Eric Evans

The Philosophical Distinction

The key difference is emphasis:

  • Use Cases (Clean Architecture): Can contain "application-specific business rules" that coordinate entities
  • Application Services (DDD): Contain no business logic at allpurely orchestration and delegation

As one practitioner explains: "Domain services hold domain logic whereas application services don't. Domain logic is everything that is related to business decisions."

In DDD, if you find business logic creeping into your Application Service, it's a signal to extract a Domain Service or push the behavior into an Entity or Value Object.

Application Layer Responsibilities

According to Eric Evans, the application layer:

"Defines the jobs the software is supposed to do and directs the expressive domain objects to work out problems. This layer is kept thin. It does not contain business rules or knowledge, but only coordinates tasks and delegates work to collaborations of domain objects in the next layer down."

What Application Services DO

  1. Orchestrate workflows - Coordinate domain objects, repositories, and domain services
  2. Manage transactions - Define transactional boundaries for use cases
  3. Handle input/output - Accept DTOs, return DTOs or view models
  4. Coordinate infrastructure - Call notification services, message queues, etc.
  5. Dispatch domain events - Publish events raised by domain operations
  6. Sequence operations - Ensure operations happen in the correct order

What Application Services DON'T DO

  1. Contain business logic - All domain decisions belong in the domain layer
  2. Make business decisions - They ask domain objects to decide
  3. Hold state - They're stateless coordinators (though may track workflow progress)
  4. Expose domain objects directly - They translate between domain and external representations

Example: Funds Transfer (from Eric Evans)

Evans provides a comprehensive example showing how Application Services orchestrate domain operations:

typescript
// Application Service - Pure orchestration
class FundsTransferApplicationService {
  constructor(
    private fundsTransferDomainService: FundsTransferDomainService,
    private notificationService: NotificationService,
    private eventPublisher: EventPublisher
  ) {}

  async transferFunds(request: TransferFundsRequest): Promise<TransferResult> {
    // 1. Digest input (parse XML, validate format, etc.)
    const { fromAccountId, toAccountId, amount } = this.parseRequest(request);

    // 2. Delegate to domain service for business logic
    const result = await this.fundsTransferDomainService.transfer(
      fromAccountId,
      toAccountId,
      amount
    );

    // 3. Listen for confirmation
    if (result.isSuccess()) {
      // 4. Decide to send notification using infrastructure
      await this.notificationService.sendConfirmation(
        result.transactionId,
        fromAccountId,
        toAccountId,
        amount
      );

      // 5. Publish domain event for other systems
      this.eventPublisher.publish(
        new FundsTransferredEvent(result.transactionId)
      );
    }

    // 6. Return result (likely to be converted to HTTP response)
    return result;
  }

  private parseRequest(request: TransferFundsRequest): ParsedTransfer {
    // Handle XML/JSON parsing, format validation
    // This is application-level concern, not domain logic
  }
}

Notice the Application Service:

  • Doesn't decide if the transfer is allowed (domain service does)
  • Doesn't calculate debits and credits (domain service does)
  • Does coordinate the workflow: parse, transfer, notify, publish

Compare this to the Domain Service which contains the actual business rules.

Three-Layer Service Pattern (from Eric Evans)

Evans illustrates how a single business capability spans three service types:

�������������������������������������������������������������
 Application: Funds Transfer App Service                     
 " Digest input (XML request)                                
 " Send message to domain service                            
 " Listen for confirmation                                   
 " Decide to send notification                               
��������������������,����������������������������������������
                     
                     ¼
�������������������������������������������������������������
 Domain: Funds Transfer Domain Service                       
 " Interact with Account and Ledger objects                  
 " Make debits and credits                                   
 " Supply confirmation (allowed or not)                      
��������������������,����������������������������������������
                     
                     ¼
�������������������������������������������������������������
 Infrastructure: Send Notification Service                   
 " Send emails, letters, communications                      
 " Technical delivery mechanism                              
�������������������������������������������������������������

Example: Library Book Checkout

A practical example showing application-level orchestration:

typescript
// Application Service
class CheckoutBookApplicationService {
  constructor(
    private bookLendingService: BookLendingService, // Domain Service
    private bookRepository: BookRepository,
    private readerRepository: ReaderRepository,
    private notificationService: NotificationService,
    private unitOfWork: UnitOfWork
  ) {}

  async execute(request: CheckoutBookRequest): Promise<CheckoutResult> {
    // Application service coordinates but doesn't decide
    const { readerId, bookId } = request;

    // Start transaction boundary
    await this.unitOfWork.begin();

    try {
      // Ask domain service to make the business decision
      const loan = await this.bookLendingService.borrowBook(
        readerId,
        bookId
      );

      // Commit the transaction
      await this.unitOfWork.commit();

      // Handle side effects (infrastructure concerns)
      await this.notificationService.sendLoanConfirmation(
        loan.readerId,
        loan.dueDate
      );

      // Return DTO (not domain object)
      return CheckoutResult.success({
        loanId: loan.id.value,
        dueDate: loan.dueDate.toISOString(),
        bookTitle: loan.book.title
      });

    } catch (error) {
      await this.unitOfWork.rollback();

      if (error instanceof BorrowingNotAllowedError) {
        return CheckoutResult.failure(error.reason);
      }

      throw error;
    }
  }
}

The Application Service:

  • Manages the transaction boundary
  • Calls the Domain Service (which contains business logic)
  • Handles infrastructure concerns (notifications)
  • Returns DTOs (not domain objects)
  • Catches domain exceptions and translates them

Learn more about orchestration patterns at Sapiensworks

Key Characteristics

According to Khalil Stemmler's comparison:

  1. Stateless - Application Services don't hold state (unlike entities)
  2. Low complexity - Should have low cyclomatic complexity (minimal branching)
  3. Thin layer - Evans emphasizes keeping the application layer thin
  4. Single use case - Each service typically implements one business operation
  5. Framework-agnostic - No dependencies on web frameworks or ORMs

Common Patterns

Command Pattern

Many DDD practitioners use command objects with Application Services:

typescript
// Command (request DTO)
class TransferFundsCommand {
  constructor(
    public readonly fromAccountId: string,
    public readonly toAccountId: string,
    public readonly amount: number
  ) {}
}

// Application Service handles the command
class FundsTransferApplicationService {
  async handle(command: TransferFundsCommand): Promise<TransferResult> {
    // Orchestrate domain operations
  }
}

Domain Event Dispatching

Application Services often dispatch events raised by domain operations:

typescript
class CreateOrderApplicationService {
  async execute(request: CreateOrderRequest): Promise<OrderCreatedResult> {
    // Create order through domain
    const order = Order.create(request.customerId, request.items);
    await this.orderRepository.save(order);

    // Dispatch domain events raised during creation
    const events = order.getUncommittedEvents();
    await this.eventDispatcher.dispatchAll(events);

    return OrderCreatedResult.success(order.id);
  }
}

Learn more about event patterns at Developer20

Best Practices

DO 

  • Keep services thin - Delegate all business logic to domain layer
  • One service per use case - Clear, focused responsibility
  • Accept DTOs - Don't expose your API to domain objects directly
  • Return DTOs or Result objects - Translate domain results for external clients
  • Manage transactions - Define clear transactional boundaries
  • Coordinate, don't decide - Ask domain objects to make decisions

DON'T L

  • Put business logic in services - That belongs in domain layer
  • Expose domain objects directly - Return DTOs or view models
  • Mix presentation concerns - No HTTP, JSON, or UI logic
  • Create fat services - If it's complex, you're doing domain work
  • Skip domain layer - Don't go straight from app service to database

Synapse's Approach

At Synapse, we tend to prefer Use Cases over Application Services. We find the Clean Architecture terminology more accessible to stakeholders and clearer in intent. However, we follow the same principle: orchestrate domain operations without implementing business logic.

When reading DDD resources, understand that:

  • Their "Application Services" are our "Use Cases"
  • Their strict "no business logic" rule aligns with our "thin use cases" principle
  • The patterns and techniques translate directly

For Synapse's standard approach, see:

Key Takeaways

  1. Application Services and Use Cases are the same pattern with different names
  2. DDD emphasizes strict separation - absolutely no business logic in Application Services
  3. They orchestrate, not decide - coordination without decision-making
  4. Three service types in DDD - Application, Domain, and Infrastructure services each have clear roles
  5. Transaction boundaries matter - Application Services define unit of work scope
  6. DTO translation layer - Bridge between external world and domain model
  7. Use the terminology that fits your context - DDD for domain-driven teams, Use Cases for broader clarity

Further Reading

Primary Sources

  • Domain-Driven Design: Tackling Complexity in the Heart of Software - Eric Evans (2003) - Chapters on Application Layer and Services
  • Clean Architecture - Robert C. Martin (2017) - Chapter 20: Business Rules

Supporting Articles