Skip to main content

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

ConceptDefinition
Application ServiceOrchestrates use cases, coordinates components
ResponsibilitiesLoad aggregates, invoke domain logic, save, publish events
ContainsOrchestration, transaction management, authorization
Does NOT ContainDomain logic (that goes in domain layer)

Application Service Responsibilities

What Application Services DO:

  1. Accept commands/queries from presentation layer
  2. Authorize the request
  3. Load aggregates from repositories
  4. Invoke domain methods on aggregates
  5. Save aggregates via repositories
  6. Publish domain events
  7. Return results or DTOs

What Application Services DON'T DO:

  1. ❌ Contain domain logic
  2. ❌ Make domain decisions
  3. ❌ Directly manipulate entity state
  4. ❌ 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:

  1. Async messaging - Return immediately, process in background
  2. Saga/Process Manager - Coordinate multi-step operations
  3. 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