Business Rule Engine (BRE)
Structure
WHEN <pattern over facts>
THEN <action / conclusion>
Unlike ECA, there is no explicit event — rules fire when their pattern matches the current state of working memory, whenever the engine is invoked.
Core Components
| Component | Role |
|---|---|
| Working Memory | Mutable set of facts the engine reasons over |
| Rule Base | Set of IF/THEN rules (the knowledge base) |
| Inference Engine | Matches facts to rules, resolves conflicts, fires actions |
| Agenda | Ordered queue of activated (matched but not yet fired) rules |
| Conflict Resolution | Strategy for ordering the agenda |
The Rete Algorithm
The dominant algorithm for efficient rule matching, invented by Charles Forgy (1979). The naive approach — re-evaluating every rule against every fact on every cycle — is O(R×F). Rete reduces this dramatically by:
Key insight: most facts and rules don’t change between cycles. Cache intermediate match state rather than recomputing it.
Rete network structure:
Facts enter at root
│
[Alpha nodes] ← single-fact pattern matching
e.g. client.diagnosis == "F32"
│
[Beta nodes] ← multi-fact joins
e.g. client.clinic_id == pathway.clinic_id
│
[Terminal nodes] ← fully matched rules → added to Agenda
- Alpha memory — stores facts matching a single condition
- Beta memory — stores partial matches across multiple conditions
- Incremental updates — when a fact is added/modified/retracted, only affected network paths are re-evaluated
Rete trades memory for CPU — it can be memory-intensive for large fact sets.
Successors: Rete II, Rete III, LEAPS, PHREAK (Drools 6+).
Forward vs Backward Chaining
Forward chaining (data-driven) Start from known facts, fire applicable rules, add new facts, repeat until no new facts are produced (fixed point / quiescence):
Facts: diagnosis=F32, phq9=18
R1: diagnosis=F32 → high_risk=true # fires, adds fact
R2: high_risk=true, phq9 > 15 → urgent=true # fires, adds fact
R3: urgent=true → flag_for_review=true # fires, adds fact
→ quiescence: no more rules applicable
Most production BREs (Drools, CLIPS) default to forward chaining.
Backward chaining (goal-driven) Start from a goal, work backwards to find facts that would satisfy it. Used in Prolog, and optionally in Drools via query syntax:
Goal: is client urgent?
← needs: high_risk=true AND phq9 > 15
← needs: diagnosis=F32
← check facts: diagnosis=F32 ✓, phq9=18 ✓ → yes
Better when you have a specific question and want to avoid evaluating irrelevant rules.
Conflict Resolution Strategies
When multiple rules are activated simultaneously, the agenda must be ordered:
| Strategy | Description |
|---|---|
| Priority / salience | Explicit numeric weight — higher fires first |
| Specificity | More conditions = more specific = fires first |
| Recency | Rule that matched the most recently added fact fires first |
| MEA (Means-Ends Analysis) | Prioritise rules that modify the first condition’s fact |
| Breadth | Oldest activated rule fires first (FIFO) |
| Complexity | Most conditions fires first |
| Random | Non-deterministic — rules must be truly independent |
Most systems let you mix strategies. Drools uses salience + recency by default.
Rule Anatomy (Drools DRL syntax)
rule "Flag high-risk client for intensive pathway"
salience 10
when
$client : Client(
diagnosisCode in ("F32", "F33"),
phq9Score > 15,
active == true
)
$pathway : Pathway(
intensity == "HIGH",
clinic == $client.clinic
)
Clinic(
this == $client.clinic,
features contains "INTENSIVE_CARE"
)
then
insert(new AssignmentFlag($client, $pathway, "high_risk_match"));
System.out.println("Flagged: " + $client.getId());
endKey features: pattern binding ($client), cross-fact joins ($client.clinic), insert adds new facts to working memory.
Fact Lifecycle
insert(fact) → adds to working memory, triggers re-matching
modify(fact) → updates in place, re-evaluates affected rules
retract(fact) → removes from working memory
update(fact) → alias for modify in some systems
Modifying facts mid-session is what drives chaining — a fired rule inserts a new fact, which activates further rules.
Sessions
Stateful session Working memory persists across multiple fireAllRules() calls. Facts accumulate. Used for long-running processes (monitoring, stateful workflows).
session = engine.new_stateful_session()
session.insert(client)
session.insert(pathway)
session.fire_all_rules()
# later...
session.insert(new_score)
session.fire_all_rules() # re-evaluates with accumulated state
session.dispose()Stateless session Working memory is discarded after each invocation. Cheaper, safe for request-scoped evaluation (validation, classification, pricing).
result = engine.execute_stateless(facts=[client, pathway, clinic])Chaining Depth & Termination
Forward chaining can loop if rules are not carefully designed:
R1: A → insert B
R2: B → insert A ← infinite loop
Prevention strategies: - No-loop flag — rule won’t re-fire on facts it modified - Lock-on-active — rule won’t fire more than once per agenda activation - Salience ordering — ensure convergence by design - Fact guards — check before inserting (not exists AssignmentFlag(...))
Truth Maintenance (TMS)
Some BREs (CLIPS, Drools with logical keyword) support justified facts — facts inserted by a rule are automatically retracted if the conditions that justified them become false:
when
Client(phq9Score > 15)
then
insertLogical(new HighRiskFact()); // retracted if phq9Score drops ≤ 15Without TMS you must manually retract derived facts. With TMS, the engine maintains consistency automatically.
Major Systems
| System | Language | Notes |
|---|---|---|
| Drools | Java/JVM | Most widely used production BRE; PHREAK algorithm; decision tables; DMN support |
| CLIPS | C | NASA-originated; embeddable; forward chaining |
| Jess | Java | CLIPS-inspired; tight Java integration |
| OpenL Tablets | Java | Excel-based rules; popular in insurance/finance |
| Easy Rules | Java | Lightweight; annotation-driven; no Rete |
| NxBRE | .NET | Forward chaining; RuleML support |
Python durable_rules |
Python | Rete-based; Redis-backed stateful sessions |
| business-rules (PyPI) | Python | Lightweight; JSON-defined rules; no Rete |
BRE in Python (lightweight pattern)
Without a full Rete engine, a simple BRE for Django:
# rules/engine.py
from dataclasses import dataclass
from typing import Any, Callable
@dataclass
class Rule:
name: str
condition: Callable[[dict], bool]
action: Callable[[dict], Any]
priority: int = 0
class RuleEngine:
def __init__(self):
self.rules: list[Rule] = []
def register(self, rule: Rule):
self.rules.append(rule)
self.rules.sort(key=lambda r: -r.priority)
def run(self, facts: dict) -> list[Any]:
results = []
changed = True
while changed: # forward chain to quiescence
changed = False
for rule in self.rules:
if rule.condition(facts):
result = rule.action(facts)
if result:
results.append(result)
changed = True # new fact may activate more rules
return results# rules/definitions.py
from .engine import Rule, RuleEngine
engine = RuleEngine()
engine.register(Rule(
name="high_risk_depression",
condition=lambda f: (
f.get("diagnosis_code") in ("F32", "F33")
and f.get("phq9_score", 0) > 15
),
action=lambda f: f.update({"high_risk": True}) or "high_risk_flagged",
priority=10,
))
engine.register(Rule(
name="intensive_pathway_candidate",
condition=lambda f: (
f.get("high_risk")
and f.get("pathway_intensity") == "HIGH"
),
action=lambda f: f.update({"recommend_intensive": True}) or "intensive_recommended",
priority=5,
))# usage
facts = {
"diagnosis_code": "F32",
"phq9_score": 18,
"pathway_intensity": "HIGH",
"client_active": True,
}
results = engine.run(facts)
# results: ["high_risk_flagged", "intensive_recommended"]
# facts now contains: high_risk=True, recommend_intensive=TrueWhere BRE Fits vs ECA (recap)
| Concern | BRE | ECA |
|---|---|---|
| Trigger | Explicit invocation | Event occurrence |
| Multi-fact reasoning | Native (joins across facts) | Not native |
| Chaining | Default operating mode | Requires deliberate event emission |
| State | Working memory persists in session | Event context only |
| Best for | Classification, scoring, validation, pricing | Reacting to state changes |
Known Pitfalls
- Rule explosion — large rule bases become unmaintainable without discipline; hundreds of rules with subtle interactions
- Debugging difficulty — non-deterministic firing order makes tracing hard; need audit logs of which rules fired
- Performance at scale — Rete is memory-hungry; stateful sessions with large fact sets can exhaust heap
- Hidden coupling — rules that modify shared facts create implicit dependencies invisible in the rule text
- Over-engineering — for simple if/else logic, a BRE adds complexity without benefit; use it only when rules are numerous, change frequently, or need non-technical authorship
Summary
BRE = declarative rule evaluation over a fact working memory
Working memory → current state of the world (facts)
Rule base → IF/THEN patterns over facts
Inference engine → Rete matching + conflict resolution + firing
Agenda → ordered queue of activated rules
Key concerns: Rete efficiency, conflict resolution, chaining termination,
session statefulness, rule maintainability
Rules are best externalised to a BRE when they are numerous, change frequently, need non-technical ownership, or require multi-fact reasoning that would otherwise produce deeply nested conditional code.