Syed Danish
Syed Danish

Reputation: 116

Implementing user-defined business rules with DDD

Let's say If I have an application which let's user create business rules to be applied on a domain entity. A rule can be a combination of a condition and multiple actions where if condition evaluates to true then corresponding actions are executed. This rule is created by users in free-form text format which is then converted to a proprietary format which rule engine can understand and execute.

E.g. For an employee management system, if there is business rule to check if an employee is working in current Role for more than an year and has performed better than expected then can be promoted to next role with a 10% salary increment. This business rule can be entered by users as below.

Condition: Employee.CurrentRoleLength > 1 && Employee.ExceededExpectations()
Action: Employee.PromoteToNextRole() | Employee.GiveSalaryIncrement(10)

Note that multiple Actions are delimited with a |. Also in order to execute this rule, application uses a separate rule engine class library to parse this condition and both actions to a proprietary format, say, ExecutableScript also defined in the rule engine class library.

Now in order to model this requirement using DDD; I have come up with following Domain objects.

Rule (Entity)
Condition (Value Object)
Action (Value Object)

where Rule is an Entity which contains a Condition Value Object and a list of Action Value Objects as below.

public class Rule : Entity
{
    public Condition Condition { get; private set; }
    public IList<Action> Actions { get; private set;}

    public Rule(Condition condition, IList<Action> actions)
    {
        Condition = condition;
        Actions = actions;
    }
}

public sealed class Condition : ValueObject<Condition>
{
    public string ConditionText { get; private set;}
    public ExecutableScript ExecutableCondition{ get; private set;}

    public Condition(string conditionText)
    {
        ConditionText = conditionText;            
    }     

    public Parse()
    {
        ExecutableCondition = // How to parse using external rule engine ??;            
    }

    public Execute()
    {
        // How to execute using external rule engine ??;            
    }
}      

public sealed class Action : ValueObject<Action>
{
    public string ActionText{ get; private set;}
    public ExecutableScript ExecutableAction{ get; private set;}

    public Action(string actionText)
    {
        ActionText = actionText;            
    }

    public Parse()
    {
        ExecutableAction = // How to parse using external rule engine ??;            
    }

    public Execute()
    {
        // How to execute using external rule engine ??;            
    }
}

Based on above domain model, I have following questions.

  1. How can I parse and execute Condition and Actions without having a dependency on external rule engine. I understand Domain layer should not have any dependency on outer layers and should be confined to it's own.

  2. Even if I Parse Condition and Actions outside their domain objects, still their parsed ExceutableScript value need to be present within them which will still need dependency on external rule engine.

  3. Is it just that DDD is not the right approach for this scenario and I am going into wrong direction.

Sorry for the long post. Any help would be highly appreciated.

Thanks.

Upvotes: 4

Views: 3832

Answers (2)

plalx
plalx

Reputation: 43728

Technical domains may benefit from DDD tactical patterns, but the cost of creating the right abstractions is usually higher than with other domains because it often requires to abstract away complex data structures.

A good way to start thinking about the required abstractions is to ask yourself what abstractions would be needed if you were to swap the underlying technologies.

Here you have a complex text-based expression from which an ExecutableScript is created by the rules engine.

If you think about it there three major elements here:

  1. The text-based expression syntax which is proprietary.
  2. The ExecutableScript which is proprietary; I will assume this is an Abstract Syntax Tree (AST) with an embedded interpreter.
  3. The rule evaluation context which is probably proprietary.

If you were to swap the underlying technology to execute the rules then the expression syntax of the other rule engine may be different and it would certainly have an entirely different rule interpretation mechanism.

At this point we have identified what have to be abstracted, but not what would be the proper abstractions.

You could decide to implement your own expression syntax, your own parser, your own AST which would be a tree-based representation of the expression in memory and finally your own rule evaluation context. This set of abstractions would then be consumed by specific rule engines. For instance, your current rule engine would have to convert a domain.Expression AST to an ExecutableScript.

Something like this (I left out the evaluation context intentionally as you did not provide any information on it).

enter image description here

However, creating your set of abstractions could be costly, especially if you do not anticipate to swap your rule engine. If the syntax of your current rules engine suits your needs then you may use it as your abstraction for text-based expressions. You can do this because it doesn't require a proprietary data structure to represent text in memory; it's just a String. If you were to swap your rule engine in the future then you could still use the old engine to parse the expression and then rely on the generated AST to generate the new one for the other rule engine or you could go back to writing your own abstractions.

At this point, you may decide to simply hold that expression String in your domain and pass it to an Executor when it has to be evaluated. If you are concerned by the performance cost of re-generating the ExecutableScript each time then you should first make sure that is indeed an issue; premature optimization is not desirable.

If you find out that it is too much overhead then you could implement memoization in the infrastructure executor. The ExecutableScript could either be stored in memory or persisted to disk. You could potentially use a hash of the string-based expression to identify it (beware collisions), the entire string, an id assigned by the domain or any other strategy.

Last but not least. Keep in mind that if rule actions aren't processed by aggregates or if the rule predicate spans multiple aggregates then the data used to evaluate the expression may have been stale. I'm not expanding on this because I have no idea how you plan to generate the rule evaluation context and process actions, but I thought it was still worth mentioning because invariant enforcement is an important aspect of every domains.

If you determine that all rules may be eventually consistent or that decisions made on stale data are acceptable then I'd also consider creating an entirely separate bounded context for that, perhaps called "Rule Management & Execution".

EDIT:

Here's an example that shows how creating a rule may look like form the application service perspective, given that expressions are stored as Strings in the domain.

//Domain
public interface RuleValidator {
    boolean isValid(Rule rule);
}

public class RuleFactory {
    private RuleValidator validator;

    //...

    public Rule create(RuleId id, Condition condition, List<Action> actions) {
        Rule rule = new Rule(id, condition, actions);

        if (!validator.isValid(rule)) {
            throw new InvalidRuleException();
        }

        return rule;
    }
}

//App
public class RuleApplicationService {
    private RuleFactory ruleFactory;
    private RuleRepository ruleRepository;

    //...
    public void createRule(String id, String conditionExpression, List<String> actionExpressions) {
        transaction {
            List<Action> actions = createActionsFromExpressions(actionExpressions);

            Rule rule = ruleFactory.create(new RuleId(id), new Condition(conditionExpression), actions);


            ruleRepository.add(rule); //this may also create and persist an `ExecutableScript` object transparently in the infrastructure, associated with the rule id.
        }
    }
}

Upvotes: 3

VoiceOfUnreason
VoiceOfUnreason

Reputation: 57299

How can I parse and execute Condition and Actions without having a dependency on external rule engine. I understand Domain layer should not have any dependency on outer layers and should be confined to it's own.

This part is easy: dependency inversion. The domain defines a service provider interface that describes how it wants to talk to some external service. Typically, the domain will pass a copy of some of its internal state to the service, and get back an answer that it can then apply to itself.

So you might see something like this in your model

Supervisor.reviewSubordinates(EvaluationService es) {
    for ( Employee e : this.subbordinates ) {
        // Note: state is an immutable value type; you can't
        // change the employee entity by mutating the state.
        Employee.State currentState = e.currentState;


        Actions<Employee.State> actions = es.evaluate(currentState);            
        for (Action<Employee.State> a : actions ) {
            currentState = a.apply(currentState);
        }

        // replacing the state of the entity does change the
        // entity, but notice that the model didn't delegate that.
        e.currentState = currentState;
    }
}

Upvotes: 1

Related Questions