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(...));
}
}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> _utcNowconstructor 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.