TL;DR: Business rules (invariants) belong in the Domain Layer, not the UI or the Database. By using a Rich Domain Model and explicit Rule objects, you ensure your system remains in a valid state regardless of how data enters it.
In many modern applications, business logic is scattered like breadcrumbs. You find some in JavaScript validation, some in Web API controllers, and the rest hidden in database constraints. This fragmentation makes systems fragile and difficult to reason about.
Domain-Driven Design (DDD) offers a better way: The Rich Domain Model.
The Role of the Domain Layer
The Domain Layer should be the "source of truth" for functional specifications. It’s the home of your Aggregate Roots—entities that are responsible for maintaining their own internal consistency. If a business rule says "A supplier's item price must always be greater than zero," the Domain Model should make it impossible for that rule to be broken.
Anemic vs. Rich Models
Most developers start with "Anemic" models—simple classes with getters and setters. In an anemic model, the validation happens outside the object, usually in a service. In a Rich Model, the object protects itself.
public class Meal
{
public string Name { get; private set; }
public decimal PricePerServing { get; private set; }
public Meal(string name, decimal pricePerServing)
{
Name = string.IsNullOrWhiteSpace(name)
? throw new ArgumentException("Meal name is required.")
: name;
SetPricePerServing(pricePerServing);
}
public void SetPricePerServing(decimal pricePerServing)
{
if (pricePerServing <= 0)
throw new InvalidOperationException("Price per serving must be greater than zero.");
PricePerServing = pricePerServing;
}
}The Power of Explicit Rules
As systems grow, simple if statements in setters can become bloated. This is where Explicit Rule Objects shine. By extracting the logic into a reusable rule, you make the requirement a first-class citizen of your code.
public interface IBusinessRule<T>
{
bool IsSatisfiedBy(T candidate);
string Message { get; }
}
public sealed class MealPriceMustBePositiveRule : IBusinessRule<decimal>
{
public string Message => "Price per serving must be greater than zero.";
public bool IsSatisfiedBy(decimal candidate) => candidate > 0;
}Now, your entity method becomes a clean consumer of this rule:
public void ChangePrice(decimal pricePerServing)
{
var rule = new MealPriceMustBePositiveRule();
if (!rule.IsSatisfiedBy(pricePerServing))
{
throw new DomainRuleViolationException(rule.Message);
}
PricePerServing = pricePerServing;
}Why this works
Consistency: The object can never be in an "invalid" state. If it exists in memory, it follows the rules.
Readability:
MealPriceMustBePositiveRuledescribes why a change failed, not just that a decimal was too small.Testability: You can unit test the rule in isolation without needing to instantiate complex entities.
My take: Validation is not the same as an Invariant. Validation is about checking user input at the edge; Invariants are about protecting the integrity of your business data at the core.
Bottom line:
Don't let your business logic leak into your infrastructure. Encode your requirements directly into your Domain Layer using invariants. Your code should be a reflection of your functional specifications—if the business says it can't happen, the code shouldn't allow it to exist.