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 3advanced8 min read

The marketing team at a shopping app changes coupon rules every week. Last week it was "cart above ₹499 in Bengaluru, or any Prime member". This week it is "cart above ₹999 with 3 or more items, but not in Mumbai". Next week Chennai gets added.

If each rule is an if block in Java, every change needs a code edit, a review and a deploy. Interpreter gives you another option. Treat the rules as a small language, and write one class for each piece of that language.

A warning first: Interpreter is one of the least used of the 23 GoF (Gang of Four) patterns. You will rarely build one from scratch. But you use interpreters every day (regex, SQL, Spring's @Value("#{...}")), and the idea comes up in interviews. So it is worth knowing.

What it is

Interpreter says: if you have a simple language, write each sentence as a tree of objects. Give every node an interpret(context) method.

That tree is called an expression tree. It is the rule cut into pieces and laid out like a family tree. The whole rule is at the top, the smallest checks are at the bottom, and the joining words (AND, OR) are in between.

  • Leaf nodes (terminal expressions) are the simple bits: cartTotal > 499, city == "Bengaluru", isPrime. "Terminal" means nothing hangs below them. They can answer on their own.
  • Inner nodes (non-terminal expressions) combine other nodes: AND, OR, NOT. They must wait for their children to answer first.
  • Context holds the facts you check the rule against, like this order's total and city.

To check a rule, call interpret on the root (the top node). Each node asks its children, then combines their answers.

Real-life analogy

Think of a recipe card. "Add salt if the dal is bland, and add chilli if the guests like it spicy or it is a weekend." The card is the rule. The cook reads it against today's situation (the context). Change the card and the cook acts differently, with no retraining.

The problem

Here is a slice of the Before code:

// 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");
}
  • Rules are buried in control flow. A one-line rule from marketing became nine lines of nested if. isPrime is checked in two places. (Control flow means the if and else structure.)
  • Every change needs a developer. Changing ₹499 to ₹599 means a code change and a deploy.
  • Rules can't be listed or shown. Marketing asks "what rules are live?" and the only answer is "read CouponService.java".
  • Rules can't be stored. You can't keep them in a database, and you can't switch one off without a release.

Class diagram

Loading diagram…
ComponentJob
Abstract expression (Expression)Declares interpret(context).
Terminal expressions (GreaterThan, EqualsTo, Variable)Leaves. Read a value from the context and compare it.
Non-terminal expressions (And, Or, Not)Hold child expressions and combine their answers.
ContextThe facts for one evaluation: cartTotal, city, isPrime, itemCount.
Client (CouponService)Builds or receives the tree, then calls interpret on the root.

If this looks like Composite, that is because it is. The expression tree is a Composite: leaves and containers share one interface. Interpreter adds one idea: "every node knows how to evaluate itself".

One call, step by step

The rule (cartTotal > 499 AND city == "Bengaluru") OR isPrime against a ₹650 order from Pune by a Prime member:

Loading diagram…

How it solves the problems

The rule now reads almost the way marketing wrote it (After code):

rules.put("BLR100", or(
    and(gt("cartTotal", 499), eq("city", "Bengaluru")),
    is("isPrime")
));
BeforeAfter
Nested ifs, up to nine lines for one ruleOne tree per rule
Every change is a code changeChange the tree. Once you add a parser, change a string in the DB.
No way to list rulestoString() prints ((cartTotal > 499 AND city == "Bengaluru") OR isPrime)
Rules stuck in JavaTrees can be built from config, stored, versioned, switched on and off

A note on parsing

Interpreter is about evaluating a tree. It does not say how the tree gets built. In this example, Main builds every tree with Java constructor and helper calls. This is a shortcut to keep the lesson simple, but it is also a real limit. Changing Mumbai to Delhi still means editing Java, compiling and deploying. The sample makes a rule easy to read and to combine. It does not make the rule configurable from outside the code.

For marketing to change stored text without a deploy, you need one more step. Store text such as cartTotal > 499 AND city == "Bengaluru". Parse it (read the text and turn it into objects) and validate it into the same expression tree. Keep versions of it, and load it safely. Writing that parser is a separate job. People usually use ANTLR, an existing expression language, or a small hand-written recursive-descent parser (one that reads the text piece by piece, top down). Keep parsing and evaluation apart, so bad input is rejected before interpret runs.

Interpreter and Visitor

Here every node has one job: interpret. Real tools soon want more. They print the rule, validate it, check that every variable exists, or turn it into a SQL WHERE clause. If each of these becomes a method on every node class, the classes get crowded fast.

That is where many compilers switch to Visitor. The tree stays plain data. "Evaluate", "print" and "type check" each become a separate visitor class. javac works this way. SpEL does the opposite and keeps evaluation on the nodes. Both are fine. A coupon engine has one operation, so interpret on the node is simpler.

Pros and cons

Pros

  • Each grammar rule (one piece of the language) is one small class, easy to test alone.
  • A new operator (LessThan, In, Between) is a new class. Existing classes don't change.
  • You can print and inspect expression trees right away. After you add a parser or configuration loader, you can also store and edit rules outside Java.
  • The tree shape makes rules easy to follow.

Cons

  • One class per grammar rule. A real language needs dozens, and it gets messy.
  • Slow for big inputs. Walking a tree of objects is much slower than running compiled code.
  • You must write the error messages, type checking and parsing yourself.
  • Easy to over-build. Most teams need a library, not their own language.

When to use it

  • The grammar is small and stable, such as a few comparisons joined by AND, OR and NOT.
  • Rules change often, and you want non-developers (or config files) to control them.
  • You need to show, store or audit the rules, not only run them.

When not to use it

  • The grammar is big. Use a parser generator (a tool that builds a parser from a grammar, such as ANTLR) or an existing expression language like Spring Expression Language (SpEL), MVEL or Apache Commons JEXL.
  • The rules are a whole business process. Use a rules engine such as Drools or Easy Rules.
  • The rules are fixed and only developers change them. The Specification pattern (from Domain-Driven Design) is simpler. It uses small objects with isSatisfiedBy(order) and and/or/not helpers. It is Interpreter without the "language" part, and most of the time that is enough. In plain Java, Predicate<Order> with .and() and .or() also gets you most of the way.
  • Performance matters a lot. Walking a tree is slow. Real engines compile expressions into bytecode or another faster form. SpEL, for example, has an optional compiler mode that turns hot expressions (the ones used most) into bytecode.

Real-world places you've seen it

  • java.util.regex.Pattern: Pattern.compile turns the regex string into a tree of node objects. Matching walks those nodes along the input.
  • SQL databases: a WHERE clause is parsed into an expression tree, then checked against each row (often after the tree is optimised or compiled).
  • Spring Expression Language (SpEL): #{order.total > 499 and order.city == 'Bengaluru'} is parsed into an AST (abstract syntax tree, the same kind of tree) with node classes like OpGT, OpAnd and OpOr, then evaluated against a context.
  • Template engines and calculators: Thymeleaf expressions, spreadsheet formulas and the classic "evaluate 3 + 4 * 2" interview problem all use the same tree idea.

Common mistakes

  • Building a full language when a Predicate would do. Start small. Use the pattern only when rules must come from outside the code.
  • Mixing parsing into the expression classes. GreaterThan should evaluate. It should not read strings.
  • Putting mutable state in the tree. Mutable means it can change. Keep expressions unchangeable (immutable) and put per-request data in the Context. Then many threads can share one tree safely.
  • Silent type errors. If cartTotal is missing or is a string, fail loudly with a clear message instead of returning false.
  • Ignoring the depth of the tree. Deeply nested rules can hit recursion limits and run slowly. Check the rule size when you load it.

Interview questions

"What is the Interpreter pattern?"

It writes a sentence of a small language as a tree of objects. Each node class matches one grammar rule and has an interpret(context) method. You evaluate the sentence by calling interpret on the root, and each node combines the answers of its children.

"What is the difference between terminal and non-terminal expressions?"

Terminal expressions are leaves, like cartTotal > 499 or a variable, and they read straight from the context. Non-terminal expressions, like AND and OR, hold other expressions and combine their results.

"How is Interpreter related to Composite?"

The expression tree is a Composite: leaves and combining nodes share one interface. Interpreter is Composite plus a specific meaning, where every node knows how to evaluate itself against a context.

"Does the Interpreter pattern include parsing?"

No. It only covers representing and evaluating the tree. Turning rule text into that tree is a separate step, done with a parser generator like ANTLR or a hand-written parser.

"Why is Interpreter rarely used directly?"

Most needs are covered by existing tools: regex, SQL, SpEL, JEXL or a rules engine like Drools. For simple fixed rules, the Specification pattern or Predicate composition is lighter. Writing your own only makes sense for a small, stable grammar.

"Interpreter vs Specification pattern?"

Both build trees of boolean conditions with and, or and not. Specification is about business rules on domain objects, usually written in code by developers. Interpreter is about a language with a grammar, usually so rules can come from text or config.

"Where have you seen Interpreter in Java?"

java.util.regex.Pattern compiles a regex into a tree of matcher nodes, Spring Expression Language parses expressions into an AST of nodes like OpAnd and OpGT, and SQL engines turn WHERE clauses into expression trees.

In this topic

Done with this topic?

How well do you know it?