DDD - how to enforce invariants but specific to the client requirements?

Viewed 173

I am trying to figure out how to keep the invariants still consistent for a few consumers (business clients) of the project who have their own requirements on the same version of the aggregate root. Let's take the Customer as an example and ask hypothetical question to meet the following silly logic:

public class Customer 
{
  public Id { get; private set;}
  public string Name { get; private set;}

  public void SetName(string name){
     //client1 -> requires the name not to be null
     //client2 -> requires the name can start with "J"
     //client3 -> some other business logic
     this.Name = name;
  }
}

For now, what I have in mind is having the custom validation/invariant check strategy logic like so:

public void SetName(string name, INameCheckStrategy strategy){
    if(!strategy.IsSatisfiedBy(name)) throw new BusinessException("name does not meet the invariant check!");
     this.Name = name;
  }

where

public class Client1NameCheckStrategy : INameCheckStrategy {
   public bool IsSatisfiedBy(string name){
     return name != null;
   } 
}

Any ideas how to handle such problem ?

3 Answers

You could take a sort of DDDD ("Dynamic Domain Driven Design", to coin a phrase) approach and reify the rules in your domain into aggregates of their own and associate them with clients. If you're going to model changing the rules, that might make things interesting (how do you handle a change that makes aggregates considered valid by a previous rule?).

If the domain model entities stay same concerning the data and the data has the same meaning for all business clients then using something like the strategy pattern (as you suggested) seams to be a good fit. Just make sure that you don't let any stuff like configuration infrastructure leak into your domain model and be strict by injecting the required information and client specific logic.

If there are several places in your aggregate where this specific logic applies you could also consider letting the repository (or a factory) inject the strategy when querying the aggregates from the repository collection.

Another option would be to go with specific Implementations of value objects which should already contain the business invariants for their data anyway. In your fabricated example there could be different types of CustomerName value objects (e.g. CustomerNameClientX). Depending on who's the current client you can then make sure that the corresponding customer name value object is created, has already validated itself during creation and is passed to the aggregate.

I know you've asked about C# and even mentioned DDD, but If I will be asked to actually help you, I'd give you an example from JS.

function setName(name) {
    this.name = name;
}

There's no type checks, and they're not needed, because limiting yourself BEFORE you actually need more safety is a bad habit.

Look, I've just used this property without checking it's type, and all my business cases are covered:

function insertToMongoByName(db, collectionName, record) {
    console.log(`Saving record by name: ${record.name}`);
    record._id = record.name;        
    db.collection(collectionName).insertOne(record);
}

Mongodb needs _id to be present, so I have satisfied this invariant.

console.log can work literally with anything, as many other libraries in JS (also see insertOne function I've used).

And here's another usage of the name:

_ = require("lodash");

function getEmailWithDisplayName(customer) {
    if(customer.fullEmail) {
        return customer.fullEmail;
    }

    if(!customer.email) {
        return undefined;
    }

    if(_.isString(customer.name)) {
        return `${customer.name} <${customer.email}>`;
    }
    else if(_.isObject(customer.name) && (customer.name.firstName || customer.name.lastName)) {
        const name = [customer.name.honorific, customer.name.firstName, customer.name.lastName]
            .filter(s => _.isString(s)).join(" ");
        return `${name} <${customer.email}>`;
    }

    return customer.email;
}

This time I had to do some typechecks around name, but it is very specific to the goal I was trying to achieve.

After this, my question to your architecture is: do you really want to fix your Customer class to support all possible future requirements? Will all future use-cases be happy about providing a "strategy" for checks, instead of moving forward with what they have in mind?

Related