Open source

PMQ.Domain

level applied-in-production

902 downloads · the most recent of the suite · repository

The DDD building blocks: entity, aggregate root, value object and domain event.

The problem

Every project that does DDD rewrites the same four base classes, and each one gets a different detail wrong: an entity compared by reference instead of by identity, a mutable value object, an event published before persistence confirms.

The decisions, in the code

Entity inherits from Validatable. A broken invariant becomes a notification accumulated on the entity itself, never an exception, and that is PMQ.Notifications coming in as the base of the domain, and not as a layer on top of it. The reasoning is written in the class:

/// Inherits Validatable: a broken invariant becomes a notification accumulated on the entity,
/// never an exception. Exceptions are for programming errors — a violated business rule is an
/// expected outcome, and unwinding the stack for it both costs more and reports only the first
/// problem instead of all of them.
public abstract class Entity<TId> : Validatable, IHasDomainEvents, IEquatable<Entity<TId>>
    where TId : notnull

Equality by identity, said out loud. Two instances with the same Id are the same entity even if everything else differs. It is the DDD definition and it is where people get it wrong the most, so it is in the class documentation along with the pointer: for value semantics, use ValueObject or record.

Parameterless constructor, explained. It exists because the ORM needs it, and it is annotated saying so: without the note, the next person to read it removes it thinking it is a leftover.

/// <summary>Parameterless constructor required by ORMs such as Entity Framework Core.</summary>
protected Entity() { }

Events accumulated, not published. The entity keeps the events in a private list and exposes IReadOnlyCollection; the application layer dispatches them, after the transaction confirms. Publishing at the moment the event is born delivers the side effect of an operation that can still fail.

The size is the proposal

Six files, 533 lines counting documentation. There is no generic repository, no unit of work, no event bus. A library of domain blocks that brings infrastructure along forces the consumer to accept persistence decisions that are not its own.

Pablo Mickael Quevedo Senior Software Engineer · Novo Hamburgo, Brazil