Skip to content

Interpreter Pattern: Turn Business Rules into a Tiny Language

Model rules like "cartTotal > 499 AND city == Bengaluru OR isPrime" as a tree of small expression objects, so changing an offer means changing data, not code.

Builds on Design Pattern: Composite (Structural)

Stage 3advanced2 min read

The marketing team runs three offers this week:

CodeRule as marketing wrote it
BLR100Cart above ₹499 in Bengaluru, or any Prime member
BULK503 or more items and cart above ₹999, but not in Mumbai
FESTIVE20Prime members in Bengaluru or Chennai

Here is how they ended up in code.

What the code does

CouponService.eligibleCoupons(order) checks every rule by hand:

// BLR100: "cart above 499 in Bengaluru, or any Prime member"
if (order.cartTotal > 499) {
    if (order.city.equals("Bengaluru")) {
        coupons.add("BLR100");
    } else if (order.isPrime) {
        coupons.add("BLR100");
    }
} else if (order.isPrime) {
    coupons.add("BLR100");
}

For four sample orders it prints:

Rs 650, Bengaluru, prime=false, items=2 -> [BLR100]
Rs 320, Pune, prime=true, items=1 -> [BLR100]
Rs 1200, Mumbai, prime=false, items=4 -> []
Rs 1500, Chennai, prime=true, items=3 -> [BLR100, BULK50, FESTIVE20]

The answers are right. The design is the problem.

Problem 1: a one-line rule became nine lines

Marketing's rule is (cartTotal > 499 AND city == "Bengaluru") OR isPrime. In code, isPrime is checked in two branches and coupons.add("BLR100") appears three times. To learn the rule, you must trace every branch. If you change one branch and forget the other, Prime users in Pune stop getting the coupon.

Problem 2: every tweak is a release

// BULK50: "3 or more items and cart above 999, but not in Mumbai"
// Last week it was "not in Delhi". Next week, who knows.
if (order.itemCount >= 3) {
    if (order.cartTotal > 999) {
        if (!order.city.equals("Mumbai")) {
            coupons.add("BULK50");
        }
    }
}

Mumbai becomes Delhi. ₹999 becomes ₹1,199. Each change needs a ticket, a code edit, a review, a test run and a deploy. Marketing may change a number on a Monday morning, and this is a lot of process for that.

Problem 3: the rules are invisible

Someone in marketing asks "which offers are live, and what are the conditions?" The honest answer is "let me read the Java". You can't print the rules, show them on an admin screen or store them in a database. You can't turn one off without a release.

Problem 4: no reuse

city == "Bengaluru" and isPrime appear in more than one offer, but each offer checks them in its own way. There are no small, named pieces you can test alone. You can only test the whole eligibleCoupons method.

Summary of the smells

SmellWhy it hurts
Rules expressed as control flowHard to read, easy to break one branch
Rules hard-coded in JavaEvery change needs a developer and a deploy
Rules not stored as dataCan't list, show, store or toggle them
No small building blocksSame checks repeated, only big tests possible

What we want instead

We want each rule to be a value: an object we can build, print, store and evaluate. We want to make it from tiny reusable pieces like "greater than", "equals", "and" and "or".

That is the Interpreter pattern. See the After version.

Run it:

javac -d out src/DesignPatterns/BehaviouralPatterns/InterpreterPattern/Before/Main.java
java -cp out DesignPatterns.BehaviouralPatterns.InterpreterPattern.Before.Main

Nested if statements are not always bad. They become a problem when the same small set of words (amounts, cities, membership, AND, OR, NOT) is mixed in new ways again and again. They also become a problem when you must inspect the rules or supply them from outside the service. If developers own three stable rules, plain Java may be the better design.

In this topic

Before code1 file

Before/Main.java85 linesGitHub
package DesignPatterns.BehaviouralPatterns.InterpreterPattern.Before;import java.util.ArrayList;import java.util.List;class Order {    final double cartTotal;    final String city;    final boolean isPrime;    final int itemCount;    Order(double cartTotal, String city, boolean isPrime, int itemCount) {        this.cartTotal = cartTotal;        this.city = city;        this.isPrime = isPrime;        this.itemCount = itemCount;    }    @Override    public String toString() {        return String.format("Rs %.0f, %s, prime=%s, items=%d", cartTotal, city, isPrime, itemCount);    }}/** * PROBLEM: every offer rule is hard-coded as nested ifs. * Marketing changes these numbers and cities every week. Each change means * a developer edits this class, a reviewer checks it, and a full deploy goes out. */class CouponService {    List<String> eligibleCoupons(Order order) {        List<String> coupons = new ArrayList<>();        // BLR100: "cart above 499 in Bengaluru, or any Prime member"        if (order.cartTotal > 499) {            if (order.city.equals("Bengaluru")) {                coupons.add("BLR100");            } else if (order.isPrime) {                coupons.add("BLR100");            }        } else if (order.isPrime) {            coupons.add("BLR100");        }        // BULK50: "3 or more items and cart above 999, but not in Mumbai"        // Last week it was "not in Delhi". Next week, who knows.        if (order.itemCount >= 3) {            if (order.cartTotal > 999) {                if (!order.city.equals("Mumbai")) {                    coupons.add("BULK50");                }            }        }        // FESTIVE20: "Prime members in Bengaluru or Chennai"        if (order.isPrime) {            if (order.city.equals("Bengaluru") || order.city.equals("Chennai")) {                coupons.add("FESTIVE20");            }        }        return coupons;    }}public class Main {    public static void main(String[] args) {        CouponService service = new CouponService();        List<Order> orders = List.of(            new Order(650, "Bengaluru", false, 2),            new Order(320, "Pune", true, 1),            new Order(1200, "Mumbai", false, 4),            new Order(1500, "Chennai", true, 3)        );        for (Order order : orders) {            System.out.println(order + " -> " + service.eligibleCoupons(order));        }    }}

Done with this topic?

How well do you know it?