Search code examples
domain-driven-designddd-service

DDD Domain services


I have an Invoice aggregate root which at some point can be sent to accounting external web service, and mark as sent by persisting some ID/number obtained from that service.

Which is the correct way to do it in DDD?

Here are my ideas:

First apprroach:

Have an invoice AggregateRoot with function SendToAccounting, and inject domain service / interface, which will send invoice to accounting, and retrieve some "id/code" in the accounting software, and set AccountingSoftwareId property

Invoice.SendToAccounting(IInvoiceDomain service)
{
     var accountingSoftwareID = service.getAccountingSoftwareId(this);
     this.AccountingSoftwareId = accountingSoftwareId;
}

///Implementation in the application service
    var invoice = _invoiceRepository.GetInvoiceById(id);
    invoice.SendToAccounting(someDomainService);
    _invoiceRepository.Update(invoice);
    _unitOfWork.Save();

Second approach:

Similar as first approach, but domain service should be responsible for persisting like this:

var invoice = _invoiceRepository.GetInvoiceById(id);
///unit of work save will be called inside this function
invoice.SendToAccounting(someDomainService);

Third approcach:

Domain service will be fully rensponsible to encapsulate this behavior

///Code inside domain service
public void SendInvoiceToAccounting(int invoiceId)
{
    var invoice =  _invoiceRepository.GetInvoiceById(invoiceId);
    string invoiceAccountingId = _accountingService.GetAccountingSoftwareId(invoice);
    invoice.SetAsSentToAccounting(invoiceAccountingId);
    _invoiceRepository.Update(invoice);
    _unitOfWork.Save();
}

Solution

  • Which is the correct way to do it in DDD?

    Your first approach is closest. The signature on your domain service should accept state as arguments, rather than the aggregate root itself.

    Invoice.SendToAccounting(IInvoiceDomain service)
    {
        var accountingSoftwareID = service.getAccountingSoftwareId(this.Id, ...);
        this.AccountingSoftwareId = accountingSoftwareId;
    }
    

    All of the arguments passed should be value types - the domain service shouldn't be able to change the state of the aggregate by manipulating its copy of the arguments, and it certainly shouldn't be able to run other commands on the aggregate.

    In a code review, I would reject the second approach you offer; from the point of view of the domain model, the domain service interface should only provide queries, not commands (in the CQS sense).

    In a code review, I would reject the third approach completely -- setters on aggregates are a code smell; the whole point is to encapsulate the state with the rules for updating it.

    BUT

    The design is somewhat alarming, in that you are making writes in two different places in the same transaction. In the happy path, it isn't a big deal, but what are you supposed to do if the command run on the accounting service succeeds, but the save of the updated invoice fails?

    Assuming that distributed transactions aren't appealing, you may want to review what Udi Dahan has to say about reliable messaging.