Maybe. I think what you aren't exactly capturing is what an invariant is and why a root-based access is so important. Heres an oversimplified example:
// invariant rule
c = a b
a = 3
b = 2
c = a b
You can't trust clients to get this right. It's not the client's responsibility. The fact that a transaction is possible only ensures that a, b, and c are persisted atomically as one. The transaction doesn't guarantee the invariant. The business logic does, and that's the point of the encapsulating root, whether the behavior is on the root or on one or more other composed types.
These values could be held by any means, such as attributes on an object or set as keys that reference values in a map ("a" -> a).
Here's a more complex example that has a similar invariant rule:
// tork is a calculation using
// the current lever and stress
class Tool {
int lever
int stress
int tork
calibrateUsing(gauge) {
lever = gauge.increase(lever)
stress = gauge.lower(stress)
tork = gauge.tork(lever, stress)
}
}
// client
tool = Tool(settings)
tool.calibrateUsing(gauge)
repository.write(tool)
The point of the encapsulation by some kind of behavioral component (even a code modual of related functions) is that the invariants can only be enforced and protected by strict control. Here's another example of the same rule using different containment:
calibrateUsing(tool, gauge) {
lever = tool("lever")
lever = gauge.increase(lever)
stress = tool("stress")
stress = gauge.lower(stress)
tork = gauge.tork(lever, stress)
return
("lever" -> lever,
"stress" -> stress,
"tork" -> tork)
}
// client
map tool = Tool(settings)
...
tool = calibrateUsing(tool, gauge)
repository.write(tool)
This example may or may not be considered an Aggregate, and who cares? The principles are the same.
In both examples, there is explicit business logic that protects the invariants. The database transaction only ensures that the values persistent atomically as one unit.