How to Validate References Between Aggregates in DDD

domain-driven-design

I'm struggling a bit with referencing between aggregates. Let's assume the aggregate Car has a reference to the aggregate Driver. This reference will be modelled by having Car.driverId.

Now my problem is how far should I go to validate the creation of a Car aggregate in CarFactory. Should I trust that the passed DriverId refers to an existing Driver or should I check that invariant?

For checking, I see two possibilities:

  • I could change the signature of the car factory to accept a complete driver entity. The factory would then just pick the id from that entity and build the car with that. Here the invariant is checked implicitly.
  • I could have a reference of the DriverRepository in the CarFactory and explicitly call driverRepository.exists(driverId).

But now I'm wondering isn't that too much of invariant checking? I could imagine that those aggregates might live in separate bounded context, and now I would pollute the car BC with dependencies on the DriverRepository or the Driver entity of the driver BC.

Also, if I would talk to domain experts, they would never question the validity of such references. I'm sensing that I pollute my domain model with unrelated concerns. But then again, at some point the user input should be validated.

Best Answer

I could change the signature of the car factory to accept a complete driver entity. The factory would then just pick the id from that entity and build the car with that. Here the invariant is checked implicitly.

This approach is appealing since you get the check for free and it's well aligned with the ubiquitous language. A Car is not driven by a driverId, but by a Driver.

This approach is in fact used by Vaughn Vernon in it's Identity & Access sample bounded context where he passes a User aggregate to a Group aggregate, but the Group only holds onto a value type GroupMember. As you can see this also allows him to check for the user's enablement (we are well aware that the check may be stale).

    public void addUser(User aUser) {
        //original code omitted
        this.assertArgumentTrue(aUser.isEnabled(), "User is not enabled.");

        if (this.groupMembers().add(aUser.toGroupMember()) && !this.isInternalGroup()) {
            //original code omitted
        }
    }

However, by passing the Driver instance you also open yourself to an accidental modification of the Driver within Car. Passing value reference makes it easier to reason about changes from a programmer's point of view, but at the same time, DDD is all about the Ubiquitous Language, so perhaps it's worth the risk.

If you can actually come up with good names to apply the Interface Segregation Principle (ISP) then you may rely on an interface that doesn't have the behavioral methods. You could perhaps also come up with a value object concept that represents an immutable driver reference and that can only be instantiated from an existing driver (e.g. DriverDescriptor driver = driver.descriptor()).

I could imagine that those aggregates might live in separate bounded context, and now I would pollute the car BC with dependencies on the DriverRepository or the Driver entity of the driver BC.

No, you wouldn't actually. There's always an anti-corruption layer to make sure that the concepts of one context won't bleed into another. It's actually much easier if you have a BC dedicated to car-driver associations because you can model existing concepts such as Car and Driver specifically for that context.

Therefore, you may have a DriverLookupService defined in the BC responsible to manage car-driver associations. This service may call a web-service exposed by the Driver Management context which returns Driver instances that will most likely be value objects in this context.

Note that web-services aren't necessarily the best integregation method between BCs. You could also rely on messaging where for instance a UserCreated message from the Driver Management context would be consumed in a remote context which would store a representation of the driver in it's own DB. The DriverLookupService could then use this DB and the driver's data would be kept up to date with further messages (e.g. DriverLicenceRevoked).

I can't really tell you which approach is better for your domain, but hopefully this will give you enough insights to make a decision.

Related Topic