Whetstone.
Entities and the aggregate rootThe aggregate owns its invariants
Module 4 · Lesson 213 min

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.

An action method returns Result (FlightDisruptionAggregate.cs)
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.

Practice

Try it yourself

Do

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 and Result.Success(...) at the end
  • Confirm no caller can now cancel without going through the validated action
Done whenThe public surface is a verb-named action returning Result that validates internally; there is no public Validate method a caller could skip.
Recall

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.

Sign in to track your progress →