Volume 3: Human-System Collaboration

Pattern 23: API-Driven Business Rules

Part III: Integration Patterns


Opening Scenario: The Loan Approval Rules That Changed Every Week

David was a loan officer at a regional bank. Every Monday morning, he received an email with updated loan approval rules:

Week of March 4, 2024 - Updated Loan Rules:

• Minimum credit score: 680 (changed from 670)
• Maximum debt-to-income ratio: 43% (unchanged)
• Minimum down payment: 
  - 10% for credit scores 750+
  - 15% for credit scores 680-749
  - 20% for credit scores below 680
• Income verification: Required for all loans over $200,000
• Employment history: Minimum 2 years (changed from 18 months)

Please update your spreadsheet accordingly.

David opened his loan calculator spreadsheet and updated the formulas. He'd been doing this every Monday for two years.

On Tuesday, a customer applied for a $250,000 loan. David pulled out his spreadsheet:

Credit Score: 720
Income: $85,000/year
Debt: $28,000/year
Down Payment: 12%

Debt-to-Income: 32.9% ✓
Credit Score: 720 ✓ (above 680)
Down Payment: 12% ✓ (above 10% for 720 score)
Employment: 3 years ✓

APPROVED

David called the customer: "Good news! You're approved."

On Wednesday, the bank's CEO discovered a compliance violation. The rule change email was WRONG. The minimum down payment for credit scores 680-749 should have been 20%, not 15%.

David's spreadsheet had the wrong rule. He'd approved loans that should have been denied.

The bank had to contact 47 customers and revoke pre-approvals. The compliance fine was $180,000.


Meanwhile, across town at a competing bank, loan officer Sarah was processing the same type of application:

Loan Application System

Customer: John Smith
Credit Score: 720
Income: $85,000
Debt: $28,000
Down Payment: 12% ($30,000)

[Check Eligibility]

Sarah clicked "Check Eligibility." The system made an API call:

POST /api/loan-rules/check-eligibility

{
  "creditScore": 720,
  "annualIncome": 85000,
  "annualDebt": 28000,
  "loanAmount": 250000,
  "downPayment": 30000,
  "employmentYears": 3
}

Response:

{
  "eligible": false,
  "reasons": [
    {
      "rule": "minimum-down-payment",
      "required": 50000,
      "provided": 30000,
      "message": "Down payment must be at least $50,000 (20%) for credit scores 680-749",
      "passed": false
    }
  ],
  "requiredChanges": [
    {
      "option": "increase-down-payment",
      "description": "Increase down payment to $50,000",
      "wouldQualify": true
    },
    {
      "option": "reduce-loan-amount",
      "description": "Reduce loan amount to $150,000",
      "wouldQualify": true
    }
  ]
}

Sarah saw the result:

❌ NOT ELIGIBLE

Down payment must be at least $50,000 (20%)
Current down payment: $30,000

Options to qualify:
• Increase down payment to $50,000, OR
• Reduce loan amount to $150,000

Sarah called the customer: "With a 12% down payment, you don't qualify. But if you can increase to 20% ($50,000), you're approved."

Same rules. Different implementation.

David had spreadsheets. Rules changed, spreadsheet got wrong update, customers got wrong answers.

Sarah had API-driven rules. Rules changed on server, all applications instantly used correct rules, customers got right answers.


The bank's technology director, Marcus, saw the $180,000 fine and said: "We need API-driven business rules."

He built a centralized rules engine:

class BusinessRulesEngine {
  constructor() {
    this.rules = new Map();
    this.ruleVersions = new Map();
  }

  defineRule(ruleName, ruleDefinition) {
    // Store rule with version
    const version = this.getNextVersion(ruleName);

    this.rules.set(ruleName, {
      name: ruleName,
      version: version,
      definition: ruleDefinition,
      effectiveDate: new Date(),
      createdBy: ruleDefinition.author,
      documentation: ruleDefinition.documentation
    });

    // Track version history
    if (!this.ruleVersions.has(ruleName)) {
      this.ruleVersions.set(ruleName, []);
    }
    this.ruleVersions.get(ruleName).push({
      version: version,
      effectiveDate: new Date(),
      definition: ruleDefinition
    });
  }

  async evaluateRule(ruleName, context) {
    const rule = this.rules.get(ruleName);

    if (!rule) {
      throw new Error(`Rule '${ruleName}' not found`);
    }

    return await rule.definition.evaluate(context);
  }

  async evaluateRuleSet(ruleSetName, context) {
    const ruleSet = this.ruleSets.get(ruleSetName);

    const results = [];

    for (const ruleName of ruleSet.rules) {
      const result = await this.evaluateRule(ruleName, context);
      results.push({
        rule: ruleName,
        passed: result.passed,
        message: result.message,
        details: result.details
      });
    }

    return {
      passed: results.every(r => r.passed),
      results: results
    };
  }
}

Now the loan rules lived in ONE place:

// Define loan eligibility rules
rulesEngine.defineRule('minimum-credit-score', {
  evaluate: (context) => {
    const minimumScore = 680;
    const passed = context.creditScore >= minimumScore;

    return {
      passed: passed,
      message: passed 
        ? `Credit score ${context.creditScore} meets minimum`
        : `Credit score ${context.creditScore} is below minimum ${minimumScore}`,
      required: minimumScore,
      actual: context.creditScore
    };
  },
  documentation: "Minimum credit score required for loan approval"
});

rulesEngine.defineRule('down-payment-percentage', {
  evaluate: (context) => {
    // Rule changed - this is the ONLY place to update
    let requiredPercentage;

    if (context.creditScore >= 750) {
      requiredPercentage = 0.10; // 10%
    } else if (context.creditScore >= 680) {
      requiredPercentage = 0.20; // 20% (CORRECTED from 15%)
    } else {
      requiredPercentage = 0.20; // 20%
    }

    const requiredAmount = context.loanAmount * requiredPercentage;
    const actualPercentage = context.downPayment / context.loanAmount;
    const passed = context.downPayment >= requiredAmount;

    return {
      passed: passed,
      message: passed
        ? `Down payment of ${Math.round(actualPercentage * 100)}% meets requirement`
        : `Down payment must be at least ${Math.round(requiredPercentage * 100)}% ($${requiredAmount})`,
      required: requiredAmount,
      actual: context.downPayment,
      requiredPercentage: requiredPercentage
    };
  }
});

All loan officers used the same API:

POST /api/loan-rules/evaluate

Response:
{
  "eligible": false,
  "rules": [
    {
      "name": "minimum-credit-score",
      "passed": true
    },
    {
      "name": "down-payment-percentage",
      "passed": false,
      "required": 50000,
      "actual": 30000
    }
  ]
}

When rules changed, they changed once, on the server. Every application, every loan officer, every system got the correct rules instantly.

No spreadsheets. No email updates. No compliance violations.

Centralized rules. Distributed access. Universal consistency.

If this works for a bank on Earth, it could work for banks on Mars. Interplanetary apps possible.

Context

API-Driven Business Rules applies when:

Business rules change frequently: Loan criteria, pricing rules, eligibility requirements

Multiple systems need same rules: Web app, mobile app, third-party integrations

Consistency is critical: Everyone must use identical rules

Rules are complex: Multi-step logic, numerous conditions, interconnected factors

Compliance required: Regulatory requirements demand rule enforcement

Centralized control needed: Business teams must update rules without developer help

Auditability essential: Track who changed what rule when

Distributed systems: Rules needed across many applications, platforms, locations (or planets!)

Problem Statement

Most systems hard-code business rules in application logic, creating chaos:

Rules duplicated everywhere:

// Bad: Same rule in 5 different places

// Web app
if (user.creditScore >= 680 && downPayment >= loanAmount * 0.15) {
  return "approved";
}

// Mobile app
if (creditScore >= 680 && downPaymentPct >= 15) {
  return "approved";
}

// Partner API
if (application.credit >= 680 && application.dp >= 0.15 * application.loan) {
  return true;
}

// When rule changes, must update ALL of these
// Miss one = inconsistency

No version control:

// Bad: No history of rule changes
const MINIMUM_CREDIT_SCORE = 680;

// Who changed this from 670 to 680?
// When?
// Why?
// What was the old value?
// Unknown!

Business teams can't update:

// Bad: Rules buried in code
function checkEligibility(application) {
  if (application.creditScore < 680) return false;
  if (application.dti > 0.43) return false;
  if (application.employmentYears < 2) return false;
  return true;
}

// Business wants to change rule?
// Needs developer
// Needs code change
// Needs deployment
// Takes days/weeks

No explanation of decisions:

// Bad: Just returns true/false
function isEligible(data) {
  return data.score >= 680 && data.income > 50000;
}

// Why was user rejected?
// Which rule failed?
// What needs to change?
// No information!

Testing nightmare:

// Bad: Can't test rules independently
// To test credit score rule, must create entire application
// To test down payment rule, must create entire application
// Can't isolate rule logic

No audit trail:

// Bad: No record of rule evaluations
const approved = checkRules(application);

// Can't prove which rules were checked
// Can't show why decision was made
// Compliance audit fails

We need centralized, versioned, documented business rules accessible via API that any system can use.

Forces

Centralization vs Performance

  • Central rules ensure consistency
  • But API calls add latency
  • Balance accuracy with speed

Flexibility vs Simplicity

  • Complex rule engines very powerful
  • But simple if/then easier to understand
  • Balance capability with usability

Business Control vs Safety

  • Business wants to change rules directly
  • But bad rules can break systems
  • Balance autonomy with guardrails

Documentation vs Code

  • Code is precise, unambiguous
  • But business prefers plain language
  • Balance technical accuracy with readability

Versioning vs Complexity

  • Version history enables rollback
  • But many versions create clutter
  • Balance auditability with manageability

Solution

Implement centralized business rules engine accessible via API that stores rules in database, evaluates them consistently, provides detailed explanations of decisions, tracks version history, enables business team updates through UI, and serves all applications from single source of truth.

The pattern has seven key strategies:

1. Centralized Rules Engine

Create server-side rule evaluation system:

class CentralizedRulesEngine {
  constructor(database) {
    this.db = database;
    this.ruleCache = new Map();
    this.evaluationLog = [];
  }

  async defineRule(ruleDefinition) {
    // Validate rule definition
    this.validateRuleDefinition(ruleDefinition);

    // Generate new version
    const existingVersions = await this.db.query(
      'SELECT MAX(version) as max FROM rules WHERE name = ?',
      [ruleDefinition.name]
    );

    const version = (existingVersions[0]?.max || 0) + 1;

    // Store rule in database
    const ruleId = await this.db.insert('rules', {
      name: ruleDefinition.name,
      version: version,
      logic: JSON.stringify(ruleDefinition.logic),
      description: ruleDefinition.description,
      category: ruleDefinition.category,
      effectiveDate: ruleDefinition.effectiveDate || new Date(),
      expiryDate: ruleDefinition.expiryDate || null,
      createdBy: ruleDefinition.createdBy,
      createdAt: new Date(),
      active: true
    });

    // Clear cache for this rule
    this.ruleCache.delete(ruleDefinition.name);

    return {
      id: ruleId,
      name: ruleDefinition.name,
      version: version
    };
  }

  async evaluateRule(ruleName, context, options = {}) {
    // Get rule (from cache or database)
    const rule = await this.getRule(ruleName, options.version);

    if (!rule) {
      throw new Error(`Rule '${ruleName}' not found`);
    }

    // Evaluate rule
    const startTime = Date.now();
    const result = await this.executeRule(rule, context);
    const duration = Date.now() - startTime;

    // Log evaluation
    if (options.logEvaluation !== false) {
      this.logEvaluation({
        ruleId: rule.id,
        ruleName: rule.name,
        ruleVersion: rule.version,
        context: context,
        result: result,
        duration: duration,
        timestamp: new Date()
      });
    }

    return result;
  }

  async getRule(ruleName, version = null) {
    const cacheKey = version ? `${ruleName}:${version}` : ruleName;

    // Check cache
    if (this.ruleCache.has(cacheKey)) {
      return this.ruleCache.get(cacheKey);
    }

    // Query database
    let query, params;

    if (version) {
      query = 'SELECT * FROM rules WHERE name = ? AND version = ? AND active = true';
      params = [ruleName, version];
    } else {
      query = `
        SELECT * FROM rules 
        WHERE name = ? AND active = true 
        AND effectiveDate <= NOW()
        AND (expiryDate IS NULL OR expiryDate > NOW())
        ORDER BY version DESC 
        LIMIT 1
      `;
      params = [ruleName];
    }

    const rows = await this.db.query(query, params);

    if (rows.length === 0) {
      return null;
    }

    const rule = {
      id: rows[0].id,
      name: rows[0].name,
      version: rows[0].version,
      logic: JSON.parse(rows[0].logic),
      description: rows[0].description,
      category: rows[0].category
    };

    // Cache it
    this.ruleCache.set(cacheKey, rule);

    return rule;
  }

  async executeRule(rule, context) {
    const logic = rule.logic;

    // Execute based on logic type
    if (logic.type === 'expression') {
      return this.evaluateExpression(logic.expression, context);
    } else if (logic.type === 'decision-table') {
      return this.evaluateDecisionTable(logic.table, context);
    } else if (logic.type === 'script') {
      return this.evaluateScript(logic.script, context);
    } else {
      throw new Error(`Unknown logic type: ${logic.type}`);
    }
  }

  evaluateExpression(expression, context) {
    // Simple expression evaluator
    // Example: "creditScore >= 680 AND dti <= 0.43"

    try {
      // Create safe evaluation context
      const safeContext = this.createSafeContext(context);

      // Evaluate expression
      const result = this.safeEval(expression, safeContext);

      return {
        passed: Boolean(result),
        expression: expression,
        context: context
      };

    } catch (error) {
      return {
        passed: false,
        error: error.message
      };
    }
  }

  evaluateDecisionTable(table, context) {
    // Evaluate decision table
    for (const row of table.rows) {
      let matches = true;

      for (const condition of row.conditions) {
        if (!this.evaluateCondition(condition, context)) {
          matches = false;
          break;
        }
      }

      if (matches) {
        return {
          passed: row.outcome.passed,
          message: row.outcome.message,
          matchedRow: row.id
        };
      }
    }

    // No rows matched - use default
    return table.default || {
      passed: false,
      message: 'No matching rule found'
    };
  }

  logEvaluation(evaluation) {
    this.evaluationLog.push(evaluation);

    // Persist to database asynchronously
    setImmediate(async () => {
      await this.db.insert('rule_evaluations', {
        ruleId: evaluation.ruleId,
        context: JSON.stringify(evaluation.context),
        result: JSON.stringify(evaluation.result),
        duration: evaluation.duration,
        timestamp: evaluation.timestamp
      });
    });
  }
}

2. Rule Definition Language

Define rules in declarative format:

class RuleDefinitionLanguage {
  static createSimpleRule(name, condition, message) {
    return {
      name: name,
      logic: {
        type: 'expression',
        expression: condition
      },
      description: message,
      category: 'simple'
    };
  }

  static createDecisionTable(name, table) {
    return {
      name: name,
      logic: {
        type: 'decision-table',
        table: {
          columns: table.columns,
          rows: table.rows,
          default: table.default
        }
      },
      description: table.description,
      category: 'decision-table'
    };
  }

  static createCompositeRule(name, rules, operator = 'AND') {
    return {
      name: name,
      logic: {
        type: 'composite',
        operator: operator, // AND, OR
        rules: rules
      },
      description: `Composite rule combining ${rules.length} rules`,
      category: 'composite'
    };
  }
}

// Example: Loan eligibility rules

// Simple rule
const creditScoreRule = RuleDefinitionLanguage.createSimpleRule(
  'minimum-credit-score',
  'creditScore >= 680',
  'Minimum credit score is 680'
);

// Decision table for down payment
const downPaymentRule = RuleDefinitionLanguage.createDecisionTable(
  'down-payment-requirement',
  {
    description: 'Down payment based on credit score',
    columns: ['creditScore', 'requiredPercentage'],
    rows: [
      {
        id: 1,
        conditions: [
          { field: 'creditScore', operator: '>=', value: 750 }
        ],
        outcome: {
          passed: true,
          requiredPercentage: 0.10,
          message: '10% down payment required for excellent credit'
        }
      },
      {
        id: 2,
        conditions: [
          { field: 'creditScore', operator: '>=', value: 680 },
          { field: 'creditScore', operator: '<', value: 750 }
        ],
        outcome: {
          passed: true,
          requiredPercentage: 0.20,
          message: '20% down payment required for good credit'
        }
      },
      {
        id: 3,
        conditions: [
          { field: 'creditScore', operator: '<', value: 680 }
        ],
        outcome: {
          passed: false,
          message: 'Credit score below minimum'
        }
      }
    ],
    default: {
      passed: false,
      message: 'Unable to determine down payment requirement'
    }
  }
);

// Composite rule
const loanEligibilityRule = RuleDefinitionLanguage.createCompositeRule(
  'loan-eligibility',
  [
    'minimum-credit-score',
    'down-payment-requirement',
    'debt-to-income-ratio',
    'employment-history'
  ],
  'AND'
);

3. RESTful Rules API

Expose rules via REST API:

class RulesAPI {
  constructor(rulesEngine) {
    this.engine = rulesEngine;
  }

  setupRoutes(app) {
    // Evaluate single rule
    app.post('/api/rules/:ruleName/evaluate', async (req, res) => {
      try {
        const result = await this.engine.evaluateRule(
          req.params.ruleName,
          req.body.context,
          {
            version: req.query.version,
            logEvaluation: req.query.log !== 'false'
          }
        );

        res.json({
          success: true,
          result: result
        });

      } catch (error) {
        res.status(400).json({
          success: false,
          error: error.message
        });
      }
    });

    // Evaluate rule set
    app.post('/api/rule-sets/:setName/evaluate', async (req, res) => {
      try {
        const result = await this.engine.evaluateRuleSet(
          req.params.setName,
          req.body.context
        );

        res.json({
          success: true,
          passed: result.passed,
          results: result.results
        });

      } catch (error) {
        res.status(400).json({
          success: false,
          error: error.message
        });
      }
    });

    // Get rule definition
    app.get('/api/rules/:ruleName', async (req, res) => {
      const rule = await this.engine.getRule(
        req.params.ruleName,
        req.query.version
      );

      if (!rule) {
        res.status(404).json({
          success: false,
          error: 'Rule not found'
        });
        return;
      }

      res.json({
        success: true,
        rule: rule
      });
    });

    // Create/update rule
    app.post('/api/rules', async (req, res) => {
      try {
        const result = await this.engine.defineRule(req.body);

        res.json({
          success: true,
          rule: result
        });

      } catch (error) {
        res.status(400).json({
          success: false,
          error: error.message
        });
      }
    });

    // Get rule versions
    app.get('/api/rules/:ruleName/versions', async (req, res) => {
      const versions = await this.engine.getRuleVersions(req.params.ruleName);

      res.json({
        success: true,
        versions: versions
      });
    });

    // Get evaluation history
    app.get('/api/rules/:ruleName/evaluations', async (req, res) => {
      const history = await this.engine.getEvaluationHistory(
        req.params.ruleName,
        {
          limit: req.query.limit || 100,
          offset: req.query.offset || 0
        }
      );

      res.json({
        success: true,
        evaluations: history
      });
    });
  }
}

// Usage from client
async function checkLoanEligibility(application) {
  const response = await fetch('/api/rules/loan-eligibility/evaluate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      context: {
        creditScore: application.creditScore,
        loanAmount: application.loanAmount,
        downPayment: application.downPayment,
        annualIncome: application.annualIncome,
        annualDebt: application.annualDebt,
        employmentYears: application.employmentYears
      }
    })
  });

  const result = await response.json();
  return result;
}

4. Business Rules UI

Enable business users to manage rules:

class BusinessRulesUI {
  renderRuleEditor(rule) {
    return `
      <div class="rule-editor">
        <h2>Edit Rule: ${rule.name}</h2>

        <form id="rule-form">
          <div class="form-group">
            <label>Rule Name</label>
            <input type="text" name="name" value="${rule.name}" readonly>
          </div>

          <div class="form-group">
            <label>Description</label>
            <textarea name="description">${rule.description}</textarea>
          </div>

          <div class="form-group">
            <label>Category</label>
            <select name="category">
              <option value="eligibility">Eligibility</option>
              <option value="pricing">Pricing</option>
              <option value="validation">Validation</option>
            </select>
          </div>

          <div class="form-group">
            <label>Logic Type</label>
            <select name="logicType" onchange="showLogicEditor(this.value)">
              <option value="expression">Expression</option>
              <option value="decision-table">Decision Table</option>
              <option value="composite">Composite</option>
            </select>
          </div>

          <div id="logic-editor">
            ${this.renderLogicEditor(rule.logic)}
          </div>

          <div class="form-group">
            <label>Effective Date</label>
            <input type="date" name="effectiveDate">
          </div>

          <div class="form-actions">
            <button type="button" onclick="testRule()">Test Rule</button>
            <button type="submit">Save Rule</button>
          </div>
        </form>

        <div id="test-results"></div>
      </div>
    `;
  }

  renderLogicEditor(logic) {
    if (logic.type === 'expression') {
      return this.renderExpressionEditor(logic);
    } else if (logic.type === 'decision-table') {
      return this.renderDecisionTableEditor(logic);
    }
  }

  renderExpressionEditor(logic) {
    return `
      <div class="expression-editor">
        <label>Expression</label>
        <textarea name="expression" class="code-editor">${logic.expression}</textarea>

        <div class="expression-help">
          <h4>Available Fields:</h4>
          <ul>
            <li>creditScore - Applicant's credit score</li>
            <li>loanAmount - Requested loan amount</li>
            <li>downPayment - Down payment amount</li>
            <li>annualIncome - Annual income</li>
            <li>annualDebt - Annual debt obligations</li>
          </ul>

          <h4>Operators:</h4>
          <ul>
            <li>>=, <=, >, <, ==, != - Comparison</li>
            <li>AND, OR, NOT - Logical</li>
          </ul>

          <h4>Example:</h4>
          <code>creditScore >= 680 AND (downPayment / loanAmount) >= 0.20</code>
        </div>
      </div>
    `;
  }

  renderDecisionTableEditor(logic) {
    return `
      <div class="decision-table-editor">
        <table class="decision-table">
          <thead>
            <tr>
              ${logic.table.columns.map(col => `<th>${col}</th>`).join('')}
              <th>Outcome</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            ${logic.table.rows.map(row => this.renderTableRow(row)).join('')}
          </tbody>
        </table>

        <button type="button" onclick="addRow()">Add Row</button>
      </div>
    `;
  }

  async testRule(ruleName, testContext) {
    const result = await fetch(`/api/rules/${ruleName}/evaluate`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ context: testContext })
    });

    const data = await result.json();

    document.getElementById('test-results').innerHTML = `
      <div class="test-results ${data.result.passed ? 'passed' : 'failed'}">
        <h3>Test Results</h3>
        <p><strong>Result:</strong> ${data.result.passed ? 'PASSED' : 'FAILED'}</p>
        ${data.result.message ? `<p><strong>Message:</strong> ${data.result.message}</p>` : ''}
        <pre>${JSON.stringify(data.result, null, 2)}</pre>
      </div>
    `;
  }
}

5. Rule Versioning

Track rule changes over time:

class RuleVersioning {
  async getRuleHistory(ruleName) {
    const versions = await db.query(`
      SELECT 
        version,
        logic,
        description,
        effectiveDate,
        expiryDate,
        createdBy,
        createdAt
      FROM rules
      WHERE name = ?
      ORDER BY version DESC
    `, [ruleName]);

    return versions.map(v => ({
      version: v.version,
      logic: JSON.parse(v.logic),
      description: v.description,
      effectiveDate: v.effectiveDate,
      createdBy: v.createdBy,
      createdAt: v.createdAt
    }));
  }

  async compareVersions(ruleName, version1, version2) {
    const v1 = await this.getRuleVersion(ruleName, version1);
    const v2 = await this.getRuleVersion(ruleName, version2);

    return {
      ruleName: ruleName,
      version1: {
        number: version1,
        logic: v1.logic,
        effectiveDate: v1.effectiveDate
      },
      version2: {
        number: version2,
        logic: v2.logic,
        effectiveDate: v2.effectiveDate
      },
      differences: this.findDifferences(v1.logic, v2.logic)
    };
  }

  async rollbackRule(ruleName, toVersion) {
    const previousVersion = await this.getRuleVersion(ruleName, toVersion);

    // Create new version with old logic
    return await rulesEngine.defineRule({
      name: ruleName,
      logic: previousVersion.logic,
      description: `Rolled back to version ${toVersion}`,
      category: previousVersion.category,
      createdBy: 'system-rollback'
    });
  }
}

6. Rule Testing Framework

Test rules before deployment:

class RuleTestFramework {
  constructor(rulesEngine) {
    this.engine = rulesEngine;
    this.testSuites = new Map();
  }

  defineTestSuite(ruleName, tests) {
    this.testSuites.set(ruleName, tests);
  }

  async runTests(ruleName) {
    const tests = this.testSuites.get(ruleName);

    if (!tests) {
      throw new Error(`No test suite for rule: ${ruleName}`);
    }

    const results = [];

    for (const test of tests) {
      const result = await this.runTest(ruleName, test);
      results.push(result);
    }

    return {
      ruleName: ruleName,
      totalTests: results.length,
      passed: results.filter(r => r.passed).length,
      failed: results.filter(r => !r.passed).length,
      results: results
    };
  }

  async runTest(ruleName, test) {
    const actual = await this.engine.evaluateRule(ruleName, test.context);

    const passed = actual.passed === test.expected.passed;

    return {
      name: test.name,
      passed: passed,
      context: test.context,
      expected: test.expected,
      actual: actual
    };
  }
}

// Define tests for loan rules
testFramework.defineTestSuite('loan-eligibility', [
  {
    name: 'Excellent credit with 10% down',
    context: {
      creditScore: 780,
      loanAmount: 300000,
      downPayment: 30000,
      annualIncome: 100000,
      annualDebt: 20000,
      employmentYears: 5
    },
    expected: {
      passed: true
    }
  },
  {
    name: 'Good credit with insufficient down payment',
    context: {
      creditScore: 720,
      loanAmount: 250000,
      downPayment: 30000, // Only 12%, needs 20%
      annualIncome: 85000,
      annualDebt: 28000,
      employmentYears: 3
    },
    expected: {
      passed: false
    }
  },
  {
    name: 'Poor credit',
    context: {
      creditScore: 650,
      loanAmount: 200000,
      downPayment: 40000,
      annualIncome: 75000,
      annualDebt: 15000,
      employmentYears: 4
    },
    expected: {
      passed: false
    }
  }
]);

// Run tests
const results = await testFramework.runTests('loan-eligibility');
console.log(`Tests: ${results.passed}/${results.totalTests} passed`);

7. Rule Explanation & Transparency

Explain why rules passed or failed:

class RuleExplainer {
  async explainDecision(ruleName, context) {
    const result = await rulesEngine.evaluateRule(ruleName, context, {
      explain: true
    });

    return {
      decision: result.passed ? 'APPROVED' : 'DENIED',
      summary: this.generateSummary(result),
      details: this.generateDetails(result),
      suggestions: result.passed ? null : this.generateSuggestions(result)
    };
  }

  generateSummary(result) {
    if (result.passed) {
      return 'All eligibility requirements have been met.';
    }

    const failedRules = result.results.filter(r => !r.passed);

    return `${failedRules.length} requirement(s) not met: ${
      failedRules.map(r => r.rule).join(', ')
    }`;
  }

  generateDetails(result) {
    return result.results.map(r => ({
      rule: r.rule,
      status: r.passed ? 'PASSED' : 'FAILED',
      message: r.message,
      details: r.details
    }));
  }

  generateSuggestions(result) {
    const suggestions = [];

    result.results.forEach(r => {
      if (!r.passed && r.suggestion) {
        suggestions.push({
          rule: r.rule,
          suggestion: r.suggestion
        });
      }
    });

    return suggestions;
  }
}

// Example explanation
const explanation = await explainer.explainDecision('loan-eligibility', {
  creditScore: 720,
  loanAmount: 250000,
  downPayment: 30000,
  annualIncome: 85000,
  annualDebt: 28000,
  employmentYears: 3
});

/*
{
  decision: 'DENIED',
  summary: '1 requirement not met: down-payment-requirement',
  details: [
    {
      rule: 'minimum-credit-score',
      status: 'PASSED',
      message: 'Credit score 720 meets minimum 680'
    },
    {
      rule: 'down-payment-requirement',
      status: 'FAILED',
      message: 'Down payment must be at least 20% ($50,000)',
      details: {
        required: 50000,
        provided: 30000,
        shortfall: 20000
      }
    },
    {
      rule: 'debt-to-income-ratio',
      status: 'PASSED',
      message: 'DTI 32.9% is below maximum 43%'
    },
    {
      rule: 'employment-history',
      status: 'PASSED',
      message: '3 years meets minimum 2 years'
    }
  ],
  suggestions: [
    {
      rule: 'down-payment-requirement',
      suggestion: 'Increase down payment by $20,000 to $50,000'
    }
  ]
}
*/

Implementation Details

Complete API-Driven Rules System

class CompleteBusinessRulesSystem {
  constructor(database) {
    this.engine = new CentralizedRulesEngine(database);
    this.api = new RulesAPI(this.engine);
    this.ui = new BusinessRulesUI();
    this.versioning = new RuleVersioning();
    this.testing = new RuleTestFramework(this.engine);
    this.explainer = new RuleExplainer();
  }

  async initialize(app) {
    // Setup API routes
    this.api.setupRoutes(app);

    // Load rules from database
    await this.engine.loadRules();

    // Start rule refresh timer
    setInterval(() => {
      this.engine.refreshCache();
    }, 60000); // Refresh every minute
  }

  async evaluateLoanEligibility(application) {
    // Evaluate all loan rules
    const result = await this.engine.evaluateRuleSet('loan-eligibility', {
      creditScore: application.creditScore,
      loanAmount: application.loanAmount,
      downPayment: application.downPayment,
      annualIncome: application.annualIncome,
      annualDebt: application.annualDebt,
      employmentYears: application.employmentYears
    });

    // Generate explanation
    const explanation = await this.explainer.explainDecision(
      'loan-eligibility',
      application
    );

    return {
      eligible: result.passed,
      explanation: explanation
    };
  }
}

// Usage
const rulesSystem = new CompleteBusinessRulesSystem(database);
await rulesSystem.initialize(app);

// From any application
const result = await fetch('/api/rules/loan-eligibility/evaluate', {
  method: 'POST',
  body: JSON.stringify({
    context: loanApplication
  })
});

// Business team updates rule (no code deployment needed)
await fetch('/api/rules', {
  method: 'POST',
  body: JSON.stringify({
    name: 'minimum-credit-score',
    logic: {
      type: 'expression',
      expression: 'creditScore >= 700' // Changed from 680
    },
    description: 'Updated minimum credit score to 700',
    effectiveDate: '2024-04-01'
  })
});

// All applications immediately use new rule

Consequences

Benefits

Single Source of Truth: - Rules defined once - Used everywhere - No duplication

Consistency Guaranteed: - All systems use same rules - No divergence - Perfect alignment

Business Agility: - Business updates rules directly - No code changes needed - Deploy instantly

Version Control: - Complete history - Rollback capability - Audit trail

Testability: - Test rules independently - Regression testing - Quality assurance

Transparency: - Explainable decisions - Clear reasoning - Compliance friendly

Scalability: - Works for 1 app or 1000 - Works on Earth or Mars - Interplanetary possible!

Liabilities

Single Point of Failure: - Rules API down = all apps affected - Need high availability - Require fallbacks

Performance: - API calls add latency - Need caching - Monitor carefully

Complexity: - Rules engine is sophisticated - Learning curve exists - Need good documentation

Governance: - Who can change rules? - How to prevent bad rules? - Need approval workflows

Migration Effort: - Moving from hardcoded rules - Requires refactoring - Significant initial work

Domain Examples

Banking: Loan Rules

// Define loan eligibility
await rulesEngine.defineRule({
  name: 'loan-eligibility',
  logic: { type: 'composite', rules: [...] }
});

// Evaluate from any channel
const result = await api.evaluate('loan-eligibility', application);

Insurance: Underwriting

// Premium calculation rules
await rulesEngine.defineRule({
  name: 'auto-insurance-premium',
  logic: { type: 'decision-table', table: {...} }
});

E-commerce: Pricing

// Dynamic pricing rules
await rulesEngine.defineRule({
  name: 'product-discount',
  logic: { type: 'expression', expression: 'tier == "gold" ? 0.20 : 0.10' }
});

Healthcare: Eligibility

// Coverage eligibility
await rulesEngine.defineRule({
  name: 'procedure-covered',
  logic: { type: 'decision-table', table: {...} }
});

Prerequisites: - Volume 3, Pattern 21: External Data Integration (provides data for rules) - Volume 3, Pattern 22: Real-Time Lookup (validates against rules)

Synergies: - Volume 3, Pattern 6: Domain-Aware Validation (rules validate domains) - Volume 3, Pattern 18: Audit Trail (log rule evaluations) - All patterns (rules can govern all behavior)

Conflicts: - Offline applications - Real-time gaming (latency sensitive) - Embedded systems (no network)

Alternatives: - Hardcoded rules (simpler but inflexible) - Configuration files (middle ground) - Smart contracts (blockchain-based rules)

Known Uses

Drools: Business rules management system

AWS IoT Rules Engine: Cloud-based rules

Stripe Radar: Fraud detection rules

Insurance Platforms: Underwriting rules

Salesforce Process Builder: Business process rules

Banking Systems: Credit decisioning

Tax Software: Tax calculation rules


Further Reading

Academic Foundations

  • Rule-Based Systems: Brownston, L., et al. (1985). Programming Expert Systems in OPS5. Addison-Wesley. ISBN: 978-0201106473
  • Business Rules: Ross, R.G. (2013). Defining Business Rules ~ What Are They Really? Business Rule Solutions. https://www.brsolutions.com/
  • Rete Algorithm: Forgy, C.L. (1982). "Rete: A Fast Algorithm for the Many Pattern/Many Object Pattern Match Problem." Artificial Intelligence 19(1): 17-37.

Practical Implementation

Standards & Specifications

  • Pattern 11: Validation Rules - Rules as validation
  • Pattern 14: Contextual Constraints - Context-driven rules
  • Pattern 26: External Data Integration - External rule services
  • Pattern 29: Webhooks and Events - Event-triggered rules
  • Volume 2, Pattern 7: Multi-Dimensional Risk Assessment - Intelligence from rules
  • Volume 1, Chapter 7: Cross-Domain Analysis - Rules architecture

Tools & Services

Implementation Examples