18

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.

5 Answers 5

8

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.

4

Let's assume the aggregate Car has a reference to the aggregate Driver. This reference will be modeled by having Car.driverId.

Yup, that's the right way to couple one aggregate to another.

if I would talk to domain experts, they would never question the validity of such references

Not quite the right question to ask your domain experts. Try "what's the cost to the business if the driver doesn't exist?"

I probably wouldn't use DriverRepository to check the driverId. Instead, I would use a domain service to do it. I think it does a better job of expressing the intent -- under the covers, the domain service still checks the system of record.

So something like

class DriverService {
    private final DriverRepository driverRepository;

    boolean doesDriverExist(DriverId driverId) {
        return driverRepository.exists(driverId);
    }
}

You actually query the domain about the driverId at a number of different points

  • From the client, before sending the command
  • In the application, before passing the command to the model
  • Within the domain model, during command processing

Any or all of these checks can reduce errors in user input. But they are all working from stale data; the other aggregate can change immediately after we ask the question. So there is always some danger of false negatives/positives.

  • In an exception report, run after the command has completed

Here, you are still working with stale data (the aggregates may be running commands while you are running the report, you may not be able to see the most recent writes to all of the aggregates). But the checks between aggregates are never going to be perfect (Car.create(driver:7) running at the same time as Driver.delete(driver:7)) So this gives you an extra layer of defense against risk.

1
  • 2
    Driver.delete shouldn't exist. I never really seen a domain where aggregates gets destroyed. By keeping ARs around you can never end up with orphans.
    – plalx
    May 30, 2016 at 2:02
3

The way you're asking the question (and proposing two alternatives) it is as if the only concern is that the driverId is still valid at the time the car is created.

However, you must also be concerned that the driver associated with driverId isn't deleted before the car is either deleted or given another driver (and possibly also that the driver isn't assigned to another car (this if domain restricts a driver to only be associated with one car)).

I suggest that instead of validation, you allocate (which would include validation of presence). You will then disallow deletions while still allocated, thus guarding against the race condition of stale data during construction, as well as the other longer term problem. (Note that the allocation both validates and marks allocated, and operates atomically.)

Btw, I agree with @PriceJones that the association between the car and driver probably is a responsibility separate from either the car or the driver. This kind of association will only grow in complexity over time, because it sounds is like a scheduling problem (drivers, cars, time slots/windows, substitutes, etc...) Even if it more like a registration problem, one may want historical registrations as well as current registrations. Thus, it may very well merit its own BC outright.

You can provide an allocation scheme (such as a boolean or reference count) within the BC of the aggregate entities being allocated, or within a separate BC, say, the one responsible for making the association between car & driver. If you do the former you can allow (valid) deletion operations issued to the car or driver BC; if you do the latter, you will need to prevent deletions from the car & driver BC's and instead send them thru the car & driver association scheduler.

You might also split some of the allocation responsibilities between BC's as follows. The car & driver BC each provide an "allocation" scheme that validates and sets the allocated boolean with that BC; when their allocation boolean is set, the BC prevents deletion of the corresponding entities. (And the system is setup so that the car & driver BC only allow allocation and deallocation from the car/driver association scheduling BC.)

The car & driver scheduling BC then maintains a calendar of drivers associated with car for some time periods/durations, now and future, and notifies the other BC's of deallocation only on the last use of a scheduled car or driver.


As a more radical solution, you can treat the car & driver BC's as append-only historical-record factories, leaving ownership to the car/driver association scheduler. The car BC may generate a new car, complete with all the details of the car, along with its VIN. The ownership of the car is handled by the car/driver association scheduler. Even if a car/driver association is deleted, and the car itself is destroyed, the car's records still exist in the car BC by definition, and we can use the car BC to look up historical data; while car/driver associations/ownerships (past, present, and potentially future scheduled) are being handled by another BC.

2

It might help to ask: Are you sure cars are constructed with drivers? I have never heard of a car composed of a driver in the real world. The reason why this question is important is because it might point you in the direction of independently creating cars and drivers and then creating some external mechanism which assigns a driver to a car. A car can exist without a driver reference and still be a valid car.

If a car must absolutely have a driver in your context, then you might want to consider the builder pattern. This pattern will be responsible for ensuring cars are constructed with existing drivers. The factories will serve up independently validated cars and drivers, but the builder will ensure the car has the reference it needs before it serves up the car.

1
  • I thought about the car / driver relationship too -- but introducing a DriverAssignment aggregate just moves which reference needs to be validated. May 27, 2016 at 13:28
1

But now I'm wondering isn't that too much of invariant checking?

I think so. Fetching a given DriverId from the DB returns an empty set if it does not exist. So checking the return result makes asking if it exists (and then fetching) unnecessary.

Then class design makes it unnecessary also

  • If there is a requirement "a parked car may or may not have a driver"
  • If a Driver object requires a DriverId and is set in the constructor.
  • If the Car needs only the DriverId, have a Driver.Id getter. No setter.

Repository is not the place for business rules

  • A Car cares if it has a Driver (or his ID at least). A Driver cares if it has a DriverId. The Repository cares about data integrity and could not care less about driver-less cars.
  • The DB will have data integrity rules. Non-null keys, not-null constraints, etc. But data integrity is about data / table schema, not business rules. We have a strongly correlated, symbiotic relationship in this case but don't mix the two.
  • The fact that a DriverId is a business domain thing is handled in the appropriate classes.

Separation of Concerns Violation

... happens when Repository.DriverIdExists() asks the question.

Build a domain object. If not a Driver then maybe a DriverInfo (just a DriverId and Name, let's say) object. The DriverId is validated upon construction. It must exist, and be the right type, and whatever else. Then it is a client class design issue how to deal with a non-existent driver/ driverId.

Maybe a Car is fine without a driver until you call Car.Drive(). In which case the Car object of course ensures its own state. Can't drive without a Driver - well, not quite yet.

Separating a property from it's class is bad

Sure, have a Car.DriverId if you wish. But it should look something like this:

public class Car {
    // Non-null driver has a driverId by definition/contract.
    protected DriverInfo myDriver;
    public DriverId {get { return myDriver.Id; }}

    public void Drive() {
       if (myDriver == null ) return errorMessage; // or something
       // ... continue driving
    }
}

Not this:

public class Car {
    public int DriverId {get; protected set;}
}

Now the Car must deal with all the DriverId validity issues - a single responsibility principle violation; and redundant code probably.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.