Skip to main content
The Intent.DomainServices module generates domain services that encapsulate business logic that doesn’t naturally fit within a single entity or value object.

Overview

Domain services handle:
  • Multi-entity operations
  • Complex business rules
  • Domain calculations
  • Third-party integrations affecting the domain
  • Operations requiring multiple aggregates

Installation

Intent.DomainServices

Key Concepts

When to Use Domain Services

Use domain services when:
  • Operation involves multiple entities
  • Logic doesn’t belong to any single entity
  • Stateless operations are needed
  • External services affect domain logic
Don’t use domain services for:
  • Simple CRUD operations (use Application Services)
  • Data access (use Repositories)
  • Infrastructure concerns (use Infrastructure Services)

Generation

Domain services are generated from the Domain Designer:
Domain Service
public interface IPricingService
{
    decimal CalculateOrderTotal(Order order);
    decimal ApplyDiscount(decimal amount, Customer customer);
    bool IsEligibleForFreeShipping(Order order, Address deliveryAddress);
}

public class PricingService : IPricingService
{
    public decimal CalculateOrderTotal(Order order)
    {
        var subtotal = order.Items.Sum(x => x.Price * x.Quantity);
        var tax = CalculateTax(subtotal, order.ShippingAddress);
        var shipping = CalculateShipping(order);
        
        return subtotal + tax + shipping;
    }

    public decimal ApplyDiscount(decimal amount, Customer customer)
    {
        if (customer.IsVIP)
            return amount * 0.9m; // 10% discount
        
        if (customer.TotalPurchases > 1000)
            return amount * 0.95m; // 5% discount
        
        return amount;
    }

    public bool IsEligibleForFreeShipping(
        Order order, 
        Address deliveryAddress)
    {
        if (order.Total >= 50)
            return true;
        
        if (deliveryAddress.IsLocal())
            return order.Total >= 25;
        
        return false;
    }

    private decimal CalculateTax(decimal amount, Address address)
    {
        // Tax calculation logic based on address
        throw new NotImplementedException();
    }

    private decimal CalculateShipping(Order order)
    {
        // Shipping calculation logic
        throw new NotImplementedException();
    }
}

Common Patterns

Transfer Operations

Transfer Service
public interface IAccountTransferService
{
    Task<TransferResult> TransferFundsAsync(
        Account fromAccount,
        Account toAccount,
        decimal amount);
}

public class AccountTransferService : IAccountTransferService
{
    private readonly IAccountRepository _accountRepository;
    private readonly IUnitOfWork _unitOfWork;

    public AccountTransferService(
        IAccountRepository accountRepository,
        IUnitOfWork unitOfWork)
    {
        _accountRepository = accountRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<TransferResult> TransferFundsAsync(
        Account fromAccount,
        Account toAccount,
        decimal amount)
    {
        // Validate
        if (fromAccount.Balance < amount)
            return TransferResult.InsufficientFunds;

        if (amount <= 0)
            return TransferResult.InvalidAmount;

        if (fromAccount.IsFrozen || toAccount.IsFrozen)
            return TransferResult.AccountFrozen;

        // Execute transfer
        fromAccount.Debit(amount);
        toAccount.Credit(amount);

        // Persist
        _accountRepository.Update(fromAccount);
        _accountRepository.Update(toAccount);
        await _unitOfWork.SaveChangesAsync();

        return TransferResult.Success;
    }
}

public enum TransferResult
{
    Success,
    InsufficientFunds,
    InvalidAmount,
    AccountFrozen
}

Validation Services

Validation Service
public interface IOrderValidationService
{
    ValidationResult ValidateOrder(Order order, Customer customer);
}

public class OrderValidationService : IOrderValidationService
{
    public ValidationResult ValidateOrder(Order order, Customer customer)
    {
        var result = new ValidationResult();

        // Validate customer credit
        if (customer.CreditLimit < customer.CurrentCredit + order.Total)
        {
            result.AddError("Customer credit limit exceeded");
        }

        // Validate order items
        if (!order.Items.Any())
        {
            result.AddError("Order must contain at least one item");
        }

        // Validate minimum order amount
        if (order.Total < 10)
        {
            result.AddError("Order total must be at least $10");
        }

        // Validate delivery address
        if (order.ShippingAddress == null)
        {
            result.AddError("Shipping address is required");
        }
        else if (!IsDeliverableAddress(order.ShippingAddress))
        {
            result.AddError("Cannot deliver to specified address");
        }

        return result;
    }

    private bool IsDeliverableAddress(Address address)
    {
        // Check if address is in serviceable area
        return true; // Simplified
    }
}

public class ValidationResult
{
    private readonly List<string> _errors = new();

    public bool IsValid => !_errors.Any();
    public IReadOnlyList<string> Errors => _errors.AsReadOnly();

    public void AddError(string error)
    {
        _errors.Add(error);
    }
}

Calculation Services

Calculation Service
public interface IShippingCalculatorService
{
    decimal CalculateShippingCost(
        Order order, 
        Address origin, 
        Address destination);
    DateTime EstimateDeliveryDate(
        Address origin, 
        Address destination, 
        ShippingMethod method);
}

public class ShippingCalculatorService : IShippingCalculatorService
{
    public decimal CalculateShippingCost(
        Order order,
        Address origin,
        Address destination)
    {
        var distance = CalculateDistance(origin, destination);
        var weight = order.Items.Sum(x => x.Weight * x.Quantity);
        
        var baseCost = distance * 0.1m;
        var weightCost = weight * 0.5m;
        
        var total = baseCost + weightCost;
        
        // Apply discounts
        if (order.Total >= 100)
            total *= 0.5m; // 50% off shipping
        else if (order.Total >= 50)
            total *= 0.75m; // 25% off shipping
        
        return Math.Max(total, 5.0m); // Minimum $5 shipping
    }

    public DateTime EstimateDeliveryDate(
        Address origin,
        Address destination,
        ShippingMethod method)
    {
        var distance = CalculateDistance(origin, destination);
        var baseDate = DateTime.Today;
        
        return method switch
        {
            ShippingMethod.Express => baseDate.AddDays(1),
            ShippingMethod.Standard => baseDate.AddDays(
                distance < 100 ? 3 : distance < 500 ? 5 : 7),
            ShippingMethod.Economy => baseDate.AddDays(
                distance < 100 ? 5 : distance < 500 ? 10 : 14),
            _ => throw new ArgumentException("Unknown shipping method")
        };
    }

    private double CalculateDistance(Address origin, Address destination)
    {
        // Implement distance calculation (e.g., using geocoding)
        return 100.0; // Simplified
    }
}

Policy Services

Policy Service
public interface IRefundPolicyService
{
    bool IsRefundable(Order order);
    decimal CalculateRefundAmount(Order order, DateTime refundDate);
    RefundMethod DetermineRefundMethod(Order order);
}

public class RefundPolicyService : IRefundPolicyService
{
    private const int StandardRefundWindowDays = 30;
    private const int ExtendedRefundWindowDays = 90;
    private const decimal RestockingFeePercentage = 0.15m;

    public bool IsRefundable(Order order)
    {
        if (order.Status == OrderStatus.Cancelled)
            return false;

        var daysSinceOrder = (DateTime.UtcNow - order.OrderDate).Days;
        var refundWindow = order.Customer.IsVIP 
            ? ExtendedRefundWindowDays 
            : StandardRefundWindowDays;

        return daysSinceOrder <= refundWindow;
    }

    public decimal CalculateRefundAmount(Order order, DateTime refundDate)
    {
        if (!IsRefundable(order))
            return 0;

        var daysSinceOrder = (refundDate - order.OrderDate).Days;
        var baseRefund = order.Total;

        // Apply restocking fee after 7 days (unless VIP)
        if (daysSinceOrder > 7 && !order.Customer.IsVIP)
        {
            baseRefund -= baseRefund * RestockingFeePercentage;
        }

        // Deduct shipping cost
        baseRefund -= order.ShippingCost;

        return Math.Max(baseRefund, 0);
    }

    public RefundMethod DetermineRefundMethod(Order order)
    {
        return order.PaymentMethod switch
        {
            PaymentMethod.CreditCard => RefundMethod.OriginalPayment,
            PaymentMethod.DebitCard => RefundMethod.OriginalPayment,
            PaymentMethod.Cash => RefundMethod.StoreCredit,
            PaymentMethod.StoreCredit => RefundMethod.StoreCredit,
            _ => RefundMethod.Check
        };
    }
}

Usage in Entities

Entities can use domain services:
Entity Using Service
public class Order
{
    public void CalculateTotal(IPricingService pricingService)
    {
        Total = pricingService.CalculateOrderTotal(this);
        
        if (pricingService.IsEligibleForFreeShipping(this, ShippingAddress))
        {
            ShippingCost = 0;
        }
    }

    public bool Validate(IOrderValidationService validationService, Customer customer)
    {
        var result = validationService.ValidateOrder(this, customer);
        return result.IsValid;
    }
}

Dependency Injection

Domain services are automatically registered:
DI Registration
services.AddScoped<IPricingService, PricingService>();
services.AddScoped<IAccountTransferService, AccountTransferService>();
services.AddScoped<IOrderValidationService, OrderValidationService>();
services.AddScoped<IShippingCalculatorService, ShippingCalculatorService>();
services.AddScoped<IRefundPolicyService, RefundPolicyService>();

Testing

Testing
public class PricingServiceTests
{
    [Fact]
    public void ApplyDiscount_VIPCustomer_Returns10PercentOff()
    {
        // Arrange
        var service = new PricingService();
        var customer = new Customer("John", "[email protected]") 
        { 
            IsVIP = true 
        };
        var amount = 100m;

        // Act
        var result = service.ApplyDiscount(amount, customer);

        // Assert
        Assert.Equal(90m, result);
    }

    [Fact]
    public void IsEligibleForFreeShipping_OrderOver50_ReturnsTrue()
    {
        // Arrange
        var service = new PricingService();
        var order = new Order { Total = 60m };
        var address = new Address("123 Main St", "City", "12345");

        // Act
        var result = service.IsEligibleForFreeShipping(order, address);

        // Assert
        Assert.True(result);
    }
}

Best Practices

Keep Stateless

Domain services should not maintain state between calls. Use entities for stateful operations.

Pure Logic

Focus on business logic, not infrastructure concerns like database access or external APIs.

Minimal Dependencies

Depend only on other domain objects and minimal infrastructure (like UnitOfWork).

Testability

Design for testability with clear inputs and outputs. Avoid hidden dependencies.

Domain Service vs Application Service

Domain ServiceApplication Service
Contains business logicOrchestrates use cases
Works with domain objectsWorks with DTOs
No infrastructure concernsCan access infrastructure
Reusable across use casesSpecific to one use case
Lives in Domain layerLives in Application layer

Additional Resources

Build docs developers (and LLMs) love