Domain-Driven Design – Integrating Aggregate Root with Open Host Service (OHS)

aggregatedomain-driven-designdomain-model

Here is the case: according to ubiquitous language – payment can be sent for processing to payment gateway (which is in different bounded context integrated via ACL (anti-corruption layer) + OHS/PL)

In local context, payment is aggregate root so I would expect design like this:

payment.send();

but the problem here is that payment needs to know how to send its state to other bounded context via ACL to OHS. So I need to inject this PaymentGatewayService/Adapter as argument:

payment.send(paymentGatewayService);

In this case paymentGatewayService returns some value object that can be processed with payment inside the send() method. But
I heard that this is not quite good practice to inject services to aggregate root via method arguments or DI, but maybe it's OK? The benefit here is that I'm not exposing aggregate structure to outside. Or should I create some domain service to cover it like:

Payment payment = ...
payment = paymentGatewayService.send(payment);
paymentRepository.store(payment);

In this case, I don't really like this inversion, because I must directly expose aggregate structure so gateway service can translate to its model.
Also this is causing anemia in payment aggregate.

How can I handle such situations properly??

Best Answer

But I heard that this is not quite good practice to inject services to aggregate root via method arguments or DI, but maybe it's OK?

There are trade offs.

Evans describes domain services as a construct to serve this role. So we pass to the aggregate an adapter that expresses the gateway concept in the domain language, so that the model can interact with it. The underlying implementation of the domain service probably delegates the actual work to an application service or an infrastructure service.

So, done correctly, it is completely consistent with the domain focused approach.

But it introduces some coupling between the side effect and the aggregate that you may not want

  • the failure modes of the side effect now pass through the aggregate
  • the aggregate and the side effect are coupled in time

Let's take a more careful look under the covers of your first example

payment.send(paymentGatewayService);

And try to break down explicitly what is going on

Payment::send(paymentGatewayService):
    x = this.getCurrentState()
    m = createPaymentMessage(x)
    y = paymentGatewayService.send(m)
    z = calculateNewState(x, y, m)
    this.setState(z)

Which is to say, that what we have here are three steps

  • we query the aggregate to discover the appropriate message to send, without touching the payment gateway
  • we dispatch the message to the payment gateway, without touching the aggregate
  • we update the aggregate using the response from the gateway as an input

and the trade off is: do we want those side effects in the aggregate, in the application, or possibly tucked into a different domain service that is responsible for the coordination?

PaymentProtocol::send(payment, paymentGateway):
    m = payment.createPaymentMessage()
    y = paymentGatewayService.send(m)
    payment.onPaymentSent(y)

In this design, the payment protocol orchestrates the side effects on the payment and the payment gateway. This gives you a clean aggregate (its responsibility is maintaining its own invariant), and keeps all of the process located in a single place.

In this case, I don't really like this inversion, because I must directly expose aggregate structure so gateway service can translate to its model. Also this is causing anemia in payment aggregate.

That's not quite right - the aggregate is still completely responsible for maintaining its own invariant; it's just absolved of the responsibility for managing a side effect that it is interested in. The aggregate is never exposing its own structure, it's only ever exposing a message that it created using its own structure.

It's still OK not to like it, of course.

Related Topic