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: {...} }
});
Related Patterns
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
- json-rules-engine: https://github.com/CacheControl/json-rules-engine - JavaScript rules engine
- Drools: https://www.drools.org/ - Business rules management system (Java)
- Easy Rules: https://github.com/j-easy/easy-rules - Lightweight Java rules engine
- node-rules: https://github.com/mithunsatheesh/node-rules - Rule engine for Node.js
- RulesEngine: https://github.com/microsoft/RulesEngine - .NET rules engine by Microsoft
Standards & Specifications
- DMN (Decision Model and Notation): https://www.omg.org/dmn/ - Decision modeling standard
- FEEL (Friendly Enough Expression Language): https://www.omg.org/spec/DMN/1.3/ - DMN expression language
- Business Rules Manifesto: http://www.businessrulesgroup.org/brmanifesto.htm - Foundational principles
Related Trilogy Patterns
- 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
- Camunda Decision Engine: https://camunda.com/products/camunda-platform/dmn-engine/ - DMN execution
- Red Hat Decision Manager: https://www.redhat.com/en/technologies/jboss-middleware/decision-manager - Enterprise BRMS
- AWS Lambda: https://aws.amazon.com/lambda/ - Serverless rule execution
- Azure Logic Apps: https://azure.microsoft.com/en-us/services/logic-apps/ - Workflow and rules
- Google Cloud Functions: https://cloud.google.com/functions - Event-driven rules
Implementation Examples
- Building a Rules Engine: https://martinfowler.com/bliki/RulesEngine.html - Martin Fowler's perspective
- DMN Tutorial: https://camunda.com/dmn/ - Learn decision modeling
- Rules vs Code: https://www.infoq.com/articles/business-rules-engines/ - When to use rules engines