Robert
Robert

Reputation: 3553

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();
}

Upvotes: 0

Views: 373

Answers (3)

chikincrow
chikincrow

Reputation: 393

The accounting BC should always return the same accountingSoftwareId for a given invoiceId.

If in the first round, the call is made on the accounting BC but the invoice update fails, you have a state t1 in the account BC and a state t0 in the invoicing BC. When you retry the command, it will perform the same call and return the same id, and if the update is successful you are in t1 state in each BC. In the worst case, even if the command has to be resolved manually, the resulting account id would always be the same for a given invoice id.

So to resolve an accounting ID for a particular invoice, you could ask the accounting BC directly.

Upvotes: 0

VoiceOfUnreason
VoiceOfUnreason

Reputation: 57377

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.

Upvotes: 1

tomliversidge
tomliversidge

Reputation: 2369

My first thought was 'isn't invoicing part of accounting?' :)

option 1 is what I've tended to use in the past where my domain objects have behaviour.

I dont like option 2 as then Invoice needs a private reference to the repository.

A more general observation is that there doesn't seem to be much behaviour in the domain here - it seems to just be setting an id. Option 3 seems to capture this. I'm wondering if an application service would suffice and just coordinate the following

  1. Load the invoice
  2. Get an AccountingId
  3. Save it on the invoice

Which is pretty much option 3 above. I'd be tempted to pass in the repository and service though, but thats really just a more functional style - the above would work too with private fields.

Upvotes: 0

Related Questions