Home / Code / Using the Specification Pattern In Your Domain Model
Specification Pattern
Specification Pattern

Using the Specification Pattern In Your Domain Model

The planning team here at eMoney Advisor is responsible for modeling business rules in the financial domain, which can often be both complex and overlapping. As developers, it is our job to break down these complex rules into simple conditions. Using the specification pattern, a simple and efficient method to combine business rules, our team is able to do just that. Let’s look at the interface in C# for example:

public interface ISpecification<T>
{
    bool IsSatisfiedBy(T obj);
}

However, that simplicity hides a lot of value. The specification pattern gives you a chance to enrich your domain model by naming things. For example, if you wanted to create a specification that checks whether a person is older than a certain age, you could create this:

public class OlderThanSpecification : ISpecification<Person>
{
    private int age;

    public OlderThanSpecification(int age)
    {
        this.age = age;
    }

    public bool IsSatisfiedBy(Person person)
    {
        return person.Age > age;
    }
}

There is a lot of value in using this concept of being above or below a certain age versus just checking it with an ‘if’ statement. If it’s important to the domain, as is often the case in financial planning when it comes to items like retirement distributions, it’s good to give the concept a concrete name that conveys its meaning and importance. It’s also nice to give it a place to live if it’s important to multiple objects.

From a technical perspective, the specification pattern shines in its ability to compose small specification objects into larger checks against more complex business rules.

The Composite Specification

A composite specification takes more than one specification as a dependency and works with both. You can do this with any number of specifications arbitrarily, but often, it’s worth creating three composite specification classes to handle AND, OR and NOT cases. The AND specification would look like this:

public class AndSpecification<T> : ISpecification<T>
{
    private ISpecification<T> firstSpec;
    private ISpecification<T> secondSpec;

    public AndSpecification(ISpecification<T> firstSpec, ISpecification<T> secondSpec)
    {
        this.firstSpec = firstSpec;
        this.secondSpec = secondSpec;
    }

    public bool IsSatisfiedBy(T obj)
    {
        return firstSpec.IsSatisfiedBy(obj) && secondSpec.IsSatisfiedBy(obj);
    }
}

I’ll leave the OR and NOT cases as an exercise for the reader.

Enough theory, let’s see an example!

If you have a 401k retirement account, you’re charged a penalty if you withdrawal funds from it before you’re 60 years old, except under certain circumstances such as becoming disabled or if you’re using the funds to pay for a qualified medical expense. Similarly, if you have a health savings account, you will be charged a penalty on a withdrawal unless you’re using the money for a qualified medical expense. This changes if you become fully disabled or reach the age of 65 – then the money is yours, penalty-free.

You can probably see where this is headed. Some of these rules overlap. Others are similar but with different thresholds. They both have many conditions and exceptions that are simple in and of themselves but you need to check them all to determine if the penalty is applicable. You could write them in an `if` statement, but it would quickly get unwieldy and you’d see some duplication. It’s also easy to imagine that there are many more rules that would use some, or all, of these conditions to determine the penalty impact of your decision. And finally, these rules often change as the IRS and Congress update laws.

I’ve already created the OlderThanSpecification() above, so the additional specifications we’d need are below, as well as the composite specification for each of the account types (401k and HSA).

public interface ITransaction
{
    Person AccountOwner { get;}
    TransactionType Type { get; }
}

public class IsQualifiedMedicalExpenseSpecification : ISpecification<ITransaction>
{
    public bool IsSatisfiedBy(ITransaction transaction)
    {
        return transaction.Type == TransactionType.QualifiedMedicalExpense;
    }
}

public class AccountOwnerIsDisabledSpecification : ISpecification<Person>
{
    public bool IsSatisfiedBy(Person person)
    {
        return person.IsDisabled;
    }
}

public class HealthSavingsAccountWithdrawalIsNotPenalizedSpecification : ISpecification<ITransaction>
{
    private readonly int AGE_LIMIT = 65;

    private OrSpecification<Person> accountHolderIsValid;
    private IsQualifiedMedicalExpenseSpecification withdrawalIsForQualifiedMedicalExpense;

    public HealthSavingsAccountWithdrawalIsNotPenalizedSpecification()
    {
        accountHolderIsValid = new OrSpecification(new OlderThanSpecification(AGE_LIMIT), new AccountOwnerIsDisabledSpecification());
        withdrawalIsForQualifiedMedicalExpense = new IsQualifiedMedicalExpenseSpecification();
    }

    public void IsSatisfiedBy(ITransaction transaction)
    {
        return accountHolderIsValid.IsSatisfiedBy(transaction.AccountOwner) || withdrawalIsForQualifiedMedicalExpense.IsSatisfiedBy(transaction);
    }
}

public class Four01kWithdrawalIsNotPenalizedSpecification : ISpecification<ITransaction>
{
    private readonly int AGE_LIMIT = 60;

    private OrSpecification<Person> accountHolderIsValid;
    private IsQualifiedMedicalExpenseSpecification withdrawalIsForQualifiedMedicalExpense;

    public HealthSavingsAccountWithdrawalIsNotPenalizedSpecification()
    {
        accountHolderIsValid = new OrSpecification(new OlderThanSpecification(AGE_LIMIT), new AccountOwnerIsDisabledSpecification());
        withdrawalIsForQualifiedMedicalExpense = new IsQualifiedMedicalExpenseSpecification();
    }

    public void IsSatisfiedBy(ITransaction transaction)
    {
        return accountHolderIsValid.IsSatisfiedBy(transaction.AccountOwner) || withdrawalIsForQualifiedMedicalExpense.IsSatisfiedBy(transaction);
    }
}

public class Four01k
{
    ...
    public void PerformWithdrawal(ITransaction transaction)
    {
        ...
        if (!(new Four01kWithdrawalIsNotPenalizedSpecification().IsSatisfiedBy(transaction)))
        {
            // create additional transaction for penalty amount
        }
    }
    ...
}

You may notice that you could pass the age into a constructor and collapse these two composite specifications into one object. I’d say be wary of that. I’ve only given a small subset of the actual rules for each. They’re also likely to change for different reasons in the future, and therefore, it makes sense to keep them separate. Lastly, we’ve created a domain concept here and it’s important to keep it separate. The concept is an abstraction of what it means for a 401k withdrawal to be penalty-free. It is likely that some condition will always cause a penalty for a 401k, although those conditions could change. Same for the health savings account.

Understanding Where Specifications Are Useful

It’s easy to see a new pattern and want to apply it everywhere. I want to take the last little bit of this blog post to warn against that. The specification pattern has a very specific use. Even though its technical implementation is simple, the real value is in its context. It allows you to make a business rule explicit in your codebase.

I would not suggest using the specification pattern to do validation. Validation typically looks for something that BREAKS the rules. The specification, conversely, aims to let you know that a thing CONFORMS to the rules. That’s subtle, but important.

At the same time, with LINQ now prevalent in .NET, I would not suggest using the specification pattern anywhere you need to do some filtering or selecting based on a criteria. This is for the same reasons as above. The specification helps make a business rule explicit. It is most useful in the domain layer where it can surface a business rule, not a technical one.

If your business domain has rules that are ever-changing, remember that you do not need to succumb to the complexity. You can break down the complexity into simple specifications and combine them!

About Daniel Donahue

Daniel Donahue is a technical lead at eMoney Advisor. He enjoys hiking, hockey and heavy metal.