Overview
A rule engine evaluates business rules (conditions and actions) against facts, separating frequently changing policy from application code. Examples: discounts (if cart > 100 and member, 10% off), fraud checks (if amount > 5,000 and new device, flag), eligibility, routing, and pricing. Rules have a condition, an action or outcome, and often a priority.
The LLD centers on a Rule interface (matches(facts), apply(facts)), composable conditions (AND, OR, NOT via the Composite or Specification pattern), a RuleEngine that evaluates rules in priority order with a strategy (first match, all matches), and optionally loading rules from configuration so non-engineers can change them.
The bouncer checks a list: 'members enter free', 'guests over 21 pay 10', 'no entry after 2 AM'. Management can change the card without retraining the bouncer.
When to use it
- Business rules change often and independently of code.
- Many combinable conditions (discounts, eligibility, fraud).
- Interview prompt: 'Design a discount or rule engine'.
Where it shows up in interviews
Recognize it when: discounts, fraud, eligibility, routing rules.
- Design a discount engine
- Design a fraud detection rule system
- Design an insurance eligibility checker
Where it is used in real software
A Java rule engine (Rete algorithm) used in banking and insurance.
Policy engines evaluate allow/deny rules against requests.
Platforms like Shopify and Magento model promotions as configurable rules.
Key terms
- Fact
- Input data the rules evaluate (cart, user, transaction).
- Condition / predicate
- Boolean test over facts.
- Action / outcome
- What happens when a rule matches.
- Specification pattern
- Composable predicates with and/or/not.
- Conflict resolution
- Priority, first match, or combine all.
How it works, step by step
- 1Define facts
A typed context object.
- 2Define conditions
Small predicates, composable.
- 3Define rules
Condition + outcome + priority.
- 4Choose evaluation strategy
First match vs all matches; stackability rules.
- 5Externalize configuration
Load rules from JSON or a DSL; validate on load.
Discount rules
Cart subtotal 150, member = true, coupon = none, strategy: best single discount
| Rule | Condition | Matches? | Discount |
|---|---|---|---|
| Member 10% | member | Yes | 15 |
| Big cart | subtotal >= 100 | Yes | 10 flat |
| Coupon SAVE20 | coupon == SAVE20 | No | - |
| Result (best single) | - | - | 15 |
NOWRule: Member 10% | Condition: member | Matches?: Yes | Discount: 15
Changing the strategy to 'stack all' would give 25; the rules themselves stay the same.
Implementation
type Facts = { subtotal: number; member: boolean; coupon?: string; itemCount: number }; // Specification pattern: composable conditionstype Condition = (f: Facts) => boolean;const and = (...cs: Condition[]): Condition => (f) => cs.every((c) => c(f));const or = (...cs: Condition[]): Condition => (f) => cs.some((c) => c(f));const not = (c: Condition): Condition => (f) => !c(f); type Rule = { name: string; priority: number; when: Condition; discount: (f: Facts) => number }; interface Strategy { combine(matches: { rule: Rule; amount: number }[]): number }const bestSingle: Strategy = { combine: (m) => Math.max(0, ...m.map((x) => x.amount)) };const stackAll: Strategy = { combine: (m) => m.reduce((s, x) => s + x.amount, 0) }; class RuleEngine { constructor(private rules: Rule[], private strategy: Strategy) {} evaluate(facts: Facts) { const matches = [...this.rules] .sort((a, b) => b.priority - a.priority) .filter((r) => r.when(facts)) .map((r) => ({ rule: r, amount: r.discount(facts) })); return { discount: Math.min(facts.subtotal, this.strategy.combine(matches)), applied: matches.map((m) => m.rule.name) }; }} const rules: Rule[] = [ { name: "member-10pct", priority: 2, when: (f) => f.member, discount: (f) => f.subtotal * 0.1 }, { name: "big-cart", priority: 1, when: and((f) => f.subtotal >= 100, not((f) => f.coupon === "SAVE20")), discount: () => 10 }, { name: "save20", priority: 3, when: or((f) => f.coupon === "SAVE20"), discount: () => 20 },]; new RuleEngine(rules, bestSingle).evaluate({ subtotal: 150, member: true, itemCount: 3 }); // { discount: 15, ... }Complexity and performance
Fine for hundreds of rules.
For thousands of rules.
Trade-offs
Externalized rules empower business users but make behavior harder to trace; log which rules fired.
Rules in code are type-safe and tested; rules in config change without deploys but need validation and versioning.
Variants and related techniques
Rows of conditions and outcomes, easy for business users.
Rules add points; thresholds decide outcomes (fraud scoring).
Common mistakes
- Rules with hidden side effects.
Fix: Keep conditions pure; perform actions after evaluation.
- Undefined conflict resolution.
Fix: Specify priority and combine strategy explicitly.
- No audit of fired rules.
Fix: Return and log which rules matched for explainability.
Interview questions
How would you design an extensible discount engine?
Rules with composable conditions (Specification pattern) and discount calculations, a priority order, and a pluggable combination strategy (best single, stack all, exclusive groups), with rules optionally loaded from validated configuration.
How do you make rules explainable?
Return the list of rules that matched with their contributions, log them with the decision, and version the rule set so decisions can be reproduced.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Discount engine with stackable rules | Medium | Strategy for combining. |
| Fraud rules loaded from JSON | Hard | Validation and versioning. |