3

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.

Syed Danish
  • 106
  • 1
  • 9
  • I think this deserves it's own bounded context (e.g. Rules Management & Evaluation). The BC would then raise events such as `EmployeeShouldBePromoted`, `EmployeeSalaryShouldBeIncreased`. You don't even need aggregates in that context. You can just have read-only representations that serves as the rules evaluation context. Keep in mind that rules would all be eventually consistent and perhaps some actions could be rejected when executed against the aggregates in the remote context. The rationale behind this is that user-defined rules evaluation will probably end up always crossing AR boundaries – plalx Jun 24 '16 at 14:24
  • @plalx; Thanks but I think I am not getting your idea completely. Are you saying not to parse rules when user creates them instead they should be just saved and events should be raised which are handled by a separate bounded context maybe using CQRS+ES or something similar? If that is the case then I think this is not the problem i am looking to solve. When user creates a rule, we have no idea what domain objects those rules operate on. Rather I am modeling rule themselves as Domain objects, parse them and apply some business logic on themselves and not the objects which they operate on. – Syed Danish Jun 24 '16 at 19:31
  • To be more specific, I just want to capture user-entered rules in free-form text format, format them into a proprietary format (**ExecutableScript**) which rule engine understands and save them to be used later by rule engine. If rules themselves refer to some domain objects (which they do by the way), those objects would be part of some other BC. My question is Can I model the process of capturing users rule, formatting them and saving into database using DDD. Is DDD right approach for it. If so then how can I resolve issue #2 mentioned in my post above. Thanks. – Syed Danish Jun 24 '16 at 19:36
  • You can use DDD tactical patterns for highly technical domains, but that means creating enough abstractions to shield the entire technology-specific solution. This may be quite a lot of overhead especially for complex data structures such as an `ExecutableScript`. Perhaps the easiest way would be to store the rules in free-form text. If you want to avoid reparsing the text every time then the infrastructure layer may persist the ExecutableScript, but the domain doesn't have to know about it. Your parse and execute methods would take in a `Parser` and `Executor` respectively. – plalx Jun 24 '16 at 19:58
  • I'm curious... what creates the execution context necessary for evaluating the rules? How data is fetched? – plalx Jun 24 '16 at 20:02
  • Execution context is created using a host object which is a class in my case that contains other objects e.g. Employee. Host object is then passed to Rule engine which uses it to execute rules (combination of Condition/Actions in stored in "ExecutableScript" format. – Syed Danish Jun 27 '16 at 21:38
  • Are the rules executed directly against the aggregate? By that I mean... will the rules modify the state of the aggregate? – plalx Jun 28 '16 at 02:57

2 Answers2

3

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.
        }
    }
}
plalx
  • 42,889
  • 6
  • 74
  • 90
  • thanks for detailed answer. I was also thinking about something similar with the only difference of how to store the ExecutableScript data. You are right I don't want to re-generate ExecutableScript from user-defined Condition/Actions so I generate them and store into database as it's serializable. To store generated ExecutableScript object, I was thinking to create another set of objects e.g. ExecutableCondition & ExecutableAction in infrastructure layer and derive from Condition & Action objects from Domain layer however doesn't it make it an anemic model leading to my 3rd question? – Syed Danish Jun 27 '16 at 21:45
  • Well, anemic is more related to behavior than state. I think the behaviors you will be able to model in the domain are the one necessary to build the rules and maintain them. However, since the execution part is delegated to the infrastructure you cannot really model that behavior in the domain and there is not much you can do about it. For your 3rd question, DDD is not about specific modeling techniques like a domain model. You may implement your business logic with transaction scripts and still practice DDD: behaviors and the ubiquitous language are key. – plalx Jun 28 '16 at 02:54
  • Actually the Build behavior of expression/actions would need to be delegated to infrastructure layer as well as they would build to ExecutableScript objects. Just like Executor, I would need a Builder with their interfaces in domain layer. In such sense, it looks to me an anemic model. Also, what do you think about ExecutableCondotion and ExecutableAction classes in infrastructure layer derived from Condition and Action classes in domain layer to hold ExecutableScript objects? Does it sound right? – Syed Danish Jun 29 '16 at 01:41
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.

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;
    }
}
VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91
  • What about another BC that raises events when predicates are satisfied? This other BC wouldn't need aggregates at all, just read-only representations of their state. I feel this is more realistic as user-defined rules will tend to cross aggregate boundaries anyway. That way you do not need to introduce a very abstract State concept in your ARs to satisfy the technical implementation. – plalx Jun 24 '16 at 13:19
  • The AR have state already - that's the purpose of an aggregate, after all. In a short answer, I'm averse to the notion of inventing a new bounded context out of thin air. I deliberately avoided the additional complexity of multiple aggregates, because it wasn't part of the example, and I didn't want to confuse the main point (dependency inversion). – VoiceOfUnreason Jun 24 '16 at 13:41
  • It is, but to me the state is usually implicit, unless there is a technical motivation for having an explicit `State`, such as to make persistence easier. I do not think that a Rules Management & Evaluation context would be so artificial. Anyway, I wasn't criticising your answer, I just wanted your input on the idea ;) – plalx Jun 24 '16 at 14:21
  • Go not to the elves for counsel, for they will answer both no and yes. The notion that this domain publishes events, and that the Rules engine subscribes to those events, and either (a) publish its own recommendation events or (b) fire off command messages "makes sense", but you have to deal with the problem that the rules engine is working from stale data, whereas the aggregate is modifying itself in the present. What's supposed to happen if the aggregate no longer satisfies the predicate when the command is processed? Not unsolvable, but there are layers.... – VoiceOfUnreason Jun 24 '16 at 14:33
  • I totally agree and that is basically the motivation of this segregation. When you start giving users the ability to declare their own rules it is quite probable that these will end up crossing AR boundaries. The system could prevent creating such rules, but by experience it is very likely to happen because ARs cannot be planned around dynamic rules. Therefore I'd suggest embracing the staleness nature of these dynamic rules by explicitly moving them to another context. If you want everything strongly consistent then rules themselves must be part of the ARs on which they get evaluated. – plalx Jun 24 '16 at 15:14
  • It may as well be a very bad idea and perhaps rule predicates should never be allowed to cross AR boundaries and if they do then all AR involved shall be "locked" through optimistic concurrency. I'm not so sure how optimistic concurrency can be used for ARs that are only read but not modified though. You would have to increase the version with an artificial operation. – plalx Jun 24 '16 at 15:21
  • @VoiceOfUnreason; I know dependency inversion is an option here but that was not my major concern. My concern was to store the external type (**ExecutableScript**) within domain object itself. Even if rules are parsed outside of Domain layer using dependency inversion, the result which need to be stored within Expression or Action object is **ExecutableScript** object which is again not a domain object rather an object with proprietary format defined in an external assembly which means Domain Layer would have an external dependency anyway which voilates the DDD rule which is my 2nd question. – Syed Danish Jun 24 '16 at 19:20
  • @plalx; Can you please elaborate your idea a little more as a separate answer with some example? Thanks. – Syed Danish Jun 24 '16 at 19:22
  • @SyedDanish I'm not sure if it's adequate so I'll refrain from answering. However, the only way to shield yourself from the `ExecutableScript` is to apply dependency inversion again. You need a new domain abstraction for this, probably an Abstract Syntax Tree. This abstraction would then be converted to an ExecutableScript in the infrastructure layer to get evaluated. – plalx Jun 24 '16 at 19:32
  • @SyedDanish I'll try to further expand on my answer later, but short version is I think you are wrong about the direction of the dependencies. – VoiceOfUnreason Jun 24 '16 at 20:05