Skip to content

Domain layer

The Domain layer is the core of the PetFolio Service. It contains all business logic, entities, value objects, and repository interfaces. This layer has zero external dependencies - it defines the rules that the rest of the application must follow.

Clean Architecture - innermost layer

Every other layer depends on Domain, but Domain depends on nothing. This isolation ensures that business rules are never compromised by infrastructure concerns like databases, HTTP, or external services.

Responsibility

  • Defining entities: Core business objects with behaviour (e.g. Animal, Account, User)
  • Defining value objects: Immutable types that enforce business invariants (e.g. Email, Name, Weight, Species)
  • Declaring repository interfaces: Contracts that Infrastructure implements (e.g. IAnimalRepository, IUnitOfWork)
  • Declaring service interfaces: Contracts for cross-cutting concerns (e.g. ICurrentUserService, ITenantProvider, IClaimsPrincipalAccessor)
  • Raising domain events: For cross-aggregate communication (e.g. AnimalCreatedEvent)
  • Providing error definitions: For domain-specific failures (e.g. AnimalErrors, AccountErrors)

Components

graph TB
    subgraph "Entities - rich domain models"
        E1[Account<br>+ Create<br>+ AddUser<br>+ RemoveUser]
        E2[Animal<br>+ Create<br>+ SetDateOfBirth<br>+ SetCurrentWeight]
        E3[User<br>+ Create<br>+ SetEmailAddress]
    end

    subgraph "Value objects - immutable"
        VO1[Email<br>RFC 5322 validation]
        VO2[Name<br>1-100 chars]
        VO3[Weight<br>Kg/Lb/G conversion]
        VO4[Species]
        VO5[Breed]
        VO6[Age<br>Calculated from DOB]
    end

    subgraph "Repository interfaces"
        R1[IAnimalRepository]
        R2[IAccountRepository]
        R3[IUserRepository]
        R4[IUnitOfWork]
    end

    subgraph "Domain events"
        DE1[AnimalCreatedEvent]
        DE2[AccountCreatedEvent]
        DE3[AccountUserAddedEvent]
    end

    subgraph "Common types"
        CT1["Result&lt;T&gt;<br>Success/Failure pattern"]
        CT2[PetfolioError<br>Code + Message]
        CT3[Entity Base Class]
    end

    E2 --> VO1
    E2 --> VO2
    E2 --> VO3
    E3 --> VO1
    E1 --> VO2
    E2 --> DE1
    E1 --> DE2

Entities

Entities are rich domain models - they contain both data and behaviour. Each entity inherits from Entity, which provides Id, CreatedAtUtc, UpdatedAtUtc, and domain event management.

DO: Encapsulate business logic and data together in entities

DON'T: Create entities with only properties and no behaviour

Rich domain model example

The following entity illustrates the rich domain model pattern used throughout the Domain layer. Real entities follow the same conventions: a static Create() factory method, value objects for type-safe fields, and behaviour methods that enforce business rules.

Key points demonstrated in the diagram:

  • Private parameterless constructor for EF Core hydration
  • Static Create() factory validates arguments and raises a domain event
  • Behaviour methods (Cancel, Reschedule) enforce invariants rather than exposing raw setters
  • Value objects (Name) provide type safety and built-in validation
  • Domain events (AppointmentCreatedEvent, AppointmentCancelledEvent) enable cross-aggregate communication
public sealed class Appointment : Entity
{
    public Name Title { get; private set; }
    public DateOnly ScheduledDate { get; private set; }
    public AppointmentStatus Status { get; private set; }

    private Appointment() { } // EF Core

    public static Appointment Create(Name title, DateOnly scheduledDate)
    {
        if (scheduledDate < DateOnly.FromDateTime(DateTime.UtcNow))
            throw new ArgumentException("Scheduled date must be in the future.");

        var appointment = new Appointment
        {
            Id = Guid.NewGuid(),
            Title = title,
            ScheduledDate = scheduledDate,
            Status = AppointmentStatus.Pending
        };

        appointment.Raise(new AppointmentCreatedEvent(appointment.Id));
        return appointment;
    }

    public void Cancel(string reason)
    {
        if (Status == AppointmentStatus.Completed)
            throw new InvalidOperationException("Cannot cancel a completed appointment.");

        Status = AppointmentStatus.Cancelled;
        Raise(new AppointmentCancelledEvent(Id, reason));
    }

    public void Reschedule(DateOnly newDate)
    {
        if (newDate < DateOnly.FromDateTime(DateTime.UtcNow))
            throw new ArgumentException("New date must be in the future.");

        ScheduledDate = newDate;
    }
}

Value objects

Value objects are immutable types that enforce business invariants. They are always sealed record types with a static Create() factory method.

DO: Use sealed records with a static Create() factory that validates invariants

DON'T: Use primitive types directly or allow construction without validation

Value object example
classDiagram
    class Email {
        <<sealed record>>
        +string Value
        +string Normalised
        +Create(string value)$ Email
        -Email()
        -Email(string value, string normalised)
        -ValidateEmail(string email)$ void
    }

    note for Email "Immutable value object\n- Private parameterless constructor for EF Core\n- Create() factory validates RFC 5322\n- Max length 254 chars\n- Normalises to lowercase\n- Throws ArgumentException if invalid"

    classDef value fill:#c8c8c826,stroke:#999999,stroke-width:1.5px

    class Email value

Validation

All business rules are enforced within the Domain layer. This is the first line of defence - if data reaches the Domain, it must be valid.

DO: Place all business rules and validation in domain entities and value objects

DON'T: Scatter validation across controllers, services, or skip it entirely

Where validation belongs in the Domain:

  • Entity factory methods (Create()) validate construction arguments
  • Entity business methods validate state transitions
  • Value object Create() methods validate format and range

The Domain layer enforces invariants by throwing exceptions. The Application Layer catches these exceptions and converts them to Result<T> failures.