Domain Service
A Domain Service is used to encapsulate domain logic that does not naturally belong to an entity or value object. It is stateless and handles business rules that span multiple aggregates or entities.
What is a Domain Service
Section titled “What is a Domain Service”Domain services have the following characteristics:
- Stateless: do not hold business state, only contain business logic
- Operate on multiple objects: usually involve multiple entities or aggregates
- Business logic: contain important domain concepts and rules
- Interface marker: implement the
IDomainServiceinterface
Defining a Domain Service
Section titled “Defining a Domain Service”using MiCake.DDD.Domain;
// Pricing servicepublic class PricingService : IDomainService{ public Money CalculateTotalPrice(Order order, Customer customer) { var subtotal = order.GetSubtotal();
// Apply the customer discount var discount = customer.GetDiscount(); var discountAmount = subtotal.Multiply(discount.AsDecimal());
// Calculate tax var taxRate = GetTaxRate(order.ShippingAddress); var taxAmount = subtotal.Subtract(discountAmount).Multiply(taxRate);
// Calculate shipping var shippingCost = CalculateShipping(order, customer);
return subtotal .Subtract(discountAmount) .Add(taxAmount) .Add(shippingCost); }
private decimal GetTaxRate(Address address) { // Calculate the tax rate based on the address return address.Country == "CN" ? 0.13m : 0.20m; }
private Money CalculateShipping(Order order, Customer customer) { if (customer.IsPremium) return Money.Zero("CNY");
return new Money(10.00m, "CNY"); }}
// Transfer servicepublic class TransferService : IDomainService{ public void Transfer(BankAccount from, BankAccount to, Money amount) { if (from.Currency != amount.Currency || to.Currency != amount.Currency) throw new DomainException("Currency mismatch");
if (from.Balance < amount) throw new DomainException("Insufficient balance");
from.Withdraw(amount); to.Deposit(amount); }}Domain Service vs Application Service
Section titled “Domain Service vs Application Service”Domain Service
Section titled “Domain Service”// A domain service: contains business rulespublic class OrderPricingService : IDomainService{ // The business rule for calculating the order price public Money CalculateOrderTotal(Order order, List<Promotion> promotions) { var total = order.GetSubtotal();
foreach (var promotion in promotions.Where(p => p.AppliesTo(order))) { total = promotion.Apply(total); }
return total; }}Application Service
Section titled “Application Service”// An application service: coordinates domain objects and infrastructurepublic class OrderApplicationService{ private readonly IRepository<Order, int> _orderRepository; private readonly IRepository<Customer, int> _customerRepository; private readonly OrderPricingService _pricingService; // Uses the domain service
public async Task<OrderDto> CreateOrder(CreateOrderCommand command) { // 1. Load the aggregate var customer = await _customerRepository.FindAsync(command.CustomerId);
// 2. Create the order var order = Order.Create(customer.Id); foreach (var item in command.Items) { order.AddItem(item.ProductId, item.Quantity, item.Price); }
// 3. Use the domain service to calculate the price var total = _pricingService.CalculateOrderTotal(order, customer.GetActivePromotions()); order.SetTotalAmount(total);
// 4. Persist (committed by the ambient UoW) await _orderRepository.AddAsync(order); await _unitOfWork.CommitAsync();
// 5. Return a DTO return MapToDto(order); }}When to Use a Domain Service
Section titled “When to Use a Domain Service”✅ Should Use a Domain Service
Section titled “✅ Should Use a Domain Service”- The operation involves multiple aggregates:
public class OrderFulfillmentService : IDomainService{ public bool CanFulfillOrder(Order order, List<Product> products) { // Check the inventory of multiple products foreach (var item in order.Items) { var product = products.First(p => p.Id == item.ProductId); if (product.Stock < item.Quantity) return false; } return true; }}- Complex business calculations:
public class ShippingCostCalculator : IDomainService{ public Money Calculate(Order order, Address from, Address to) { var distance = CalculateDistance(from, to); var weight = order.GetTotalWeight(); var dimensions = order.GetTotalDimensions();
// Complex shipping cost calculation logic return CalculateShippingCost(distance, weight, dimensions); }}- Business rule validation:
public class CreditCheckService : IDomainService{ public bool CheckCredit(Customer customer, Money amount) { var creditLimit = customer.GetCreditLimit(); var outstandingBalance = customer.GetOutstandingBalance();
return outstandingBalance.Add(amount).Amount <= creditLimit.Amount; }}❌ Should Not Use a Domain Service
Section titled “❌ Should Not Use a Domain Service”- Logic that belongs to a single entity:
// ❌ Don't: this should be a method of Orderpublic class OrderService : IDomainService{ public void AddItemToOrder(Order order, int productId, int quantity) { order.AddItem(productId, quantity); }}
// ✅ Correct: implement it directly in Orderpublic class Order : AggregateRoot<int>{ public void AddItem(int productId, int quantity) { // Implementation logic }}- Pure technical operations:
// ❌ Don't: this is an infrastructure service, not a domain servicepublic class EmailSenderService : IDomainService{ public void SendEmail(string to, string subject, string body) { // Send the email }}
// ✅ Correct: put it in the infrastructure layerpublic interface IEmailService{ Task SendEmailAsync(string to, string subject, string body);}Dependency Injection
Section titled “Dependency Injection”Domain services are used through dependency injection:
// Marked as a scoped service and registered automaticallypublic class PricingService : IDomainService, IScopedService{ private readonly ITaxRateProvider _taxRateProvider;
public PricingService(ITaxRateProvider taxRateProvider) { _taxRateProvider = taxRateProvider; }
public Money CalculateTotal(Order order) { var subtotal = order.GetSubtotal(); var taxRate = _taxRateProvider.GetRate(order.ShippingAddress); return subtotal.Multiply(1 + taxRate); }}
// Used in an application servicepublic class OrderApplicationService{ private readonly PricingService _pricingService;
public OrderApplicationService(PricingService pricingService) { _pricingService = pricingService; }
public async Task ProcessOrder(int orderId) { var order = await _orderRepository.FindAsync(orderId); var total = _pricingService.CalculateTotal(order); order.SetTotal(total); await _unitOfWork.CommitAsync(); }}Best Practices
Section titled “Best Practices”1. Keep It Stateless
Section titled “1. Keep It Stateless”// ✅ Correct - statelesspublic class DiscountCalculator : IDomainService{ public Money Calculate(Order order, Customer customer) { // Does not save any state return order.GetSubtotal().Multiply(customer.DiscountRate); }}
// ❌ Wrong - statefulpublic class DiscountCalculator : IDomainService{ private Order _currentOrder; // Do not save state
public void SetOrder(Order order) { _currentOrder = order; }}2. Use Clear Method Names
Section titled “2. Use Clear Method Names”public class OrderService : IDomainService{ // ✅ Clear business meaning public bool CanBeCancelled(Order order) { } public Money CalculateTotalWithTax(Order order, Address address) { } public bool MeetsMinimumOrderValue(Order order) { }}3. Depend on Interfaces Rather Than Implementations
Section titled “3. Depend on Interfaces Rather Than Implementations”public class PricingService : IDomainService{ private readonly ITaxRateProvider _taxRateProvider; // Interface private readonly IShippingCalculator _shippingCalculator; // Interface
public PricingService( ITaxRateProvider taxRateProvider, IShippingCalculator shippingCalculator) { _taxRateProvider = taxRateProvider; _shippingCalculator = shippingCalculator; }}Real-World Example
Section titled “Real-World Example”An Order Pricing Service
Section titled “An Order Pricing Service”public class OrderPricingService : IDomainService, IScopedService{ private readonly ITaxRateProvider _taxRateProvider; private readonly IShippingCalculator _shippingCalculator;
public OrderPricingService( ITaxRateProvider taxRateProvider, IShippingCalculator shippingCalculator) { _taxRateProvider = taxRateProvider; _shippingCalculator = shippingCalculator; }
public OrderPricing CalculatePricing(Order order, Customer customer) { // 1. Item subtotal var subtotal = CalculateSubtotal(order);
// 2. Apply the discount var discount = CalculateDiscount(subtotal, customer);
// 3. Calculate tax var taxAmount = CalculateTax(subtotal - discount, order.ShippingAddress);
// 4. Calculate shipping var shippingCost = _shippingCalculator.Calculate( order, customer.ShippingAddress );
return new OrderPricing { Subtotal = subtotal, Discount = discount, Tax = taxAmount, Shipping = shippingCost, Total = subtotal - discount + taxAmount + shippingCost }; }
private Money CalculateSubtotal(Order order) { return order.Items .Select(i => i.Price.Multiply(i.Quantity)) .Aggregate((a, b) => a.Add(b)); }
private Money CalculateDiscount(Money subtotal, Customer customer) { var discountRate = customer.GetDiscountRate(); return subtotal.Multiply(discountRate.AsDecimal()); }
private Money CalculateTax(Money amount, Address address) { var taxRate = _taxRateProvider.GetRate(address); return amount.Multiply(taxRate); }}Summary
Section titled “Summary”Domain services are used to encapsulate business logic that does not belong to entities or value objects:
- Implement the
IDomainServiceinterface - Keep them stateless
- Handle operations that span multiple aggregates
- Contain complex business rules
- Used through dependency injection
Next steps:
- Learn about Unit of Work to understand transaction management
- Read about Aggregate Roots to understand aggregate design
- Check out Repositories to learn about data access
