loading...
Cover image for Where Do I Put My Business Rules And Validation?

Where Do I Put My Business Rules And Validation?

jamesmh profile image James Hickey Originally published at builtwithdot.net Updated on ・7 min read

This article originally appeared on the builtwithdot.net Blog.

Ever struggle with where to place your app's business rules and validation?

Should you put them in your MVC models?

Should you create special classes for validation?

To answer the question, I'll show you a battle-tested technique used in Domain Driven Design!

A Short DDD Primer

Domain Driven Design is a holistic approach and framework for designing software. It places a heavy emphasis on exploring business problems by starting with the business problem, how people involved currently deal with the problem, figuring out who the experts in this field are, consulting with the experts, exploring potential ways of modelling the problem, etc.

DDD is a huge subject. For today, we'll be looking at a technique that comes from DDD.

Let's look at two DDD concepts to lay the foundation.

Entity

An entity is simply a model that is identified by some unique identifier. Maybe a Customer, Order, Insurance Policy, etc.

When testing whether two entities are the same or equal, we test the unique ids of each to see if they match.

Value Object

Value objects correspond to things that aren't uniquely identifiable. Things like an address, a person's name, or a date range could be value objects.

They can hold multiple pieces of data like, as mentioned, a date range (which has a start and end date).

When testing whether two value objects are the same or equal, we test to see if all values are the same.

Combining The Two

Entities and value objects usually go hand-in-hand.

An entity is usually comprised of (a) other entities and (b) value objects.

For example, an Order entity might hold a reference to a few OrderItem entities. But it might also hold a reference to the price of the purchase (value object).

Where Does My Validation Go?

In DDD, it's a good practice for business rules and validation to go into the value objects.

There are two basic properties that make this a great technique - immutability and immediate validation at creation time.

Immediate Validation

By testing whether a value object is valid at creation time we ensure that it's not possible to have a value object that is in an invalid state.

Immutability

Making value objects immutable means that once they are created they cannot be modified.

Both of these properties ensure that you cannot have an object that is in an invalid state - either by creating it with invalid data or by causing side-effects that change the data.

It's quite simple yet powerful to know that you will always have instances of valid objects.

Insurance Policy Scenario

Let's look at a real-world example of how you might use value objects to perform business validation.

Imagine we had requirements to validate an insurance policy. This policy has:

  • Person Insured Age
  • Person Insured Gender
  • Person Insured Address
  • Date Started

Let's imagine the following rules exist:

  • Base price is $15 /month
  • When age is above 50 the price is doubled
  • When gender is Male the price is doubled (because men can be more prone to injure themselves more often than not...)
  • When an address is in Canada then the date started must be the beginning of the current month

Modelling Techniques

There's an entire area of study around modelling entities and value objects. At the end of the day, the general rule is to keep data that changes together in the same place.

This is actually a fundamental Object Oriented principle. One we often choose to ignore...

So, we need to figure out, based on business requirements, what pieces of data change together:

  • Age and Gender affect the Price
  • Address affects the Date Started

We can see from tracking what data changes together what our models might look like:

public class Object1
{
  public int Age { get; private set; }
  public Gender Gender { get; private set; }
  public decimal Price { get; private set; }

  public Object1(int age, Gender gender)
  {
    this.Age = age;
    this.Gender = gender;
    this.Price = 15.00m; // 1st business requirement: Base price is $15/month
  }
}
public class Object2
{
  public Address Address { get; private set; }
  public DateTime DateStarted { get; private set; }

  public Object2(Address address, DateTime dateStarted)
  {
    this.Address = address;
    this.DateStarted = dateStarted;
  }
}

Note About Naming

When creating objects using this technique, is usually recommended to not name your value objects and entities right away.

This ensures you focus on the data and where things belong based on the business. It's all too tempting to label our objects in our own minds - which brings in a bias about what "should" belong in each model.

But we shouldn't decide that - the business requirements should. We can give them meaningful names once we are sure our models are as good as we can make them.

Adding Our Business Rules

Let's add our first 3 rules.

We'll add this validation in our object's constructor. This satisfies the property of value objects needing to be valid at creation time. You should never be able to create an object that holds invalid data.

public class Object1
{
  public int Age { get; private set; }
  public Gender Gender { get; private set; }
  public decimal Price { get; private set; }

  public Object1(int age, Gender gender)
  {
    // Rule #1: Base price is $15/month.
    decimal basePrice = 15.00m;

    // Rule #2: When age is above 50 then price is doubled.
    if(age >= 50)
    {
      basePrice = basePrice * 2;
    }

    // Rule #3: When gender is Male then price is doubled.
    if(gender == Gender.Male)
    {
      basePrice = basePrice * 2;
    }

    this.Age = age;
    this.Gender = gender;
    this.Price = basePrice;
  }
}

How To Process Invalid State

There's a technique we'll use to ensure that our value object can never be in an invalid state:

  • Test a business requirement in the constructor of a value object
  • Throw an exception if a rule fails

For this, we'll create a new Exception type:

public class BusinessRuleException : Exception 
{
  public BusinessRuleException(string message) : base(message) { }
}

We'll use it to add our next business rule:

public class Object2
{
  public Address Address { get; private set; }
  public DateTime DateStarted { get; private set; }

  public Object2(Address address, DateTime dateStarted)
  {
    // Rule #4: If address is in Canada then Date Started must be the beginning of the current month.
    if(address.IsCanadian && dateStarted.Day != 1)
    {
      throw new BusinessRuleException("Canadian policies must begin on the first day of the current month.");
    }

    this.Address = address;
    this.DateStarted = dateStarted;
  }
}

One might point out that we could just change the Date Started to the beginning of the month automatically.

But, this is part of the requirement. Do we transform the date for the user or not? In this case, our business simply wants to inform the user of this business rule.

Further up, the caller would catch the exception and display the error message to the user.

But What About Showing Multiple Error Messages?

Glad you asked!

As a total aside - one way would be to replace the constructor with a static factory method. You might return a tuple (in C#) as the result:

public class Object2
{
  public Address Address { get; private set; }
  public DateTime DateStarted { get; private set; }

  // Make sure no one can create this object with a constructor.
  private Object2() { }

  // Static Factory Method that returns (a) the value object and (b) any errors.
  public static (Object2, IEnumerable<string>) Make(Address address, DateTime dateStarted)
  {
    var errors = new List<string>();

    // Rule #4: If address is in Canada then Date Started must be the beginning of the current month.
    if(address.IsCanadian && dateStarted.Day != 1)
    {
      errors.Add("Canadian policies must begin on the first day of the current month.");
    }

    // Imagine that there might be more business rules here.

    if(errors.Any()){
      return (null, errors); // Never return an invalid instance of the value object ;)
    }

    return (
      new Object2
      {
        Address = address,
        DateStarted = dateStarted
      }, 
      errors
    );
  }
}

There are other patterns for doing this, such as the Result Object pattern.

Building Our Entity

Let's now name our value objects and create an entity for our overall insurance policy.

Note: My names are not the best. It actually takes a long time and thought to give these meaningful names. If your business is taking DDD to it's fullest, then looking at an ubiquitous language upfront will always inform what names you choose to use, and potentially how you choose the model your objects.

public class InsurancePolicy
{
  public PolicyPricing Pricing { get; private set; }
  public PolicyAddress Address { get; private set; }

  public InsurancePolicy(PolicyPricing pricing, PolicyAddress address)
  {
    this.Pricing = pricing;
    this.Address = address;
  }
}

Using Our Value Objects For Validation

Here's how we would supply user input and create our entity:

try {
  var pricing = new PolicyPricing(age, gender);
  var address = new PolicyAddress(userAddress, dateStarted);

  // You can never get here unless all business rules are valid.
  var entity = new InsurancePolicy(pricing, address);

  // Now you might save the entity in a DB?
}
catch(BusinessRuleException ex)
{
  errorMessage = ex.Message; // This would be returned to the UI, etc.
}

Testing

You might notice that testing your value objects can make testing specific business rules very easy!

Get Going!

I hope this was a helpful introduction to how you might want to incorporate this Domain Driven Design concept in your code.

Using value objects this way can give you a framework to use for:

  • Modelling your business problems correctly
  • Figuring out where business validation should go

Keep In Touch

Don't forget to connect with me on twitter or LinkedIn!

Navigating Your Software Development Career Newsletter

An e-mail newsletter that will help you level-up in your career as a software developer! Ever wonder:

✔ What are the general stages of a software developer?
✔ How do I know which stage I'm at? How do I get to the next stage?
✔ What is a tech leader and how do I become one?
✔ Is there someone willing to walk with me and answer my questions?

Sound interesting? Join the community!

Discussion

pic
Editor guide
Collapse
turnerj profile image
James Turner

Well written article! While I do like parts of the DDD concept, I'm not a fan of validation occurring on the creation of objects as it feels like one too many concerns for a constructor.

Take that final snippet in your post, the code doesn't actually read like it does any validation at all - it is just instantiating instances of objects. There is probably a case to say other objects do this too (eg. you can't create an instance of Uri that is invalid) but it just feels wrong (to me) having business objects like that.

That said, it does eliminate the case that validation is accidentally missed.

Collapse
jamesmh profile image
James Hickey Author

I would agree that in scenarios that are not very complex there's overhead to this too.

But it really shines whenever you start dealing with complex business problems.

Some people prefer to do something like this, but instead of doing validation in the constructor they will expose some Validate() method.

It's one of many tools 😂

Collapse
turnerj profile image
James Turner

One of the things I like in C# is actually the built-in validation system in the System.ComponentModel.DataAnnotations namespace. You can have basic things like whether a property is required or a string is meant to be an email address. You can easily add your own custom validation attributes too. There is a class called Validator in that namespace with a ValidateObject function which processes it all.

The validation doesn't need to be constrained to the attributes either because if your object implements IValidatableObject, then a Validate method on your object gets called too when using Validator.

Keep up the good articles 🙂

Thread Thread
jamesmh profile image
James Hickey Author

Thanks! I've seen people do that too.

There's always the discussion as to how much should be validated using ModelState in .NET etc. too.

I find it helpful, all things equal, to have the bulk of non-trivial validation located in the same place (inside the business logic - wherever that may be).

I've worked on a system that had to validate ALL business rules client-side, then in the app layer, and finally in the DB (stored procs).

So like the exact same business rules re-done in JS, C# and then TSQL. Really weird. But that's what they wanted!

I did learn about all 3 languages really quick 😂

Collapse
ken473 profile image
Kenneth Mutombo

You summed it up well.

Collapse
qm3ster profile image
Mihail Malo

I feel like having "validation" in the constructor, or on the value object at all is at odds with evolving business rules.

If we have an escape hatch that skips validation when deserializing "known good", or rather "previously accepted" instances, means in our other logic we can't rely on the "invariants" being true, but without that being immediately obvious.

And always going through the constructor will just make our code break on old instances whenever we make the validation "stricter".

It's extremely common, unavoidable, really, to have an active InsurancePolicy that could no longer be created/issued with the current rules, and yet needs to be honored and handled until its end of life, which may be indefinite.

Collapse
jamesmh profile image
James Hickey Author

Sure, there's this version issue when applying something like this. It also exists when using event sourcing, for example. It's a trade-off that you need to be aware of. Basically, you need to keep your aggregates backward compatible in the event the scenario you mentioned applies.

Just like most things in programming - it's about trade-offs. If your domain isn't that complex then DDD isn't going to be helpful.

If your domain is pretty complex, then the trade-off might be worth it 👍

Collapse
qm3ster profile image
Mihail Malo

I'd argue that the more complex the domain, the more likely it is the rules will change. That's why I prefer for the entities to be dumb, and the validation to happen in the command handler. This way the current rules apply to the "decisions" that are yet to be made, but old decisions are respected.

Thread Thread
jamesmh profile image
James Hickey Author

Just to make sure I understand 🙂:

All your individual use cases are represented by command handlers, and each command handler has its own unique set of validation?

In that case, if new rules of this kind appear but you are using let's say a DDD aggregate kind of thing, depending on the rule that needs to change, etc. you could just create a new aggregate model?

That way, commands that share the same rules can just use the same aggregates, and the ones that don't can just use different aggregates. I'm fine with that.

Either way, this and what (I believe) you said are almost the same thing. The only difference being that by using aggregates you just have the ability to share a grouping of business invariants/rules across handlers.

👍

Thread Thread
qm3ster profile image
Mihail Malo

I mean yeah, you do end up constructing a write model to understand if a command is allowed. And having the model allows you to, if needed, express some of your rules as assertions on the model. In which case, you could express validation as:

If we assume the command is valid, and apply to the in-memory model the events that handling it would persist, will the resulting state of the model still fulfill all the invariants? If it does, persist the events, and consider the command successful.

There are some good properties to this, such as writing your invariants as assertions, and this automatically applying to all handlers.

But in practice it seems very brittle, since, once again, the "state" of an old entity might violate the currently checked invariants... which, while not great and probably requiring some thought, doesn't really need to fail commands that are orthogonal to the violated invariant.

Looking at a real world example, having an incorrectly-formatted phone number/billing address is "bad". But failing all transactions to/from the affected account until the issue is fixed, despite all the prerequisites specific to the transaction being met, is much worse.

And, of course, there are a lot more reasons to fail a command than "succeeding the command would put/leave the state in violation of an invariant". And these rules that apply to the command and not the state still need to share logic.

Collapse
rodrigojuarez profile image
Rodrigo Juarez

Nice article! Can you elaborate about how to do validation when the data is outside the entity (in your scenario, for example, customer settings)

Collapse
jamesmh profile image
James Hickey Author

If the data is coming from somewhere else, but is required to do a particular task, then usually I would have some repository or something that will fetch the data I need and then include that as part of the value object's dependencies.

Sometimes, if this means returning too much data, then having a repository with a method that returns a bool might be a better choice?

Collapse
rodrigojuarez profile image
Rodrigo Juarez

Usually, that's my approach; I have a validate method in the repo returning a tuple from there.

Collapse
qm3ster profile image
Mihail Malo

Is the "not prematurely naming your object" from the DDD book?

Collapse
jamesmh profile image
James Hickey Author

No, that was a tip I gleaned from Udi Dahan.

Collapse
rajaasyraf profile image
Raja Asyraf

As a junior, I always wonder what is the best way to deal with complex business rules. This is really helpful for me to start exploring DDD. Thanks for sharing.

Collapse
jamesmh profile image
James Hickey Author

You're welcome. DDD is fantastic for, like you said, dealing with those really nasty complex rules.

For simpler stuff, just do something simple.

The beauty of designing software is that you can use different tools for solving different problems!