The aggregate owns its invariants
The aggregate root is the entity that guards a cluster of objects and their rules. Outside code talks to the aggregate, never to its insides. Two conventions define how Fusion writes one, and they’re the ones Nick’s PR feedback hammered on.
First: action methods, not validate methods (convention N1, anti-pattern #5). The aggregate exposes verbs: ConfirmDecision, RevertDecision, CreateAndCalculate. Each validates internally and returns a Result<T>. There is no public Validate* a caller must remember to run first. The action IS the validation boundary.
public Result<RevertDecisionResult> RevertDecision(string flightNumber, DateTime revokedAtUtc)
{
if (!_revertSnapshotExists)
return Result<RevertDecisionResult>.Failure(new EntityNotFound(nameof(flightNumber),
$"No cost calculation snapshot exists for flight '{flightNumber}'. Calculate a cost before reverting a decision."));
if (!_revertHasActiveDecision)
return Result<RevertDecisionResult>.Failure(new DecisionNotFound(nameof(flightNumber),
$"No active decision exists for flight '{flightNumber}'. Confirm a decision before attempting to revert."));
return Result<RevertDecisionResult>.Success(new RevertDecisionResult(flightNumber, revokedAtUtc));
}Second: the aggregate is loaded with the state it needs to enforce its rules (convention D1). The application layer is a thin orchestrator: it loads the aggregate whole, calls one action, and saves. It must not pre-compute domain state and pass booleans in, because then the aggregate is just a stateless validator over caller-supplied data and the real logic has leaked into the application layer.
Try it yourself
Rename a validate into an action
Retrain the reflex from Nest's service.validateX() to an aggregate action.
- Take a hypothetical
public bool ValidateCancellation(...)on an aggregate that a caller runs before cancelling - Rewrite it as a private-state-validating action:
public Result<CancelResult> CancelDecision(DateTime cancelledAtUtc) - Move the checks inside; return
Result.Failure(...)for each broken rule andResult.Success(...)at the end - Confirm no caller can now cancel without going through the validated action
What the application layer must not do
Convention D1 in one line.
Under convention D1, what is the application layer's job with an aggregate, and what must it NOT do?
Reveal answer
It loads the aggregate whole, calls one action method, and saves (thin orchestrator). It must NOT pre-compute domain state or pass booleans/DTOs of derived state into the aggregate, because that turns the aggregate into a stateless validator and leaks its invariants into the application layer.