5

I have read Evans, Nilsson and McCarthy, amongst others, and understand the concepts and reasoning behind a domain driven design; however, I'm finding it difficult to put all of these together in a real-world application. The lack of complete examples has left me scratching my head. I've found a lot of frameworks and simple examples but nothing so far that really demonstrates how to build a real business application following a DDD.

Using the typical order management system as an example, take the case of order cancellation. In my design I can see an OrderCancellationService with a CancelOrder method which accepts the order # and a reason as parameters. It then has to perform the following 'steps':

  1. Verify that the current user has the necessary permission to cancel an Order
  2. Retrieve the Order entity with the specified order # from the OrderRepository
  3. Verify that the Order may be canceled (should the service interrogate the state of the Order to evaluate the rules or should the Order have a CanCancel property that encapsulates the rules?)
  4. Update the state of the Order entity by calling Order.Cancel(reason)
  5. Persist the updated Order to the data store
  6. Contact the CreditCardService to revert any credit card charges that have already been processed
  7. Add an audit entry for the operation

Of course, all of this should happen in a transaction and none of the operations should be allowed to occur independently. What I mean is, I must revert the credit card transaction if I cancel the order, I cannot cancel and not perform this step. This, imo, suggests better encapsulation but I don't want to have a dependency on the CreditCardService in my domain object (Order), so it seems like this is the responsibility of the domain service.

I am looking for someone to show me code examples how this could/should be "assembled". The thought-process behind the code would be helpful in getting me to connect all of the dots for myself. Thx!

jpm70
  • 51
  • 2

2 Answers2

2

Your domain service may look like this. Note that we want to keep as much logic as possible in the entities, keeping the domain service thin. Also note that there is no direct dependency on credit card or auditor implementation (DIP). We only depend on interfaces that are defined in our domain code. The implementation can later be injected in the application layer. Application layer would also be responsible for finding Order by number and, more importantly, for wrapping 'Cancel' call in a transaction (rolling back on exceptions).

    class OrderCancellationService {

    private readonly ICreditCardGateway _creditCardGateway;
    private readonly IAuditor _auditor;

    public OrderCancellationService(
        ICreditCardGateway creditCardGateway, 
        IAuditor auditor) {
        if (creditCardGateway == null) {
            throw new ArgumentNullException("creditCardGateway");
        }
        if (auditor == null) {
            throw new ArgumentNullException("auditor");
        }
        _creditCardGateway = creditCardGateway;
        _auditor = auditor;
    }

    public void Cancel(Order order) {
        if (order == null) {
            throw new ArgumentNullException("order");
        }
        // get current user through Ambient Context:
        // http://blogs.msdn.com/b/ploeh/archive/2007/07/23/ambientcontext.aspx
        if (!CurrentUser.CanCancelOrders()) {
            throw new InvalidOperationException(
              "Not enough permissions to cancel order. Use 'CanCancelOrders' to check.");
        }
        // try to keep as much domain logic in entities as possible
        if(!order.CanBeCancelled()) {
            throw new ArgumentException(
              "Order can not be cancelled. Use 'CanBeCancelled' to check.");
        }
        order.Cancel();

        // this can throw GatewayException that would be caught by the 
        // 'Cancel' caller and rollback the transaction
        _creditCardGateway.RevertChargesFor(order);

        _auditor.AuditCancellationFor(order);
    }
}
Dmitry
  • 15,996
  • 2
  • 37
  • 65
  • Why wouldn't I want the 'lookup', transaction management and call to persist changes within the service? It seems that would ensure proper usage every time. – SonOfPirate May 15 '12 at 12:08
  • Lookup - maybe. Transaction management does not belong to domain service, it is usually implemented at the application layer (caller of the domain service). 'Call to persist changes' is handled by ORM or UnitOfWork since we are changing existing objects no explicit call is need in NHibernate case. The idea is to keep domain code as persistence agnostic as possible. – Dmitry May 15 '12 at 13:00
  • Yea, I would use an OrderRepository and UoW to keep the domain as persistence agnostic as possible, but nothing prevents the application code from calling your cancellation service without persisting the changes to the Order entity. Being agnostic, I didn't think it mattered until now that we are not using NHibernate, so any assumptions based on that ORM are not valid. – SonOfPirate May 15 '12 at 13:26
  • I should also mention that I always work in a team environment where code has to be highly intentional and cannot make assumptions about the upstream coder (caller), such as whether they're implementing all of the rules. It was my impression that this is the role a domain service played in the design and that the application layer is the UI. Am I mistaken? – SonOfPirate May 15 '12 at 13:28
  • If you "cannot make assumptions about the upstream coder" then you can create an _application_ level service that would handle transactions. At this point your design would be driven by API best practices as oppose to DDD. The business logic is still encapsulated in domain service. You may find this interesting: http://www.tobinharris.com/past/2008/8/22/ddd-repositories-in-the-wild/ Or just google 'application service in DDD' in DDD. – Dmitry May 15 '12 at 18:49
  • "and that the application layer is the UI. Am I mistaken?" - Yes you are. The UI calls the Application layer. In client/server world, the application layer would be the web service. This would initiate a transaction and invoke methods on domain objects & services. – David Masters May 16 '12 at 08:59
  • Our current approach is to have the web services as thin facades delegating into the domain layer which can be shared by multiple web services. Thus the notion of having everything related to an operation encapsulated at a layer below the facade. My guess is that you would still have an application service and the facade delegates to that service, yes? – SonOfPirate May 16 '12 at 12:14
  • 1
    Yes, that would be fine. The point is that the 'Application Service' is the entry point to your domain model/services. This level is were transactions are orchestrated. I personally manage user permissions at this level too so that my model purely focuses on domain logic. – David Masters May 17 '12 at 08:24
2

A slightly different take on it:

//UI
public class OrderController
{
    private readonly IApplicationService _applicationService;

    [HttpPost]
    public ActionResult CancelOrder(CancelOrderViewModel viewModel)
    {
        _applicationService.CancelOrder(new CancelOrderCommand
        {
            OrderId = viewModel.OrderId,
            UserChangedTheirMind = viewModel.UserChangedTheirMind,
            UserFoundItemCheaperElsewhere = viewModel.UserFoundItemCheaperElsewhere
        });

        return RedirectToAction("CancelledSucessfully");
    }
}

//App Service
public class ApplicationService : IApplicationService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IPaymentGateway _paymentGateway;

    //provided by DI
    public ApplicationService(IOrderRepository orderRepository, IPaymentGateway paymentGateway)
    {
        _orderRepository = orderRepository;
        _paymentGateway = paymentGateway;
    }

    [RequiredPermission(PermissionNames.CancelOrder)]
    public void CancelOrder(CancelOrderCommand command)
    {
        using (IUnitOfWork unitOfWork = UnitOfWorkFactory.Create())
        {
            Order order = _orderRepository.GetById(command.OrderId);

            if (!order.CanBeCancelled())
                throw new InvalidOperationException("The order cannot be cancelled");

            if (command.UserChangedTheirMind)
                order.Cancel(CancellationReason.UserChangeTheirMind);
            if (command.UserFoundItemCheaperElsewhere)
                order.Cancel(CancellationReason.UserFoundItemCheaperElsewhere);

            _orderRepository.Save(order);

            _paymentGateway.RevertCharges(order.PaymentAuthorisationCode, order.Amount);
        }
    }
}

Notes:

  • In general I only see the need for a domain service when a command/use case involves the state change of more than one aggregate. For example, if I needed to invoke methods on the Customer aggregate as well as Order, then I'd create the domain service OrderCancellationService that invoked the methods on both aggregates.
  • The application layer orchestrates between infrastructure (payment gateways) and the domain. Like domain objects, domain services should only be concerned with domain logic, and ignorant of infrastructure such as payment gateways; even if you've abstracted it using your own adapter.
  • With regards to permissions, I would use aspect oriented programming to extract this away from the logic itself. As you see in my example, I've added an attribute to the CancelOrder method. You can use an intercepter on that method to see if the current user (which I would set on Thread.CurrentPrincipal) has that permission.
  • With regards to auditing, you simply said 'audit for the operation'. If you just mean auditing in general, (i.e. for all app service calls), again I would use interceptors on the method, logging the user, which method was called, and with what parameters. If however you meant auditing specifically for the cancellation of orders/payments then do something similar to Dmitry's example.
David Masters
  • 7,556
  • 1
  • 37
  • 65