It's clear that an entity should never be in an inconsistent state; if a change would violate an invariant, a domain object should reject the change. But what if it's left in a valid, but inappropriate-for-the-input state?
For example, consider a calendar aggregate that supports a "delay all meetings" feature – upon delay, each meeting entity within the calendar has its time moved a given amount into the future. To implement this, a calendar aggregate might sequentially delay each meeting which would edit its time window.
Now imagine that one of a series of meetings has some internal invariant that means it cannot be delayed. It would reject a delay operation and the calendar would maintain that rejection. But what if other meetings have already been delayed? It's likely that each individual delay prior to this one has left the whole aggregate in a perfectly valid state! However, that aggregate state represents neither the result of the operation nor the original state.
In db-backed applications, the exception generally prevents persistence of the changed aggregate, so the partial change is ultimately ignored. Still, it feels wrong to leave the in-memory aggregate in this unwanted (albeit internally consistent) state.
I've considered these options, but they all have their own problems:
- Operate on copies of the meeting (either as immutable behavior or by cloning first), then replace the full collection of meetings only once all have succeeded. This seems to contradict the nature of entities.
- Perform a "can delay?" check for each meeting before actually delaying any of them. This may duplicate a significant amount of effort.
- Revert the changes that were made before the failed change. Unless there's a domain reason to "un-delay" a meeting on its own, this mixes cleanup code into domain code. Plus, cleanup is a task in its own right with its own reasons to fail. Maybe some changes are irreversible?
The other issue common to these approaches is that it feels leaky for the child to change the way it behaves based on the usage patterns of the consumer. The problem compounds when entities are nested and combined in different ways.
Is there consensus or best practice that covers this situation?
Or, as an alternative formulation, where should we direct our attention when facing a situation like this – Such as prevention or rollback at the domain level vs. rejection of changes in the application layer?