DEV Community

Rohit Maharashi
Rohit Maharashi

Posted on

Salesforce Rules Inference Engine – Version 1 (Field-Optimized Modular Approach)

This version of the rules engine is designed for clarity, modularity, and maintainability. It evaluates rules against SObject records in a scalable, bulkified manner, with optimizations to limit redundant field access and criteria evaluation.

Process & Architecture
High-Level Flow

  • Load Rules & Criteria: Retrieve all active rules and their associated criteria for the target object.

  • Organize by Field: Group criteria by the field they reference, so each field is only accessed once per record.

  • For Each Record:

    • For each relevant field, evaluate all associated criteria.
    • Store the result of each criterion (true/false).
    • For each rule, substitute the results into the rule’s logical expression (e.g., 1 AND (2 OR 3)).
    • If the rule expression evaluates to true, collect the rule as a match for that record.
  • Return Results: Output a list of matches (record, rule, and optionally actions).

Key Components & Code Snippets

A. RuleEngine (Entry Point)

public class RuleEngine {
    public static List<RuleMatchResult> evaluate(List<SObject> records, String targetObjectApiName) {
        RuleRepository repo = new RuleRepository();
        RuleExecutionContext context = repo.loadContext(targetObjectApiName);
        List<RuleMatchResult> results = new List<RuleMatchResult>();
        for (SObject record : records) {
            Map<Integer, Boolean> criteriaResults = CriteriaEvaluator.evaluate(record, context);
            for (RuleExpression ruleExpr : context.ruleExpressions) {
                Boolean matched = RuleEvaluator.evaluate(ruleExpr, criteriaResults);
                if (matched) {
                    results.add(new RuleMatchResult(record.Id, ruleExpr.ruleId));
                }
            }
        }
        return results;
    }
}
Enter fullscreen mode Exit fullscreen mode

B. RuleRepository (Load & Organize Rules/Criteria)

public class RuleRepository {
    public RuleExecutionContext loadContext(String targetObjectApiName) {
        // Query active rules
        List<Rule__c> rules = [SELECT Id, RecordType.name, Status__c, Rule_End_Date__c, Rule_Expression__c, Rule_Name__c, Rule_Start_Date__c, Where_Clause__c, Fulfillment_Type__c, Rule_Criteria__c, Transaction_Type__c, 
                               Transaction_Subtype__c, Transaction_Status__c FROM Rule__c 
                               WHERE Status__c = 'Active' AND RecordType.name = 'Fulfillment Rules Engine'];
        Set<Id> ruleIds = new Map<Id, Rule__c>(rules).keySet();
        // Query criteria and junctions
        List<Rule_Criteria__c> junctions = [SELECT Id, RecordType.Name, Rule__c, Criteria_Number__c, Condition__r.Field_Name__c, Condition__r.Operation_Type__c, Condition__r.Value__c, Condition__r.Condition_Signature__c, 
                                           Condition__r.Field_Type__c, Condition__r.SObject_Name__c, Negate_Condition__c 
                                           FROM Rule_Criteria__c where Rule__c IN  :ruleIds];
        RuleExecutionContext context = new RuleExecutionContext();
        for (Rule__c rule : rules) {
            context.ruleExpressions.add(new RuleExpression(rule.Id, rule.Rule_Expression__c));
        }
        for (Rule_Criteria__c jc : junctions) {
            if(!context.fieldToCriteria.containsKey(jc.Condition__r.Field_Name__c)) {
                context.fieldToCriteria.put(jc.Condition__r.Field_Name__c, new List<RuleCriteriaWrapper>());
            }
            context.fieldToCriteria.get(jc.Condition__r.Field_Name__c).add(new RuleCriteriaWrapper(jc.Rule__c, Integer.valueOf(jc.Criteria_Number__c), jc.Condition__r.Operation_Type__c, jc.Condition__r.Value__c));
        }
        return context;
    }
}
Enter fullscreen mode Exit fullscreen mode

C. RuleExecutionContext (Runtime Data)

public class RuleExecutionContext {
    public Map<String, List<RuleCriteriaWrapper>> fieldToCriteria = new Map<String, List<RuleCriteriaWrapper>>();
    public List<RuleExpression> ruleExpressions = new List<RuleExpression>();
}
Enter fullscreen mode Exit fullscreen mode

D. CriteriaEvaluator (Evaluate All Criteria for a Record)

public class CriteriaEvaluator {
    public static Map<Integer, Boolean> evaluate(SObject record, RuleExecutionContext context) {
        Map<Integer, Boolean> result = new Map<Integer, Boolean>();
        for (String field : context.fieldToCriteria.keySet()) {
            Object actual = record.get(field);
            for (RuleCriteriaWrapper rc : context.fieldToCriteria.get(field)) {
                Boolean passed = evaluateCriterion(actual, rc.operator, rc.value);
                result.put(rc.criteriaNumber, passed);
            }
        }
        return result;
    }
    private static Boolean evaluateCriterion(Object actualValue, String operator, String expectedValue) {
        if (actualValue == null) return false;
        String a = String.valueOf(actualValue);
        String b = expectedValue;
        if (operator == 'equal_to') return a == b;
        if (operator == '!=') return a != b;
        if (operator == '>') return Decimal.valueOf(a) > Decimal.valueOf(b);
        if (operator == '<') return Decimal.valueOf(a) < Decimal.valueOf(b);
        // Add more as needed
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

E. RuleEvaluator & ExpressionResolver (Logical Expression Evaluation)

public class RuleEvaluator {
    public static Boolean evaluate(RuleExpression expr, Map<Integer, Boolean> criteriaResults) {
        String resolved = ExpressionResolver.substitute(expr.expression, criteriaResults);
        return ExpressionResolver.evalBooleanExpression(resolved);
    }
}
public class ExpressionResolver {
    public static String substitute(String expr, Map<Integer, Boolean> results) {
        for (Integer key : results.keySet()) {
            expr = expr.replaceAll('\\b' + String.valueOf(key) + '\\b', String.valueOf(results.get(key)));
        }
        return expr;
    }
    public static Boolean evalBooleanExpression(String expr) {
        expr = expr.replaceAll('AND', '&&').replaceAll('OR', '||').replaceAll('NOT', '!');
        // Note: For production, use a robust parser here!
        return Boolean.valueOf(evalWithCustomParser(expr));
    }
    private static Boolean evalWithCustomParser(String expr) {
        // Placeholder: implement or use a library for complex logic
        return expr.contains('true') && !expr.contains('false'); // naive
    }
}
Enter fullscreen mode Exit fullscreen mode

F. RuleMatchResult (Result Structure)

public class RuleMatchResult {
    public Id recordId;
    public Id ruleId;
    public RuleMatchResult(Id recordId, Id ruleId) {
        this.recordId = recordId;
        this.ruleId = ruleId;
    }
}
Enter fullscreen mode Exit fullscreen mode

🧮 Time Complexity (Optimized)

Where
N = Number of records to evaluate
R = Number of rules
C = Average number of criteria per rule
F = Number of distinct fields referenced across all rules
K = Average number of criteria per field (i.e., how many criteria reference the same field)

Pros

  • Modular & Maintainable: Each component is focused and testable.
  • Bulkified: Supports processing lists of records efficiently.
  • Field-Level Optimization: Only accesses each relevant field once per record, reducing SOQL and CPU usage.
  • Extensible: Easy to add new operators, criteria types, or actions.
  • Declarative Logic: Business logic is data-driven and manageable by admins.

Cons

  • No Cross-Record Memoization: If many records have the same field value, the same criteria may be evaluated repeatedly (less efficient for large, repetitive data sets).
  • Expression Evaluation is Basic: Current boolean logic parser is simplistic; complex nested logic requires a more robust solution.
  • No Early Rule Skipping: All rules are evaluated for all records, even if some could be skipped based on missing fields.
  • Not RETE-Optimized: Does not share evaluation work across records as in advanced rule engines.

When to Use

  • Moderate Data Volumes: When the number of records and rules is manageable, and field values are highly variable.
  • Simplicity & Maintainability: When code clarity and ease of maintenance are more important than maximum performance.
  • Foundation for Future Optimization: Can be incrementally improved (e.g., add memoization, postfix parsing) as needs grow.

Summary Table

Conclusion
Version 1 of the Salesforce Rules Inference Engine provides a clean, maintainable, and scalable foundation for rules evaluation. It is well-suited for most business scenarios with moderate scale and complexity. For enterprise-scale or highly repetitive data, further optimizations (as in Version 2) may be warranted.

For enhancements, consider adding:

  • Robust boolean expression parsing (postfix/infix parser)
  • Cross-record memoization for shared field-value evaluations
  • Early rule skipping based on required fields

Top comments (0)