Aggregate Root
Definition
Section titled “Definition”An Aggregate Root is a DDD building block that forms the root of a consistency
boundary. It encapsulates invariants, manages state transitions via behavior methods,
and raises domain/integration events. External code interacts through methods
(Approve(), Cancel()), never through direct property mutation.
When to Use
Section titled “When to Use”Use AggregateRoot (or audited variants) when the entity:
- Has a state machine (status transitions with business rules)
- Raises domain or integration events
- Encapsulates invariants that must be protected from external mutation
- Is the root of a consistency boundary
Use plain Entity when the entity is:
- Append-only / immutable after creation (audit logs, consent records)
- A configuration record (settings, feature flags)
- A cache/mirror of an external system
- A lookup table (reference data)
Diagram
Section titled “Diagram”classDiagram
class Entity {
+Guid Id
}
class AggregateRoot {
+IReadOnlyCollection~IDomainEvent~ DomainEvents
+IReadOnlyCollection~IIntegrationEvent~ IntegrationEvents
#AddDomainEvent(IDomainEvent)
#AddDistributedEvent(IIntegrationEvent)
}
class CreationAuditedEntity {
+DateTimeOffset CreatedAt
+string CreatedBy
}
class CreationAuditedAggregateRoot {
+IReadOnlyCollection~IDomainEvent~ DomainEvents
+IReadOnlyCollection~IIntegrationEvent~ IntegrationEvents
#AddDomainEvent(IDomainEvent)
#AddDistributedEvent(IIntegrationEvent)
}
Entity <|-- AggregateRoot
Entity <|-- CreationAuditedEntity
CreationAuditedEntity <|-- CreationAuditedAggregateRoot
Granit Implementation
Section titled “Granit Implementation”Choosing the Right Base Class
Section titled “Choosing the Right Base Class”| Base Class | Use When |
|---|---|
AggregateRoot | No audit trail needed, manages own timestamps |
CreationAuditedAggregateRoot | Needs CreatedAt / CreatedBy |
AuditedAggregateRoot | Needs creation + modification audit |
FullAuditedAggregateRoot | Needs full audit + soft delete (ISoftDeletable) |
Gold Standard: BlobDescriptor
Section titled “Gold Standard: BlobDescriptor”BlobDescriptor is the reference implementation for a well-designed aggregate root:
public sealed class BlobDescriptor : AggregateRoot, IMultiTenant{ // 1. Private EF Core constructor — required for materialization private BlobDescriptor() { }
// 2. Factory method — the only way to create an instance public static BlobDescriptor Create( Guid id, Guid? tenantId, string containerName, string objectKey, BlobUploadRequest request, DateTimeOffset createdAt) => new() { Id = id, TenantId = tenantId, ContainerName = containerName, ObjectKey = objectKey, Status = BlobStatus.Pending, CreatedAt = createdAt, };
// 3. Private setters — no external mutation public string ContainerName { get; private set; } = string.Empty; public BlobStatus Status { get; private set; }
// 4. Explicit interface for infrastructure injection public Guid? TenantId { get; private set; } Guid? IMultiTenant.TenantId { get => TenantId; set => TenantId = value; }
// 5. Behavior method with guard clause + domain event public void MarkAsValid( string contentType, long sizeBytes, DateTimeOffset at) { if (Status != BlobStatus.Uploading) throw new InvalidOperationException( $"Cannot transition from {Status} to Valid.");
Status = BlobStatus.Valid; VerifiedContentType = contentType; SizeBytes = sizeBytes; ValidatedAt = at;
AddDomainEvent(new BlobValidatedEvent(Id, ContainerName, contentType, sizeBytes)); }}Key Patterns
Section titled “Key Patterns”Private EF Core Constructor
Section titled “Private EF Core Constructor”Every aggregate root with private set properties must keep a parameterless
constructor for EF Core materialization:
private BlobDescriptor() { } // EF Core uses this, not the factoryWithout it, EF Core attempts to use the factory method and crashes at runtime.
Explicit Interface for IMultiTenant
Section titled “Explicit Interface for IMultiTenant”When TenantId has private set, the interceptor cannot assign it. Use an explicit
interface implementation to allow infrastructure injection while preserving DDD
encapsulation:
public Guid? TenantId { get; private set; }Guid? IMultiTenant.TenantId{ get => TenantId; set => TenantId = value;}Two-Tier Event Model
Section titled “Two-Tier Event Model”AddDomainEvent(): local, dispatched after commit. Naming:XxxEvent(e.g.,BlobValidatedEvent).AddDistributedEvent(): durable outbox, dispatched before commit. Naming:XxxEto(e.g.,PersonalDataDeletedEto).
See Event-Driven Architecture for details.
Architecture Tests
Section titled “Architecture Tests”DomainConventionTests enforces:
ValueObjectsubclasses must be sealed- Domain entities must not reside in
Internalnamespaces - Manual
IDomainEventSourceimplementors are flagged (should use aggregate root bases)
Related Patterns
Section titled “Related Patterns”- Event-Driven Architecture — domain and integration events
- State Machine — status transitions
- Guard Clause — precondition validation
- Factory Method — controlled creation
- ADR-017 — full strategy rationale