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;
}
}
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;
}
}
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>();
}
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;
}
}
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
}
}
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;
}
}
🧮 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)