Application Services: Use Case Orchestration
Application Services orchestrate use cases by coordinating domain objects, repositories, and infrastructure services. They're the entry point for application behavior but contain no domain logic themselves.
TL;DR
| Concept | Definition |
|---|---|
| Application Service | Orchestrates use cases, coordinates components |
| Responsibilities | Load aggregates, invoke domain logic, save, publish events |
| Contains | Orchestration, transaction management, authorization |
| Does NOT Contain | Domain logic (that goes in domain layer) |
Application Service Responsibilities
What Application Services DO:
- Accept commands/queries from presentation layer
- Authorize the request
- Load aggregates from repositories
- Invoke domain methods on aggregates
- Save aggregates via repositories
- Publish domain events
- Return results or DTOs
What Application Services DON'T DO:
- ❌ Contain domain logic
- ❌ Make domain decisions
- ❌ Directly manipulate entity state
- ❌ Know about UI concerns
Basic Structure
public class OrderApplicationService
{
private readonly IOrderRepository _orderRepository;
private readonly ICustomerRepository _customerRepository;
private readonly IPricingService _pricingService;
private readonly IEventPublisher _eventPublisher;
private readonly IUnitOfWork _unitOfWork;
public OrderApplicationService(
IOrderRepository orderRepository,
ICustomerRepository customerRepository,
IPricingService pricingService,
IEventPublisher eventPublisher,
IUnitOfWork unitOfWork)
{
_orderRepository = orderRepository;
_customerRepository = customerRepository;
_pricingService = pricingService;
_eventPublisher = eventPublisher;
_unitOfWork = unitOfWork;
}
public async Task<OrderId> PlaceOrder(PlaceOrderCommand command)
{
// 1. Load aggregates
var customer = await _customerRepository.GetById(command.CustomerId);
if (customer == null)
throw new CustomerNotFoundException(command.CustomerId);
// 2. Create aggregate (factory method)
var order = Order.Create(
customer.Id,
command.ShippingAddress);
// 3. Add items (domain logic in aggregate)
foreach (var item in command.Items)
{
var price = await _pricingService.GetPrice(
item.ProductId,
customer,
item.Quantity);
order.AddLine(item.ProductId, item.Quantity, price);
}
// 4. Execute domain operation
order.Place();
// 5. Save
await _orderRepository.Save(order);
await _unitOfWork.Commit();
// 6. Publish events
foreach (var @event in order.DomainEvents)
{
await _eventPublisher.Publish(@event);
}
// 7. Return result
return order.Id;
}
}
Command Pattern
Command Definition
// Commands are simple DTOs
public record PlaceOrderCommand(
CustomerId CustomerId,
Address ShippingAddress,
List<OrderItemDto> Items
);
public record OrderItemDto(
ProductId ProductId,
int Quantity
);
public record CancelOrderCommand(
OrderId OrderId,
string Reason
);
public record UpdateShippingAddressCommand(
OrderId OrderId,
Address NewAddress
);
Command Handler Pattern
public interface ICommandHandler<TCommand, TResult>
{
Task<TResult> Handle(TCommand command, CancellationToken ct = default);
}
public class PlaceOrderHandler : ICommandHandler<PlaceOrderCommand, OrderId>
{
private readonly IOrderRepository _orderRepository;
// ... other dependencies
public async Task<OrderId> Handle(PlaceOrderCommand command, CancellationToken ct)
{
// Same logic as before
var order = Order.Create(command.CustomerId, command.ShippingAddress);
// ...
return order.Id;
}
}
// Usage with MediatR
public class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
[HttpPost]
public async Task<ActionResult<OrderId>> PlaceOrder(PlaceOrderCommand command)
{
var orderId = await _mediator.Send(command);
return Ok(orderId);
}
}
Complete Examples
Example 1: Order Lifecycle
public class OrderApplicationService
{
public async Task<OrderId> PlaceOrder(PlaceOrderCommand command)
{
var customer = await _customerRepository.GetById(command.CustomerId)
?? throw new CustomerNotFoundException(command.CustomerId);
var order = Order.Create(customer.Id, command.ShippingAddress);
foreach (var item in command.Items)
{
var price = await _pricingService.GetPrice(item.ProductId, customer);
order.AddLine(item.ProductId, item.Quantity, price);
}
order.Place();
await _orderRepository.Save(order);
return order.Id;
}
public async Task ShipOrder(ShipOrderCommand command)
{
var order = await _orderRepository.GetById(command.OrderId)
?? throw new OrderNotFoundException(command.OrderId);
// Domain logic in aggregate
order.Ship(command.TrackingNumber);
await _orderRepository.Save(order);
}
public async Task CancelOrder(CancelOrderCommand command)
{
var order = await _orderRepository.GetById(command.OrderId)
?? throw new OrderNotFoundException(command.OrderId);
// Domain logic in aggregate
order.Cancel(command.Reason);
await _orderRepository.Save(order);
}
public async Task UpdateShippingAddress(UpdateShippingAddressCommand command)
{
var order = await _orderRepository.GetById(command.OrderId)
?? throw new OrderNotFoundException(command.OrderId);
// Domain logic in aggregate
order.UpdateShippingAddress(command.NewAddress);
await _orderRepository.Save(order);
}
}
Example 2: Cross-Aggregate Operation
public class TransferApplicationService
{
private readonly IAccountRepository _accountRepository;
private readonly IMoneyTransferService _transferService;
private readonly IUnitOfWork _unitOfWork;
public async Task<TransferResult> TransferMoney(TransferMoneyCommand command)
{
// Load both aggregates
var sourceAccount = await _accountRepository.GetById(command.SourceAccountId)
?? throw new AccountNotFoundException(command.SourceAccountId);
var destinationAccount = await _accountRepository.GetById(command.DestinationAccountId)
?? throw new AccountNotFoundException(command.DestinationAccountId);
// Use domain service for cross-aggregate logic
var result = _transferService.Transfer(
sourceAccount,
destinationAccount,
command.Amount);
if (!result.IsSuccess)
return result;
// Save both aggregates
await _accountRepository.Save(sourceAccount);
await _accountRepository.Save(destinationAccount);
await _unitOfWork.Commit();
return result;
}
}
Example 3: With Authorization
public class OrderApplicationService
{
private readonly ICurrentUserService _currentUser;
private readonly IAuthorizationService _authorizationService;
public async Task CancelOrder(CancelOrderCommand command)
{
var order = await _orderRepository.GetById(command.OrderId)
?? throw new OrderNotFoundException(command.OrderId);
// Authorization check
var authResult = await _authorizationService.AuthorizeAsync(
_currentUser.User,
order,
OrderOperations.Cancel);
if (!authResult.Succeeded)
throw new UnauthorizedException("Cannot cancel this order");
order.Cancel(command.Reason);
await _orderRepository.Save(order);
}
}
Example 4: With Validation
public class OrderApplicationService
{
private readonly IValidator<PlaceOrderCommand> _validator;
public async Task<OrderId> PlaceOrder(PlaceOrderCommand command)
{
// Validate command
var validationResult = await _validator.ValidateAsync(command);
if (!validationResult.IsValid)
throw new ValidationException(validationResult.Errors);
// Proceed with use case
var order = Order.Create(command.CustomerId, command.ShippingAddress);
// ...
}
}
// Validator (using FluentValidation)
public class PlaceOrderCommandValidator : AbstractValidator<PlaceOrderCommand>
{
public PlaceOrderCommandValidator()
{
RuleFor(x => x.CustomerId).NotEmpty();
RuleFor(x => x.ShippingAddress).NotNull();
RuleFor(x => x.Items).NotEmpty()
.WithMessage("Order must have at least one item");
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.Quantity).GreaterThan(0);
});
}
}
Query Services (CQRS)
For reads, use separate query services:
// Query service - bypasses domain model
public class OrderQueryService
{
private readonly IDbConnection _connection;
public async Task<OrderDetailsDto?> GetOrderDetails(OrderId orderId)
{
const string sql = @"
SELECT o.Id, o.Status, o.Total, o.CreatedAt,
c.Name as CustomerName, c.Email as CustomerEmail
FROM Orders o
JOIN Customers c ON c.Id = o.CustomerId
WHERE o.Id = @OrderId";
return await _connection.QuerySingleOrDefaultAsync<OrderDetailsDto>(
sql,
new { OrderId = orderId.Value });
}
public async Task<PagedResult<OrderListItemDto>> GetOrders(
OrderListQuery query)
{
// Direct database query, returns DTOs
// No domain objects involved
}
}
// DTOs for queries
public record OrderDetailsDto(
Guid Id,
string Status,
decimal Total,
DateTime CreatedAt,
string CustomerName,
string CustomerEmail
);
Error Handling
public class OrderApplicationService
{
public async Task<Result<OrderId>> PlaceOrder(PlaceOrderCommand command)
{
try
{
var customer = await _customerRepository.GetById(command.CustomerId);
if (customer == null)
return Result<OrderId>.Failure(Error.NotFound("Customer not found"));
var order = Order.Create(customer.Id, command.ShippingAddress);
foreach (var item in command.Items)
{
var priceResult = await _pricingService.GetPrice(item.ProductId);
if (priceResult.IsFailure)
return Result<OrderId>.Failure(priceResult.Error);
order.AddLine(item.ProductId, item.Quantity, priceResult.Value);
}
order.Place();
await _orderRepository.Save(order);
return Result<OrderId>.Success(order.Id);
}
catch (DomainException ex)
{
return Result<OrderId>.Failure(Error.Domain(ex.Message));
}
}
}
// Result type
public class Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public Error? Error { get; }
public static Result<T> Success(T value) => new(true, value, null);
public static Result<T> Failure(Error error) => new(false, default, error);
}
Common Mistakes
Mistake 1: Domain Logic in Application Service
// ❌ Domain logic in application service
public class OrderApplicationService
{
public async Task PlaceOrder(PlaceOrderCommand command)
{
var order = await _orderRepository.GetById(command.OrderId);
// Domain logic leaked into application service!
if (order.Status != OrderStatus.Draft)
throw new InvalidOperationException("Only draft orders can be placed");
if (!order.Lines.Any())
throw new InvalidOperationException("Cannot place empty order");
order.Status = OrderStatus.Placed;
order.PlacedAt = DateTime.UtcNow;
// ...
}
}
// ✅ Domain logic in aggregate
public class Order
{
public void Place()
{
if (Status != OrderStatus.Draft)
throw new InvalidOrderStateException("Only draft orders can be placed");
if (!_lines.Any())
throw new CannotPlaceEmptyOrderException();
Status = OrderStatus.Placed;
PlacedAt = DateTime.UtcNow;
AddDomainEvent(new OrderPlaced(Id));
}
}
public class OrderApplicationService
{
public async Task PlaceOrder(PlaceOrderCommand command)
{
var order = await _orderRepository.GetById(command.OrderId);
order.Place(); // Domain logic encapsulated
await _orderRepository.Save(order);
}
}
Mistake 2: Too Many Dependencies
// ❌ Application service doing too much
public class OrderApplicationService
{
public OrderApplicationService(
IOrderRepository orderRepository,
ICustomerRepository customerRepository,
IProductRepository productRepository,
IInventoryRepository inventoryRepository,
IPaymentService paymentService,
IShippingService shippingService,
IEmailService emailService,
IAnalyticsService analyticsService,
ILogger logger,
// 10 more dependencies...
)
public async Task PlaceOrder(...)
{
// Does everything in one method
}
}
// ✅ Split into focused services + use events
public class PlaceOrderHandler : ICommandHandler<PlaceOrderCommand, OrderId>
{
// Only dependencies needed for placing order
private readonly IOrderRepository _orderRepository;
private readonly IPricingService _pricingService;
public async Task<OrderId> Handle(PlaceOrderCommand command)
{
var order = Order.Create(...);
order.Place(); // Emits OrderPlaced event
await _orderRepository.Save(order);
return order.Id;
}
}
// Other concerns handled by event handlers
public class SendOrderConfirmationHandler : IEventHandler<OrderPlaced> { }
public class ReserveInventoryHandler : IEventHandler<OrderPlaced> { }
public class TrackOrderAnalyticsHandler : IEventHandler<OrderPlaced> { }
Mistake 3: Returning Aggregates
// ❌ Returning domain object to presentation layer
public async Task<Order> GetOrder(OrderId orderId)
{
return await _orderRepository.GetById(orderId);
}
// ✅ Return DTO
public async Task<OrderDto> GetOrder(OrderId orderId)
{
var order = await _orderRepository.GetById(orderId);
return MapToDto(order);
}
// ✅ Or use query service
public async Task<OrderDto> GetOrder(OrderId orderId)
{
return await _orderQueryService.GetOrderDetails(orderId);
}
Staff+ Interview Questions
Q: What's the difference between application service and domain service?
A:
- Application Service: Orchestrates use cases. Loads aggregates, calls domain methods, saves. No domain logic.
- Domain Service: Contains domain logic that doesn't fit in entities. Stateless operations on domain objects.
// Application service - orchestration
public class OrderAppService
{
public async Task PlaceOrder(cmd)
{
var order = Order.Create(...);
var price = _pricingService.Calculate(...); // Domain service
order.AddLine(price);
order.Place();
await _repo.Save(order);
}
}
// Domain service - domain logic
public class PricingService
{
public Money Calculate(Product p, Customer c) { /* rules */ }
}
Q: How do you handle long-running operations in application services?
A: Options:
- Async messaging - Return immediately, process in background
- Saga/Process Manager - Coordinate multi-step operations
- Polling - Return operation ID, client polls for status
public async Task<OperationId> ProcessLargeOrder(LargeOrderCommand cmd)
{
var operationId = OperationId.New();
// Queue for background processing
await _messageQueue.Publish(new ProcessLargeOrderMessage(operationId, cmd));
return operationId; // Client can poll status
}
Quick Reference Card
┌─────────────────────────────────────────────────────────┐
│ APPLICATION SERVICE QUICK REFERENCE │
├─────────────────────────────────────────────────────────┤
│ │
│ RESPONSIBILITIES │
│ • Orchestrate use cases │
│ • Load aggregates from repositories │
│ • Invoke domain methods │
│ • Save aggregates │
│ • Publish events │
│ • Handle authorization │
│ │
│ DOES NOT │
│ • Contain domain logic │
│ • Make domain decisions │
│ • Manipulate entity state directly │
│ │
│ PATTERN │
│ 1. Receive command │
│ 2. Authorize │
│ 3. Load aggregates │
│ 4. Call domain methods │
│ 5. Save changes │
│ 6. Return result │
│ │
└─────────────────────────────────────────────────────────┘
Next Steps
- Factories - Complex object creation
- Domain Services - Domain logic outside entities
- Testing DDD Code - Testing application services