Before diving in to what Domain Services are, let’s first under the problem they solve. Let’s recall one of the important rules of entities from our Aggregate Roots post.
An Aggregate and its child entities should never contain object references to other aggregates. This breaks transactional boundaries and promotes tight coupling, use reference by identity instead.
If you haven’t already go back and read about Aggregates Roots and Entities
If Aggregates and their child entities cannot contain object references to other Aggregates than how do we handle business actions that mutates multiple aggregates? This is where Domain Services come in. Domain Services are stateless objects that, generally speaking, encapsulate business rules spanning multiple aggregates. However, they don’t have to span multiple Aggregates all the time. They can be home for domain logic that doesn’t fit into an entity.
Understanding our Problem Space
Let’s define the business requirements in our problem space. You’re the proud owner of a pizza restaurant and you allow customers to build their own pizzas and place online orders. For simplicity, Orders can only contain one Pizza. Every Pizza requires a crust, sauce and cheese to be selected. When the customer places their order the cooks prepare and bake the pizza and when it’s cooked they inspect it for quality and if it passes inspection the order is ready for pickup. The order is not ready for pickup until the Pizza passes inspection.
Now, based on these requirements there are two Aggregates (nouns) in our domain, Pizza
and Order
. There’s also a good amount of behavior here. The Pizza
aggregate transitions through many different phases of the cooking process and when it passes inspection a sibling aggregate, Order
, needs to be ready for pickup. Now, the Pizza
aggregate may transition its own status and the Order
may transition its own status, but neither of these aggregates can transition the other. Doing so would require an object reference (and knowledge) of the other and that’s not cool!
Here’s a solution to the problem using a Domain Service.
Defining our Pizza Aggregate
First, let’s look at the Pizza
aggregate and it’s properties. Let’s remember first that Pizza
has to transition from Preparing -> Baking -> Baked -> InspectionPassed per our business rules.
public class Pizza : IAggregateRoot
{
public Guid Id { get; private set; }
public Crust Crust { get; private set; }
public Sauce Sauce { get; private set; }
public Cheese Cheese { get; private set; }
public PizzaStatus Status { get; private set; }
/// For simplicity, we only require a Crust selection when creating a Pizza instance
/// Normally I'd use a concrete Value Object type for Entity ID to reduce duplicate validation code. Leaving out for brevity.
public Pizza(Crust crust)
{
Id = Guid.NewGuid();
Status = PizzaStatus.Preparing;
Crust = crust;
}
public void SelectSauce(Sauce sauce)
{
if (Status != PizzaStatus.Preparing)
{
throw new PizzaStatusInvalid("Cannot change a pizza that's not being prepared.");
}
Sauce = sauce;
}
public void SelectCheese(Cheese cheese)
{
if (Status != PizzaStatus.Preparing)
{
throw new PizzaStatusInvalid("Cannot change a pizza that's not being prepared.");
}
Cheese = cheese;
}
// Transitions the pizza into a Baking state.
// The cook at the restaurant will use a POS system that calls this entity action.
public void StartBaking()
{
if (Status != PizzaStatus.Preparing)
{
throw new PizzaStatusInvalid("Cannot bake a pizza that's not being prepared.");
}
// We require sauce and cheese to be selected prior to baking.
if (cheese == Cheese.NotSpecified)
{
throw new InvalidPizzaCheese("You must select a cheese.");
}
if (sauce == Sauce.NotSpecified)
{
throw new InvalidPizzaSauce("You must select a sauce.");
}
Status = PizzaStatus.Baking;
DomainEvents.Raise(new PizzaBakingStartedEvent(Id));
}
// Transitions the pizza into a Baked state.
// The cook at the restaurant will use a POS system that calls this entity action. Alternatively an IOT enabled oven could call this via API.
public void BakingDone()
{
if (Status != PizzaStatus.Baking)
{
throw new PizzaStatusInvalid("Cannot finish baking a pizza that's not baking.");
}
Status = PizzaStatus.Baked;
DomainEvents.Raise(new PizzaBakingDoneEvent(Id));
}
// Transitions the pizza into an Inspection Passed state.
// The cook at the restaurant will use a POS system that calls this entity action.
public void InspectionPassed()
{
if (Status != PizzaStatus.Baked)
{
throw new PizzaStatusInvalid("Cannot inspect a pizza that's not baked.");
}
Status = PizzaStatus.InspectionPassed;
DomainEvents.Raise(new PizzaInspectionPassedEvent(Id));
}
}
public enum PizzaStatus
{
Preparing,
Baking,
Baked,
InspectionPassed
}
public enum Cheese
{
NotSpecified,
Cheddar
}
public enum Sauce
{
NotSpecified,
Tomato,
Barbeque
}
As you can see our Pizza
aggregate is pretty simple which is expected given our simple requirements. Did you notice how the public properties have private setters? This is one of the reasons why I love Aggregates and Entities as defined by DDD. Outsiders are prevented from modifying public properties directly and instead they have to go through entity methods.
Before we dive into the domain service let’s first review our last aggregate, the Order
.
Defining our Order Aggregate
Much like the Pizza
aggregate our Order
defines private property setters and forces consumers to modify state by calling entity methods. I hope the more you see this pattern the more it grows on you like it has me, I really think it improves code readability and suppleness.
public class Order : IAggregateRoot
{
public Guid Id { get; private set; }
public Guid PizzaId { get; private set; }
public OrderStatus Status { get; private set; }
/// For simplicity, Orders can have 1 pizza and pricing is left out
/// Normally I'd use a concrete Value Object type for Entity ID to reduce duplicate validation code. Leaving out for brevity.
public Order(Guid pizzaId)
{
Id = Guid.NewGuid();
Status = OrderStatus.Cooking;
PizzaId = pizzaId;
DomainEvents.Raise(new OrderCookingEvent(Id));
}
// Transitions the Order into a Ready For Pickup state.
// The cook at the restaurant will use a POS system that calls this entity action.
public void ReadyForPickup()
{
if (Status != OrderStatus.Cooking)
{
throw new OrderStatusInvalid("Cannot inspect an Order that's not cooking.");
}
Status = OrderStatus.ReadyForPickup;
DomainEvents.Raise(new OrderReadyForPickupEvent(Id));
}
}
public enum OrderStatus
{
Cooking,
ReadyForPickup
}
Not much explanation needed here. Now let’s dive into the domain service.
Defining the Domain Service
When the pizza passes inspection two things need to happen. The pizza needs to transition to InspectionPassed
and the Order needs to transition to ReadyForPickup
. Both of these actions are handled by the domain service below.
public class OrderReadyForPickupService
{
private IOrderRepository _orderRepository;
private IPizzaRepository _pizzaRepository;
public OrderReadyForPickupService(IOrderRepository orderRepository, IPizzaRepository pizzaRepository)
{
_orderRepository = orderRepository;
_pizzaRepository = pizzaRepository;
}
// Transitions the Pizza to Inspection Passed and Order to Ready For Pickup.
// The cook at the restaurant will use a POS system that calls an API endpoint that delegates to this domain service.
public void OrderReadyForPickup(Guid orderId, Guid pizzaId)
{
var order = _orderRepository.Get(orderId);
var pizza = _pizzaRepository.Get(pizzaId);
// Validate order and pizza are not null...
pizza.InspectionPassed();
order.ReadyForPickup();
// I admit, this domain service seems unnecessary, but only because our business rule only allows 1 pizza per order...
// However, in the real world orders could have multiple pizzas, so within this method we would fetch a list of all Pizzas associated to orderId. Then loop over each Pizza and assert their Status == PizzaStatus.InspectionPassed. Only then would we call order.ReadyForPickup()
// But that logic really depends on the business rules revolving around the pizza domain! If the cooks are expected to inspect each pizza in the order at the time of setting the Order to ReadyForPickup, then within this method we could loop over the pizzas and call pizza.InspectionPassed() instead of asserting Pizza.Status == PizzaStatus.InspectionPassed.
}
}
It’s pretty simple! Instantiate it with the Order and Pizza repositories to and then call its method and it will fetch both aggregates and call their necessary entity methods.
Wrapping Up Domain Services
One more thing. You may have noticed in the domain service there’s no call to a repository or unit of work to save changes to the database. This is by design and for good reason! Don’t pollute the Domain Service with the notion of database transactions. What if your Repository implementation today uses a transactional database, but next week your team realizes it’s better to use NoSQL? A change like that requires modifying the domain service, all because you changed the storage layer, yuck!
Ideally another class like a MVC [Web API] controller will call this domain service and afterwards the controller would commit the database transaction if applicable.
Before you go I want to leave you with this question. Where is the best place to call your database save changes action? As I mentioned previously the MVC controller can do this, but controllers are considered to be in the “presentation” layer and it’s possible applications will have multiple presentation layers which would result in duplicate code.