Whetstone.
DDD layers and CQRSThe aggregate owns invariants
Module 9 · Lesson 115 min

The aggregate owns invariants

The domain layer holds the rules. An aggregate root is the object that owns a consistency boundary and its invariants: you load it whole, call an action on it, and it either succeeds or returns a failed Result. The application layer is a thin orchestrator: load the aggregate, call the action, save. It must not pre-compute domain decisions and hand them in; that would make the aggregate a hollow validator over caller-supplied data (convention D1).

A thin application use case (real RevertDisruptionDecision)
public sealed class RevertDisruptionDecision
{
    private readonly IFlightDisruptionAggregateRepository _repo;
    private readonly Func<DateTime> _utcNow;   // injected clock = testable time

    public async Task<Result<FlightDisruptionAggregate.RevertDecisionResult>> Execute(
        RevertDisruptionDecisionRequest request)
    {
        var summary = await _repo.LoadRevertSummary(request.EjdpUniqueFlightKey);
        var aggregate = FlightDisruptionAggregate.ForRevertFlow(
            snapshotExists: summary is not null,
            hasActiveDecision: summary?.HasActiveDecision ?? false);

        var result = aggregate.RevertDecision(flightNumber, _utcNow());
        if (!result.IsSuccess) return result;         // guard, then continue

        var updated = await _repo.MarkAsReverted(summary!.SnapshotId, summary.EjdpUniqueFlightKey, result.Value.RevokedAtUtc);
        return updated ? result : Result<...>.Failure(new DecisionNotFound(...));
    }
}
Practice

Try it yourself

Do

Trace a use case end to end

See the load / act / save shape and the injected clock.

  • Open application/disruption-decisions/RevertDisruptionDecision.cs
  • Identify the three moves: load (LoadRevertSummary), act (aggregate.RevertDecision), save (MarkAsReverted)
  • Note the Func<DateTime> _utcNow constructor parameter: time is injected so tests can pin it to a fixed instant
  • Note the use case returns Result<...>, never throws for a business failure
Done whenYou can point to the load, the domain action, and the save, and explain why the clock is injected rather than calling DateTime.UtcNow directly.
Sign in to track your progress →