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.