Skip to main content

Design an Extensible Rules/Decision Engine

Problem Statement

Design an extensible rules and decision engine (similar to Drools or JSON-Rules-Engine). The engine must allow developers to compile complex business validation rules (e.g., for calculating discounts or evaluating fraud scores) dynamically, compose them into a hierarchical tree using logical operators (AND, OR, NOT), and recursively evaluate them against dynamic context maps.

Asked In Companies
PayPal Stripe Razorpay

Design Decisions & Patterns Used

Hardcoding complex business rules inside nested if-else conditions makes application code fragile and difficult to maintain. A rules engine extracts this logic into structured rule definitions. By compiling rules into an Abstract Syntax Tree (AST), we can parse, modify, and evaluate nested expressions dynamically.

We will utilize the following Design Patterns:

  • Composite Pattern: Treating individual rule checks (leaf nodes) and groups of rules (composite logical nodes) uniformly.
  • Interpreter Pattern: Evaluating the compiled rule tree recursively against input parameters.
  • Builder Pattern: Providing a fluent programmatic API to construct deeply nested rule groups easily.

Functional Requirements

  • Model comparison conditions (e.g., equals, greater than) as pluggable rule nodes.
  • Model logical operator nodes (AND, OR, NOT) to combine multiple rules.
  • Support evaluating rules against a dynamic execution context (a map of key-value properties).
  • Build an extensible API to easily add new comparison operators.

Objects Required

  • EvaluationContext (Wrapper managing input facts/parameters)
  • Rule (Component interface exposing evaluation methods)
  • EqualsRule, GreaterThanRule (Leaf rules implementing basic comparison logic)
  • AndRule, OrRule, NotRule (Composite rules implementing logical operators)

EvaluationContext & Rule Interface

The EvaluationContext class stores the variables to evaluate, and the Rule interface defines the execution contract.


import java.util.HashMap;
import java.util.Map;

public class EvaluationContext {
    private final Map<String, Object> facts;

    public EvaluationContext() {
        this.facts = new HashMap<>();
    }

    public void setFact(String name, Object value) {
        facts.put(name, value);
    }

    public Object getFact(String name) {
        return facts.get(name);
    }
}

Let's define the Rule interface:


public interface Rule {
    boolean evaluate(EvaluationContext context);
}

The evaluate() method accepts the context facts and returns a boolean value indicating whether the rule conditions are met.


Comparison Rules (Leaf Nodes)

We implement comparison rules to serve as leaf nodes in our rules tree: EqualsRule and GreaterThanRule.


public class EqualsRule implements Rule {
    private final String key;
    private final Object expectedValue;

    public EqualsRule(String key, Object expectedValue) {
        this.key = key;
        this.expectedValue = expectedValue;
    }

    @Override
    public boolean evaluate(EvaluationContext context) {
        Object actual = context.getFact(key);
        if (actual == null) return false;
        return actual.equals(expectedValue);
    }
}

The EqualsRule checks if a context variable matches the expected value, returning false if the variable is missing.


public class GreaterThanRule implements Rule {
    private final String key;
    private final double threshold;

    public GreaterThanRule(String key, double threshold) {
        this.key = key;
        this.threshold = threshold;
    }

    @Override
    public boolean evaluate(EvaluationContext context) {
        Object actual = context.getFact(key);
        if (actual instanceof Number) {
            return ((Number) actual).doubleValue() > threshold;
        }
        return false;
    }
}

The GreaterThanRule performs numeric comparisons by parsing variables to double values and evaluating them against the threshold.


Logical Rules (Composite Nodes)

We implement logical rules to combine child rules: AndRule, OrRule, and NotRule. These nodes enable parsing nested boolean logic.


import java.util.ArrayList;
import java.util.List;

public class AndRule implements Rule {
    private final List<Rule> rules;

    public AndRule() {
        this.rules = new ArrayList<>();
    }

    public void addRule(Rule rule) {
        rules.add(rule);
    }

    @Override
    public boolean evaluate(EvaluationContext context) {
        for (Rule rule : rules) {
            if (!rule.evaluate(context)) {
                return false; // Short-circuit logic
            }
        }
        return true;
    }
}

The AndRule evaluates child rules sequentially, short-circuiting and returning false immediately if any child rule fails.


import java.util.ArrayList;
import java.util.List;

public class OrRule implements Rule {
    private final List<Rule> rules;

    public OrRule() {
        this.rules = new ArrayList<>();
    }

    public void addRule(Rule rule) {
        rules.add(rule);
    }

    @Override
    public boolean evaluate(EvaluationContext context) {
        for (Rule rule : rules) {
            if (rule.evaluate(context)) {
                return true; // Short-circuit logic
            }
        }
        return false;
    }
}

The OrRule returns true immediately if any child rule evaluates to true.


public class NotRule implements Rule {
    private final Rule rule;

    public NotRule(Rule rule) {
        this.rule = rule;
    }

    @Override
    public boolean evaluate(EvaluationContext context) {
        return !rule.evaluate(context);
    }
}

The NotRule negates the boolean outcome of its single wrapped child rule.


Main Driver Class

This class tests our extensible rules engine by creating and evaluating a complex checkout validation rule.


public class Main {
    public static void main(String[] args) {
        // Build rule tree:
        // (OrderTotal > 100.0 AND PaymentMethod == "CREDIT_CARD") OR UserType == "VIP"
        
        AndRule standardPromoRule = new AndRule();
        standardPromoRule.addRule(new GreaterThanRule("orderTotal", 100.0));
        standardPromoRule.addRule(new EqualsRule("paymentMethod", "CREDIT_CARD"));

        OrRule checkoutDiscountRule = new OrRule();
        checkoutDiscountRule.addRule(standardPromoRule);
        checkoutDiscountRule.addRule(new EqualsRule("userType", "VIP"));

        System.out.println("Rule structure compiled successfully.");

        // Case 1: Standard user, high order value, paying with credit card (Should pass)
        System.out.println("\n--- Evaluating Case 1 (Standard User, Total: 150, Credit Card) ---");
        EvaluationContext context1 = new EvaluationContext();
        context1.setFact("orderTotal", 150.0);
        context1.setFact("paymentMethod", "CREDIT_CARD");
        context1.setFact("userType", "STANDARD");
        
        boolean result1 = checkoutDiscountRule.evaluate(context1);
        System.out.println("Result: " + result1 + " (Expected: true)");

        // Case 2: Standard user, low order value, paying with credit card (Should fail)
        System.out.println("\n--- Evaluating Case 2 (Standard User, Total: 50, Credit Card) ---");
        EvaluationContext context2 = new EvaluationContext();
        context2.setFact("orderTotal", 50.0);
        context2.setFact("paymentMethod", "CREDIT_CARD");
        context2.setFact("userType", "STANDARD");

        boolean result2 = checkoutDiscountRule.evaluate(context2);
        System.out.println("Result: " + result2 + " (Expected: false)");

        // Case 3: VIP user, low order value, paying with cash (Should pass due to VIP status)
        System.out.println("\n--- Evaluating Case 3 (VIP User, Total: 20, Cash) ---");
        EvaluationContext context3 = new EvaluationContext();
        context3.setFact("orderTotal", 20.0);
        context3.setFact("paymentMethod", "CASH");
        context3.setFact("userType", "VIP");

        boolean result3 = checkoutDiscountRule.evaluate(context3);
        System.out.println("Result: " + result3 + " (Expected: true)");
    }
}

The main() driver assembles a composite rule tree, configures multiple verification contexts, and runs the evaluation logic to test short-circuiting and node routing behaviors.


Also See

Comments

Popular posts from this blog

Designing a Parking Lot - Low Level Design

Problem Statement Design a parking lot that can handle vehicles entering and leaving while managing parking across multiple floors. Each vehicle should be assigned a suitable parking spot based on its type, and the spot should be freed once the vehicle exits. The design should also support generating a ticket at entry and optionally calculating the parking fee based on the duration of stay. Asked In Companies Amazon Google Microsoft Uber Walmart Flipkart Meta PayPal Oracle Salesforce Adobe Apple Intuit LinkedIn Atlassian Functional Requirements The design should support multiple vehicle types such as bikes, cars, and trucks A vehicle must be assigned a parking spot compatible with its type A parking spot cannot be assigned to more than one vehicle at a time The parking lot should support multiple levels (floors) The design should search and allocate an availa...

Most Frequently Asked Low Level Design(LLD) Interview Questions

Below are the curated list of most commonly asked Low Level Design (LLD) interview problems. Each problem includes a short description and a link to the complete solution with code and class diagrams. Design Parking Lot System The system should handle parking for different vehicle types such as bikes, cars, and trucks. It should manage slot allocation, availability tracking, and entry/exit flow. The design also ensures efficient usage of parking space under varying load conditions. View Solution Design Elevator / Lift System The system should support multiple elevators operating across floors with request handling logic. It focuses on scheduling algorithms to minimize wait time and optimize movement. It also manages direction control and concurrent floor requests. View Solution Design Movie Ticket Booking System The system should allow users to browse movies, select shows, and book seats. It handles seat ...

Software Design Patterns for LLD Interviews: A Complete Guide

Software Design Patterns for LLD Interviews: A Complete Guide In Software Development Engineer (SDE) interviews—especially for mid-level and senior roles—low-level design (LLD) rounds assess your ability to write clean, reusable, maintainable, and extensible code. The foundation of resolving these architectural challenges lies in the standard Gang of Four (GoF) Design Patterns. Rather than memorizing theoretical definitions, interviewers expect you to apply these patterns to real-world scenarios, identifying the trade-offs of each. Below is a comprehensive guide to the 12 most frequently asked design patterns in LLD interviews, categorized by their classification (Creational, Structural, and Behavioral). Each pattern contains a concrete, real-world Java implementation and a detailed breakdown of design decisions. Creational Design Patterns Creational design patterns deal with object creation mechanisms. They abstract the instantiation process, making a system independent of how...