Business Rule Engine (BRE)

A BRE is a system that externalises business logic from application code into declarative rules, evaluates them against a set of facts, and produces conclusions or actions. The core premise: rules are data, not code.
Author

Benedict Thekkel

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());
end

Key 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])

Rule Authoring Patterns

Decision table Rules expressed as a spreadsheet — each row is a rule, columns are conditions and actions. Non-technical authors can maintain them:

diagnosis phq9 > intensity → flag reason
F32 15 HIGH high_risk_intensive
F32 STANDARD standard_depression
F33 10 HIGH high_risk_intensive

Rule templates Parameterised rules generated from data — avoids copy-paste for rule families that differ only in values.

Domain Specific Language (DSL) Custom vocabulary layered over the rule syntax so domain experts write in plain English:

When the client has a diagnosis of depression
And the PHQ-9 score is greater than 15
Then flag the client for intensive pathway review

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 ≤ 15

Without 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=True

Where 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.

Back to top