Every business rule in your system should have exactly one home: the Domain layer. In Clean Architecture that's the innermost layer, holding entities, value objects, domain events, domain services, repository interfaces, and domain errors, with zero external package references.
Every Clean Architecture diagram puts the Domain layer at the center, but most of them never tell you what actually goes inside it. That's a problem, because the Domain layer is the one part of the system you can't afford to get wrong. Here is what belongs there, what doesn't, and how to keep it free of every framework concern.
What Is the Domain Layer?
The Domain layer is the innermost layer in Clean Architecture. It sits at the center of the dependency graph, and nothing depends on anything outside of it.
This is where your business rules live. Not HTTP concerns. Not database details. Just the rules that make your business what it is.
The Domain layer contains:
- Entities - objects with identity and lifecycle
- Value Objects - immutable objects defined by their attributes
- Domain Events - notifications that something meaningful happened
- Enumerations - smart enums representing domain concepts
- Domain Services - business logic that doesn't belong to a single entity
- Repository Interfaces - abstractions for data access
- Custom Exceptions - domain-specific error types
Entities
Entities are objects defined by their identity, not their attributes. Two customers with the same name are still different customers.
public abstract class Entity : IEquatable<Entity>
{
private readonly List<IDomainEvent> _domainEvents = new();
protected Entity(Guid id)
{
Id = id;
}
public Guid Id { get; private init; }
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
public void ClearDomainEvents() => _domainEvents.Clear();
protected void RaiseDomainEvent(IDomainEvent domainEvent) =>
_domainEvents.Add(domainEvent);
public bool Equals(Entity? other)
{
return other is not null && Id == other.Id;
}
public override bool Equals(object? obj)
{
return obj is Entity entity && Equals(entity);
}
public override int GetHashCode() => Id.GetHashCode();
}
The base class also collects domain events (we'll define IDomainEvent in a moment), so entities can record what happened and let an outer layer publish it after saving.
And a concrete entity:
public sealed class Customer : Entity
{
private Customer(Guid id, string name, Email email) : base(id)
{
Name = name;
Email = email;
}
public string Name { get; private set; }
public Email Email { get; private set; }
public static Customer Create(string name, Email email)
{
var customer = new Customer(Guid.NewGuid(), name, email);
customer.RaiseDomainEvent(new CustomerCreatedDomainEvent(customer.Id));
return customer;
}
public void UpdateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new DomainException("Customer name cannot be empty.");
}
Name = name;
}
}
Key principles:
- Private constructor - creation goes through a factory method that enforces invariants
- Private setters - state changes go through methods that validate the transition
- Domain events - the entity signals when something important happens
This is what makes the domain model the best place to enforce invariants: there's no way to construct or mutate the entity into an invalid state.
Value Objects
Value Objects represent concepts with no identity. Two Email("test@example.com") instances are equal.
public sealed class Email : ValueObject
{
private Email(string value) => Value = value;
public string Value { get; }
public static Result<Email> Create(string email)
{
if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
{
return Result.Failure<Email>(DomainErrors.Email.InvalidFormat);
}
return new Email(email.Trim().ToLowerInvariant());
}
protected override IEnumerable<object> GetAtomicValues()
{
yield return Value;
}
}
Use Value Objects to replace primitives wherever a concept has business rules attached to it.
Domain Events
Domain events represent something that happened in the domain. They're raised by entities and handled by the Application layer.
public interface IDomainEvent
{
Guid Id { get; }
DateTime OccurredOnUtc { get; }
}
public sealed record CustomerCreatedDomainEvent(Guid CustomerId) : IDomainEvent
{
public Guid Id { get; } = Guid.NewGuid();
public DateTime OccurredOnUtc { get; } = DateTime.UtcNow;
}
Domain events live in the Domain layer because they describe domain facts. But the handlers live in the Application layer.
Smart Enumerations
Instead of plain enum types, use smart enums for domain concepts that carry behavior:
public abstract class OrderStatus : Enumeration<OrderStatus>
{
public static readonly OrderStatus Draft = new DraftStatus();
public static readonly OrderStatus Confirmed = new ConfirmedStatus();
public static readonly OrderStatus Shipped = new ShippedStatus();
public static readonly OrderStatus Delivered = new DeliveredStatus();
public static readonly OrderStatus Cancelled = new CancelledStatus();
private OrderStatus(int id, string name) : base(id, name) { }
public abstract bool CanTransitionTo(OrderStatus next);
private sealed class DraftStatus : OrderStatus
{
public DraftStatus() : base(1, "Draft") { }
public override bool CanTransitionTo(OrderStatus next) =>
next == Confirmed || next == Cancelled;
}
private sealed class ConfirmedStatus : OrderStatus
{
public ConfirmedStatus() : base(2, "Confirmed") { }
public override bool CanTransitionTo(OrderStatus next) =>
next == Shipped || next == Cancelled;
}
// ... other statuses
}
The state transition rules are embedded in the enum itself. No service needed to check if an order can be cancelled.
The Enumeration<TEnum> base class handles equality and provides static lookups like FromValue and FromName, which is what the Infrastructure layer uses to persist the enum as a string.
Repository Interfaces
Repository interfaces can live in the Domain layer, right next to the aggregates they load and save. That's the convention I'm showing here. Their implementations always go in Infrastructure.
public interface ICustomerRepository
{
Task<Customer?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<Customer?> GetByEmailAsync(Email email, CancellationToken cancellationToken = default);
void Add(Customer customer);
void Update(Customer customer);
}
This follows the Dependency Rule: the Domain layer defines what it needs, and outer layers provide it.
Note: some developers place repository interfaces in the Application layer instead. Both approaches are valid - the key is that implementations go in Infrastructure either way.
Domain Services
When business logic doesn't naturally belong to a single entity, use a Domain Service:
public sealed class PricingService
{
public Money CalculateTotal(
IReadOnlyCollection<OrderLineItem> items,
DiscountCode? discountCode)
{
var subtotal = items.Aggregate(
Money.Zero(Currency.Usd),
(sum, item) => sum + item.TotalPrice);
if (discountCode is not null)
{
subtotal = discountCode.Apply(subtotal);
}
return subtotal;
}
}
Domain Services are stateless. They operate on entities and value objects passed to them.
Domain Errors
Define domain-specific errors in the Domain layer:
public static class DomainErrors
{
public static class Email
{
public static readonly Error InvalidFormat = new(
"Email.InvalidFormat",
"The email address is not in a valid format.");
}
public static class Customer
{
public static readonly Error NotFound = new(
"Customer.NotFound",
"The customer was not found.");
public static readonly Error EmailNotUnique = new(
"Customer.EmailNotUnique",
"The email address is already in use.");
}
public static class Order
{
public static readonly Error AlreadyCancelled = new(
"Order.AlreadyCancelled",
"The order has already been cancelled.");
}
}
These errors are used with the Result pattern for explicit error handling without exceptions.
Folder Structure
Domain/
Customers/
Customer.cs
CustomerCreatedDomainEvent.cs
ICustomerRepository.cs
Orders/
Order.cs
OrderLineItem.cs
OrderStatus.cs
OrderCompletedDomainEvent.cs
IOrderRepository.cs
Shared/
Entity.cs
AggregateRoot.cs
ValueObject.cs
IDomainEvent.cs
Result.cs
Error.cs
DomainException.cs
Organize by aggregate or concept, not by pattern type. Don't create folders like Entities/, ValueObjects/, Events/.
What Does NOT Belong in the Domain Layer
- DTOs - those belong in the Application layer
- Database concerns - no
DbContext, no[Table]attributes, no migration code - Logging - the domain doesn't know about
ILogger - External service calls - no HTTP, no message queue access
- Validation with FluentValidation - command/query validation belongs in Application
The Domain layer has zero external package references. It references only .NET base class libraries.
Enforcing the Rules
Use architecture tests to keep the Domain layer pure:
[Fact]
public void Domain_Should_Not_Have_Dependencies_On_Other_Layers()
{
var result = Types
.InAssembly(typeof(Customer).Assembly)
.ShouldNot()
.HaveDependencyOnAny("Application", "Infrastructure", "Presentation")
.GetResult();
result.IsSuccessful.Should().BeTrue();
}
Summary
The Domain layer is the most stable and valuable part of your Clean Architecture solution. It contains the business rules that make your application unique - everything else is infrastructure.
Keep it pure, keep it focused, and the rest of your architecture will thank you.
Thanks for reading, and stay awesome!
Frequently Asked Questions
What belongs in the domain layer in Clean Architecture?
Entities, value objects, domain events, domain services, repository interfaces, and domain errors or exceptions. In short: the business rules and the types that express them, with no infrastructure concerns.
Should the domain layer have NuGet package references?
Ideally zero. The domain layer should reference only the .NET base class libraries. Framework attributes, EF Core, logging, and messaging packages all belong in outer layers.
What is the difference between an entity and a value object?
An entity is defined by its identity: two customers with the same name are still different customers. A value object is defined by its attributes: two Email objects with the same address are interchangeable. Value objects are immutable.
Do repository interfaces belong in the domain layer?
It is a valid and common choice, since repositories are conceptually part of an aggregate contract. Some teams put them in the application layer instead. Either way, the implementations always live in the infrastructure layer.
Where do domain event handlers live?
In the application layer. The domain layer defines and raises the events because they are domain facts, but reacting to them is use case orchestration, which is the application layer job.



