Spyros PonarisThe Rule Pattern in C#, and Why It Works So Well with the Result Pattern Business rules...
Business rules have a bad habit of spreading everywhere. A little validation in a command handler, another check in a service, one more if in the entity, and before long the real policy of the system is buried under conditionals. The Rule pattern solves that by giving each business rule its own object and a clear name. In practice, this style is very close to what Fowler describes as the Specification pattern, especially when you start combining rules with AND, OR, and NOT. (martinfowler.com)
At the same time, many of us do not want to throw exceptions for expected business failures. That is where the Result pattern fits naturally. Instead of throwing, we return a success or failure object that makes the outcome explicit to the caller. This is a common way to model expected failures in application code. (Enterprise Craftsmanship)
Put those two together, and the design becomes very clean:
That combination is not a formal framework rule, but it is a very practical pairing. One expresses business intent, the other expresses application flow. (martinfowler.com)
The Rule pattern wraps one business condition in one class.
Instead of writing this:
if (order.TotalAmount < 50m)
return Result.Failure("Order must be at least 50.");
if (!order.Customer.IsVerified)
return Result.Failure("Customer must be verified.");
if (order.Customer.CurrentBalance + order.TotalAmount > order.Customer.CreditLimit)
return Result.Failure("Credit limit exceeded.");
You move each rule into its own object:
public interface IRule<T>
{
bool IsSatisfiedBy(T entity);
string ErrorMessage { get; }
}
That interface is tiny, but it already gives you two important things: a way to evaluate a business rule, and a way to describe why it failed.
This is the heart of the pattern. A rule becomes a first-class concept in the codebase instead of just another anonymous if. Fowler’s Specification write-up is essentially about doing exactly this, making business criteria explicit and composable. (martinfowler.com)
Your BaseRule<T> adds the nice part:
public abstract class BaseRule<T> : IRule<T>
{
public abstract bool IsSatisfiedBy(T entity);
public abstract string ErrorMessage { get; }
public BaseRule<T> And(IRule<T> other) => new AndRule<T>(this, other);
public BaseRule<T> Or(IRule<T> other) => new OrRule<T>(this, other);
public BaseRule<T> Not() => new NotRule<T>(this);
}
Now a rule is no longer just a check. It is a building block. You can combine simple rules into richer business policies, which is one of the main strengths of Specification-style modeling. (martinfowler.com)
That means your code can start reading more like the business itself:
var rule = new OrderMinimumAmountRule(50m)
.And(new CustomerVerifiedRule())
.And(new CreditLimitRule());
This is much easier to understand than a long block of unrelated conditions.
Your AndRule<T> is straightforward:
public class AndRule<T> : BaseRule<T>
{
private readonly IRule<T> _left, _right;
public AndRule(IRule<T> left, IRule<T> right) => (_left, _right) = (left, right);
public override bool IsSatisfiedBy(T entity) =>
_left.IsSatisfiedBy(entity) && _right.IsSatisfiedBy(entity);
public override string ErrorMessage =>
_left.ErrorMessage + "; " + _right.ErrorMessage;
}
This says both rules must pass.
Your OrRule<T> says at least one rule must pass:
public class OrRule<T> : BaseRule<T>
{
private readonly IRule<T> _left, _right;
public OrRule(IRule<T> left, IRule<T> right) => (_left, _right) = (left, right);
public override bool IsSatisfiedBy(T entity) =>
_left.IsSatisfiedBy(entity) || _right.IsSatisfiedBy(entity);
public override string ErrorMessage => _left.ErrorMessage;
}
And NotRule<T> flips the result:
public class NotRule<T> : BaseRule<T>
{
private readonly IRule<T> _inner;
public NotRule(IRule<T> inner) => _inner = inner;
public override bool IsSatisfiedBy(T entity) => !_inner.IsSatisfiedBy(entity);
public override string ErrorMessage => $"Negated: {_inner.ErrorMessage}";
}
Conceptually, this is exactly the kind of composite behavior Fowler describes for specifications. (martinfowler.com)
Let’s say your domain has three simple rules:
Those rules become very small classes:
public sealed class OrderMinimumAmountRule : BaseRule<Order>
{
private readonly decimal _minimumAmount;
public OrderMinimumAmountRule(decimal minimumAmount)
{
_minimumAmount = minimumAmount;
}
public override bool IsSatisfiedBy(Order order)
=> order.TotalAmount >= _minimumAmount;
public override string ErrorMessage
=> $"Order must be at least {_minimumAmount:C}.";
}
public sealed class CustomerVerifiedRule : BaseRule<Order>
{
public override bool IsSatisfiedBy(Order order)
=> order.Customer.IsVerified;
public override string ErrorMessage
=> "Customer must be verified.";
}
public sealed class CreditLimitRule : BaseRule<Order>
{
public override bool IsSatisfiedBy(Order order)
=> order.Customer.CurrentBalance + order.TotalAmount <= order.Customer.CreditLimit;
public override string ErrorMessage
=> "Credit limit exceeded.";
}
Each class has one job. Each class is easy to test. Each class has a business name, not a technical name. That clarity is one of the biggest wins of this pattern. (martinfowler.com)
A rule can tell us whether the order is valid. But something still has to return that outcome to the caller.
That is where Result becomes useful.
Your handler already shows the idea well:
public class PlaceOrderCommandHandler(
IOrderRepository orders,
ICustomerRepository customers) : ICommandHandler<PlaceOrderCommand>
{
private readonly IOrderRepository _orders = orders;
private readonly ICustomerRepository _customers = customers;
public async Task<Result> Handle(PlaceOrderCommand command, CancellationToken cancellationToken)
{
var customer = await _customers.GetByIdAsync(command.CustomerId, cancellationToken);
if (customer is null)
return Result.Failure("Customer not found.");
var lines = command.Lines.Select(l => OrderLine.Create(l.ProductId, l.Quantity, l.UnitPrice));
var order = Order.Draft(customer, lines);
var rule = new OrderMinimumAmountRule(50m)
.And(new CustomerVerifiedRule())
.And(new CreditLimitRule());
if (!rule.IsSatisfiedBy(order))
return Result.Failure(rule.ErrorMessage);
order.Place();
await _orders.AddAsync(order, cancellationToken);
return Result.Success();
}
}
This reads nicely because the responsibilities are split cleanly. The rule answers, “Is this order allowed?” The result answers, “What happened when we tried?” That aligns well with Result-based error handling, where expected failures are returned explicitly instead of being hidden behind exceptions. (Enterprise Craftsmanship)
The Rule pattern is about decision making.
The Result pattern is about outcome modeling.
That distinction matters.
A rule does not need to know anything about HTTP, APIs, UI messages, or pipeline behavior. It just checks business truth. The application layer then takes that truth and wraps it in a Result, which can be returned to a controller, endpoint, or caller in a predictable way. That separation lines up well with layered DDD guidance, where the application layer coordinates work and the domain layer holds the business logic. (Microsoft Learn)
One nice improvement is to teach rules how to convert themselves into a Result.
For example:
public static class RuleExtensions
{
public static Result ToResult<T>(this IRule<T> rule, T entity)
{
return rule.IsSatisfiedBy(entity)
? Result.Success()
: Result.Failure(rule.ErrorMessage);
}
}
Then the handler becomes even cleaner:
public async Task<Result> Handle(PlaceOrderCommand command, CancellationToken cancellationToken)
{
var customer = await _customers.GetByIdAsync(command.CustomerId, cancellationToken);
if (customer is null)
return Result.Failure("Customer not found.");
var lines = command.Lines.Select(l => OrderLine.Create(l.ProductId, l.Quantity, l.UnitPrice));
var order = Order.Draft(customer, lines);
var rule = new OrderMinimumAmountRule(50m)
.And(new CustomerVerifiedRule())
.And(new CreditLimitRule());
var validationResult = rule.ToResult(order);
if (validationResult.IsFailure)
return validationResult;
order.Place();
await _orders.AddAsync(order, cancellationToken);
return Result.Success();
}
Now the handler is just orchestration, which is exactly what many DDD and CQRS guides recommend for the application layer. (Microsoft Learn)
For a tutorial, putting the rule composition in the command handler is totally fine because it makes the pattern easy to see.
In a richer domain model, though, the most important invariants often belong inside the aggregate itself. Microsoft’s DDD guidance is very explicit here: aggregates are responsible for enforcing invariants across state changes, and application code should not become the real guardian of business consistency. (Microsoft Learn)
So a more domain-driven version could look like this:
public Result Place()
{
var rule = new OrderMinimumAmountRule(50m)
.And(new CustomerVerifiedRule())
.And(new CreditLimitRule());
if (!rule.IsSatisfiedBy(this))
return Result.Failure(rule.ErrorMessage);
Status = OrderStatus.Placed;
return Result.Success();
}
Then the handler becomes:
public async Task<Result> Handle(PlaceOrderCommand command, CancellationToken cancellationToken)
{
var customer = await _customers.GetByIdAsync(command.CustomerId, cancellationToken);
if (customer is null)
return Result.Failure("Customer not found.");
var lines = command.Lines.Select(l => OrderLine.Create(l.ProductId, l.Quantity, l.UnitPrice));
var order = Order.Draft(customer, lines);
var result = order.Place();
if (result.IsFailure)
return result;
await _orders.AddAsync(order, cancellationToken);
return Result.Success();
}
This version is often closer to true DDD, because the order protects its own validity. (Microsoft Learn)
Your example is great for teaching, but there is one thing worth mentioning in the article so readers do not copy it blindly.
Right now, AndRule.ErrorMessage always concatenates both messages:
public override string ErrorMessage =>
_left.ErrorMessage + "; " + _right.ErrorMessage;
That means it can return messages for rules that actually passed.
And OrRule.ErrorMessage always returns the left message:
public override string ErrorMessage => _left.ErrorMessage;
So for production code, many teams move toward one of these options:
Result with structured errorsIReadOnlyList<string> GetErrors(T entity)
That kind of richer error modeling fits especially well with Result-based application flows. (Enterprise Craftsmanship)
A simple improvement could look like this:
public interface IRule<T>
{
bool IsSatisfiedBy(T entity);
IEnumerable<string> GetErrors(T entity);
}
But for a beginner-friendly tutorial, your current shape is still excellent because it keeps the idea small and easy to follow.
The Rule pattern is a clean way to model business rules as reusable objects. Instead of burying domain knowledge inside scattered conditionals, you give each rule a name, a purpose, and a place in the model. When you add composition with AND, OR, and NOT, the code starts to read much closer to the business language itself. (martinfowler.com)
The Result pattern complements that perfectly. Rules decide whether an operation is allowed, while Results communicate whether the operation succeeded or failed in a clear and explicit way. Used together, they produce application code that is easier to read, easier to test, and much harder to misunderstand. (Enterprise Craftsmanship)
Martin Fowler, Specifications. (martinfowler.com)
Microsoft Learn, Designing validations in the domain model layer. (Microsoft Learn)
Microsoft Learn, Implementing the microservice application layer using the Web API. (Microsoft Learn)
Microsoft Learn, Implementing a microservice domain model with .NET. (Microsoft Learn)
Vladimir Khorikov, Specification Pattern vs Always-Valid Domain Model. (Enterprise Craftsmanship)
Vladimir Khorikov, Error handling: Exception or Result? (Enterprise Craftsmanship)
Milan Jovanović, Functional Error Handling in .NET With the Result Pattern. (Milan Jovanović)
I can also turn this into a LinkedIn/dev.to version with a stronger hook and shorter paragraphs.