Domain Services are stateless operations that don’t naturally belong to entities or value objects, but are still part of the domain model.

⚠️ Warning: Avoid generic services named after entities (OrderService, CustomerService, ProductService). These are code smells that often indicate an anemic domain model. Domain services should have specific, verb-based names describing what they do.

When to Use Domain Services

Default Rule: Behavior belongs on entities and value objects. Only use domain services when the operation truly doesn’t belong to any single aggregate.

Operations Spanning Multiple Aggregates

When behavior involves multiple aggregates but doesn’t belong to any single one.

// Not right on Order or Customer
public class PricingService
{
    public Money CalculatePrice(Order order, Customer customer, List<Promotion> promotions)
    {
        var basePrice = order.CalculateSubtotal();
        var customerDiscount = customer.GetLoyaltyDiscount();
        var promotionalDiscount = promotions.GetBestDiscount(order);
 
        return basePrice.Apply(customerDiscount).Apply(promotionalDiscount);
    }
}

Domain Operations Without Natural Home

Operations that are clearly domain logic but don’t fit on an entity or value object.

Money transfer is the classic case: it belongs to neither Account, so a domain service is the right home for the decision. But the service must still respect the aggregate boundary — one aggregate per transaction. So it debits the source and lets a separate transaction handle the credit, driven by an event. (Mutating and saving both accounts in one call is the Multi-Aggregate Transactions anti-pattern.)

public class TransferMoneyService
{
    // The operation doesn't belong on either Account, and it must not
    // modify both aggregates in one transaction. A service has no event
    // list of its own, so the source aggregate records the fact: TransferOut
    // collects TransferInitiated, which the dispatcher fires after the commit
    // so a saga can credit the destination in its own transaction.
    public void Transfer(Account from, AccountId to, Money amount)
    {
        from.TransferOut(amount, to);
    }
}

Calculations or Algorithms

Complex domain calculations that would bloat entities.

public class ShippingCostCalculator
{
    public Money Calculate(Address destination, Weight packageWeight, ShippingMethod method)
    {
        // Complex shipping cost algorithm
        var distance = _distanceService.Calculate(destination);
        var baseRate = method.GetBaseRate();
        var weightMultiplier = CalculateWeightMultiplier(packageWeight);
 
        return baseRate.MultiplyBy(distance).MultiplyBy(weightMultiplier);
    }
}

Characteristics

Stateless

  • No internal state
  • Pure functions of inputs
  • Can be safely shared
// Good: Stateless
public class OrderValidationService
{
    public ValidationResult Validate(Order order)
    {
        // Uses only inputs, no instance state
        var result = new ValidationResult();
 
        if (!order.Lines.Any())
            result.AddError("Order must have at least one line");
 
        if (order.Total <= Money.Zero)
            result.AddError("Order total must be positive");
 
        return result;
    }
}
 
// Bad: Stateful
public class OrderValidationService
{
    private ValidationResult _lastResult;  // State! Bad!
 
    public ValidationResult Validate(Order order)
    {
        _lastResult = new ValidationResult();
        // ...
    }
}

Part of Domain Layer

  • Express domain concepts
  • Use ubiquitous language
  • Not infrastructure services
// Domain Service: Business logic
public class RiskAssessmentService
{
    public RiskLevel AssessRisk(Customer customer, Order order)
    {
        // Business rules about risk
        if (customer.IsNew && order.Total > Money.Of(1000))
            return RiskLevel.High;
 
        return RiskLevel.Low;
    }
}
 
// Application Service: Orchestration (NOT domain service)
public class PlaceOrderApplicationService
{
    public void PlaceOrder(PlaceOrderCommand command)
    {
        var order = _orderRepository.GetById(command.OrderId);
        order.Submit();
        _orderRepository.Save(order);
        _emailService.SendConfirmation(order);  // Infrastructure
    }
}

Named with Verbs

  • Describe what they do
  • TransferMoney, CalculatePrice, AssessRisk
  • Not OrderService, CustomerService (too generic)

Domain Service vs. Other Services

Domain Service

// Lives in Domain layer
// Contains business logic
// Works with domain objects
public class LoanApprovalService
{
    public ApprovalResult Approve(LoanApplication application, CreditReport report)
    {
        // Business rules for loan approval
        if (report.Score < 600)
            return ApprovalResult.Denied("Credit score too low");
 
        if (application.Amount > report.MaxLoanAmount)
            return ApprovalResult.Denied("Amount exceeds limit");
 
        return ApprovalResult.Approved();
    }
}

Application Service

// Lives in Application layer
// Orchestrates use cases
// Thin coordinator
public class LoanApplicationService
{
    private readonly ILoanRepository _loanRepository;
    private readonly LoanApprovalService _approvalService;  // Domain service
    private readonly INotificationService _notifications;   // Infrastructure
 
    public void ProcessLoanApplication(Guid applicationId)
    {
        var application = _loanRepository.GetById(applicationId);
        var creditReport = _creditService.GetReport(application.ApplicantId);
 
        var result = _approvalService.Approve(application, creditReport);
 
        if (result.IsApproved)
        {
            application.Approve();
            _notifications.SendApproval(application);
        }
        else
        {
            application.Deny(result.Reason);
            _notifications.SendDenial(application);
        }
 
        _loanRepository.Save(application);
    }
}

Infrastructure Service

// Lives in Infrastructure layer
// Technical concerns
// External integrations
public class EmailService : IEmailService
{
    private readonly SmtpClient _smtpClient;
 
    public void SendEmail(EmailAddress to, string subject, string body)
    {
        // SMTP, email server, etc.
        var message = new MailMessage();
        message.To.Add(to.Value);
        message.Subject = subject;
        message.Body = body;
 
        _smtpClient.Send(message);
    }
}

Examples

Currency Conversion

public class CurrencyConversionService
{
    private readonly IExchangeRateProvider _exchangeRates;
 
    public Money Convert(Money source, Currency targetCurrency)
    {
        if (source.Currency == targetCurrency)
            return source;
 
        var rate = _exchangeRates.GetRate(source.Currency, targetCurrency);
        var convertedAmount = source.Amount * rate;
 
        return new Money(convertedAmount, targetCurrency);
    }
}

Authorization

public class AuthorizationService
{
    public bool CanApprove(User user, PurchaseOrder order)
    {
        // Business rules for approval authority
        if (user.Role == Role.Manager && order.Total <= Money.Of(10000))
            return true;
 
        if (user.Role == Role.Director && order.Total <= Money.Of(50000))
            return true;
 
        if (user.Role == Role.CEO)
            return true;
 
        return false;
    }
}

Complex Validation

public class ProductCompatibilityService
{
    public bool AreCompatible(Product product1, Product product2)
    {
        // Complex business rules about product compatibility
        if (product1.Category != product2.Category)
            return false;
 
        if (product1.RequiredComponents.Intersect(product2.IncompatibleComponents).Any())
            return false;
 
        return true;
    }
}

Best Practices

Keep Logic in Entities When Possible

// Prefer: Logic in entity
public class Order
{
    public void ApplyDiscount(Discount discount)
    {
        if (!discount.AppliesTo(this))
            throw new InvalidOperationException("Discount not applicable");
 
        _appliedDiscounts.Add(discount);
    }
}
 
// Use service when: Involves multiple aggregates
public class DiscountValidationService
{
    public bool CanCombine(Discount discount1, Discount discount2)
    {
        // Business rules about combining discounts
        return !discount1.IsExclusive && !discount2.IsExclusive;
    }
}

Interface for Testing

public interface IShippingCostCalculator
{
    Money Calculate(Address destination, Weight weight);
}
 
public class ShippingCostCalculator : IShippingCostCalculator
{
    public Money Calculate(Address destination, Weight weight)
    {
        // Implementation
    }
}
 
// Easy to mock in tests
var mockCalculator = new Mock<IShippingCostCalculator>();
mockCalculator.Setup(c => c.Calculate(It.IsAny<Address>(), It.IsAny<Weight>()))
    .Returns(Money.Of(10));

Inject Dependencies

public class PricingService
{
    private readonly ICurrencyConversionService _currencyService;
    private readonly IPromotionRepository _promotions;
 
    public PricingService(
        ICurrencyConversionService currencyService,
        IPromotionRepository promotions)
    {
        _currencyService = currencyService;
        _promotions = promotions;
    }
 
    public Money CalculatePrice(Order order, Currency targetCurrency)
    {
        var price = order.CalculateSubtotal();
        var converted = _currencyService.Convert(price, targetCurrency);
        var promotions = _promotions.GetActiveForOrder(order);
 
        return ApplyPromotions(converted, promotions);
    }
}

Anti-Patterns

Generic Domain Services (OrderService, CustomerService, etc.)

The Problem: Services named after domain entities (OrderService, CustomerService, ProductService) are a code smell. They become dumping grounds for logic that should live elsewhere.

// BAD: Generic "OrderService" - anti-pattern!
public class OrderService
{
    // This should be on Order entity
    public void AddItem(Order order, Product product, int quantity)
    {
        order.Lines.Add(new OrderLine(...));
    }
 
    // This should be on Order entity
    public Money CalculateTotal(Order order)
    {
        return order.Lines.Sum(l => l.Price * l.Quantity);
    }
 
    // This is application layer concern
    public void CreateOrder(CreateOrderCommand command)
    {
        var order = new Order(...);
        _repository.Save(order);
        _emailService.SendConfirmation(order);
    }
 
    // This might be legitimate domain service, but wrong name
    public void TransferOrder(Order from, Customer to)
    {
        // Cross-aggregate operation
    }
}

The Fix: Put behavior on entities, use specific domain services, move orchestration to application layer.

// GOOD: Logic on entity
public class Order
{
    public void AddItem(Product product, int quantity)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Cannot modify submitted order");
 
        _lines.Add(new OrderLine(product, quantity));
    }
 
    public Money CalculateTotal()
    {
        return _lines.Sum(l => l.Subtotal);
    }
}
 
// GOOD: Specific domain service with verb-based name
public class OrderTransferService
{
    public void TransferOwnership(Order order, Customer newOwner)
    {
        // Cross-aggregate business logic
        if (!newOwner.CanAcceptOrder(order))
            throw new InvalidOperationException("Customer cannot accept order");
 
        // ChangeOwner records OrderOwnershipTransferred on the aggregate;
        // it's dispatched after the transaction commits.
        order.ChangeOwner(newOwner.Id);
    }
}
 
// GOOD: Application layer orchestration (NOT domain service)
public class PlaceOrderHandler
{
    public void Handle(PlaceOrderCommand command)
    {
        var order = _orderRepository.GetById(command.OrderId);
        order.Submit();  // Entity method
        _orderRepository.Save(order);
        _emailService.SendConfirmation(order);  // Infrastructure
    }
}

Anemic Domain Model Enabler

// Bad: Service doing entity's job
public class AccountService
{
    public void Withdraw(Account account, decimal amount)
    {
        if (account.Balance < amount)
            throw new Exception("Insufficient funds");
        account.Balance -= amount;  // Direct property manipulation!
    }
}
 
// Good: Rich entity
public class Account : AggregateRoot<AccountId>
{
    public void Withdraw(Money amount)
    {
        if (Balance < amount)
            throw new InsufficientFundsException();
 
        Balance = Balance.Subtract(amount);
        AddDomainEvent(new MoneyWithdrawn(Id, amount));
    }
}

God Service

// Bad: Generic service with everything
public class OrderService  // Name itself is a code smell!
{
    public void CreateOrder() { }
    public void AddItem() { }
    public void RemoveItem() { }
    public void ApplyDiscount() { }
    public void CalculateShipping() { }
    public void ProcessPayment() { }
    public void SendConfirmation() { }
    // ... 50 more methods that belong in different places
}
 
// Good: Focused, specific services with verb-based names
public class PricingCalculator { }  // Domain service
public class ShippingCostCalculator { }  // Domain service
public class OrderOwnershipTransferService { }  // Domain service

References