Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

InversionOfControl (Dependency Injection) in DDD Aggregates and Entities

My question is about what you guys are doing in practice to inject Domain- or InfrastructureServices into your DDD aggregates. I am convinced that it is in general a bad idea to allow DependencyInjection in DomainObjects, since from my experience it encourages rash developers to do nasty stuff with that. However for sure there are always exceptional cases where DependencyInjection in Domainobjects might make sense, especially when it is conducive for readability and simplicity. In my case I am trying to solve the problem of how to create new Ids of an aggregate's root.

Lets assume we have an UserAddresses aggregate having a List of Address Entities as its aggregate root. Let's further assume we have a method changeAddress(AddressId addressId, AddressChangeDto dto) in that aggregate:

public AddressId changeAddress(AddressId addressId, AddressChangeDto dto) {
    Address address = nullableAddress(addressId);

    if (address == null) {
        // Doesn't matter for this question
    } else if (address.hasChanged(dto)) {
        address = changeAddress(address, dto);            
    }
}

and a private changeAddress(Address address, AddressChangeDto dto) method simplified like this:

private Address changeAddress(Address address, AddressChangeDto dto) {
    if (!addressCopyNeeded(address)) {
        address.change(dto);
        return address;
    }

    // Address is both Shipping- & BillingAddress, hence it must be copied to 2 separate
    // entities.
    Address copiedAddress = address.copy(new AddressId()); // <-- New AddressId created
    ...
}

Now I want to refactor the AddressId creation to be created from a AddressIdFactory DomainService residing in my DomainModel backed by a MongoDbRepository residing in infrastructure, to create a performance optimized ObjectId from MongoDb, which I prefer over the long UUIDs.

One could solve that easily by adding that dependency as an formal parameter like this:

public AddressId changeAddress(AddressId addressId, AddressChangeDto dto, AddressIdFactory idFactory) {
    Address address = nullableAddress(addressId);

    if (address == null) {
        // Doesn't matter for this question
    } else if (address.hasChanged(dto)) {
        address = changeAddress(address, dto, idFactory);            
    }
}

private Address changeAddress(Address address, AddressChangeDto dto, AddressIdFactory idFactory) {
    if (!addressCopyNeeded(address)) {
        address.change(dto);
        return address;
    }

    // Address is both Shipping- & BillingAddress, hence it must be copied to 2 separate
    // entities.
    Address copiedAddress = address.copy(idFactory.nextAddressId());
    ...
}

However, I really do not like that Design at all, since it obviously confuses clients of that method on the very first sight: "Why do I have to pass the AddressIdFactory when I want to change an existing address?" The client doesn't, and and should not have to know what internally is going on and that there is the possibility that a new Address must be created in a certain constellation. This also leads to my next argument, we could also refactor the whole method you could say, however this will always lead to a design where the client is responsible to pass in a new AddressId or pass in a xyz DomainService as a method parameter, which is simply not optimal I think.

Because of that I am thinking of doing Dependency Injection for that very matter as an exceptional case to be able to keep the logic of "When" and "How" to create a new Address in the aggregate for sake of simplicity for the Aggregates' clients.

The question is how. Yes we are using Spring and no I cannot use just autowire the DomainService because the whole aggregate is an persisted Spring Data MongoDb object, where the DomainObject is deserialized from JSON during runtime.

@Aggregate
@Document(collection = "useraddresses)
public class UserAddresses extends Entity {

    // When reading from MongoDb the object's creation lays in the responsibility of 
    // Spring Data MongoDb, not Spring, therefore Spring cannot inject the dependency 
    // at all.         
    @Autowired
    private AddressIdFactory addressIdFactory;

    @PersistenceConstructor
    public UserAddresses(String id, Map<String, Address> userAddresses) {
    setUserAddressesId(new UserAddressesId(id));

    if (userAddresses != null) {
        this.userAddresses.putAll(userAddresses);
    }
}

Of course, I could inject that dependency manually in the Repository, by invoking a setter() or something, however this is ugly as well because I would have a public setter on my "beatiful" aggregate only for the purpose of a technical concern.

Another idea would be to use reflection to set the dependency directly to the private field, however also this is ugly and a slight performance drawback I guess.

I am wondering how your approach would look like?

like image 618
Stefano L Avatar asked Oct 18 '22 03:10

Stefano L


1 Answers

So the solution to that is "Inversion of Control", which is really just a pretentious way of saying "taking an argument".

I don't blame you for not liking the design at all; it does feel like implementation details of the model are getting leaked out to the application.

Part of the answer to the riddle is this: you can interfaces to insulate the application from the details of the implementation.

interface UserAddresses {
    AddressId changeAddress(AddressId addressId, AddressChangeDto dto);
}

So when the application loads "the aggregate" from the repository, it gets a something that implements this interface.

{
    UserAddresses root = repository.get(...)
    root.changeAddress(addressId, dto)
}

But it doesn't have to be the case that the application is talking directly to the "root entity" of the aggregate; the application could be talking to an adapter.

Adapter::changeAddress(AddressId addressId, AddressChangeDto dto) {
    this.target.changeAddress(addressId, dto, this.addressFactory);
}

And likewise "the" repository that the application is talking to is an adapter around the repository that accesses the data store, with additional plumbing that injects the address factory into the adapter.

like image 94
VoiceOfUnreason Avatar answered Oct 21 '22 03:10

VoiceOfUnreason