The main Components here should be
action.execute(context)` without knowing what the action does. This makes adding new actions as simple as implementing a single-method interface.execute(context: EvaluationContext): void — performs an operation when a rule's condition is metDiscountAction(order_total_field, percentage) — modifies fact valuesNotificationAction(message, channel) — triggers external side effects (email, SMS, alert)FlagOrderAction(flag_name) — tags facts for downstream processingCompositeAction(actions) — chains multiple actions for one rulemodified_keys to support this safely.NotificationAction call external systems. Consider making them async to avoid blocking the evaluation pipeline.The above interfaces helps developer add any condition actions easily without touching core logic of the rule engine.
For each class, define the attributes (data) it will hold and the methods (functions) that operate on the attributes. Ensure they align with the object's responsibilities and adhere to the principle of encapsulation. Write your code in the code editor below.
Explain design tradeoffs you considered. Check and explain whether your design adheres to SOLID principles. Explain how your design can handle changes in scale and whether it would be easy to extend with new functionalities. Identify areas for future improvement...
Each call to rule_engine.evaluate(rule_set_id, facts) creates a fresh EvaluationContext — this is your isolation boundary:
# Concurrent requests — each gets its own context
def handle_request(request):
facts = Facts(request.data) # fresh facts per session
context = engine.evaluate("pricing", facts) # isolated context
return context.results
Why this works:
Trade-off: If multiple sessions need to see the same fact changes (e.g., inventory deductions), you'd layer a shared cache (Redis) on top and have actions call into it explicitly — the evaluation session stays isolated, but side effects go to shared storage.
Our current design shows the Evaluation Engine passing results to the Execution Engine. If rules modify shared state (facts), We need coordination across instances — otherwise two workers might evaluate stale facts simultaneously.
Rete Algorithm — For large rule sets at scale, evaluating every rule against every fact each time is O(rules × facts). The Rete algorithm pre-compiles conditions into a discrimination tree, so when a fact changes, only affected partial matches are re-evaluated — near O
A domain-specific language (DSL) could let business users write rules like:
IF order_total > 100 AND customer_tier == "gold"
THEN apply_discount(15%)
A parser transforms this into CompositeCondition(AND, ThresholdCondition, ComparisonCondition) + DiscountAction. A visual rule builder with drag-and-drop condition/action blocks could generate the same AST — requiring no coding. The key design requirement: the Rule/Condition/Action interfaces must remain stable so new DSL constructs map to existing primitives.
RuleSets should support versioned deployments:
engine.evaluate("pricing:v2", facts)Forward chaining starts with facts and fires matching rules. Backward chaining starts with a goal and works backwards to find what facts would make it true:
Goal: customer_gets_discount(true)
→ Which rules conclude this? Rule(condition: tier == "gold", action: set discount)
→ So we need fact: tier == "gold"
→ Check if tier is in facts. If not, find rules that set it.
This is powerful for diagnostic/validation scenarios — "why didn't this customer get a discount?" — and can be layered as an optional alternative to forward chaining via a BackwardChainingEngine implementing the same RuleEngine interface.