Interpreter Design Pattern in Java: Building an Expression Evaluator

You need to evaluate expressions like (5 + 3) * 2 at runtime — from a config file, a business rule engine, or a user-defined formula. One approach: write a hand-rolled parser. Another: use the Interpreter pattern — map each grammar rule to a class, compose them into an abstract syntax tree (AST), then evaluate by calling interpret() on the root.

Interpreter is a GoF behavioral pattern. You encounter it constantly: SQL engines, Java’s regex Pattern class, Spring Expression Language (SpEL), Thymeleaf templates, and JEXL all apply this pattern. Understanding its structure explains how expression languages work under the hood.

Pattern Structure

Four participants:

  • AbstractExpression — declares interpret(). Every node in the AST implements this.
  • TerminalExpression — leaf node. Implements interpret() directly with no child expressions. A number literal is a terminal.
  • NonTerminalExpression — internal node. Holds references to child expressions and delegates to them inside interpret(). An operator is a non-terminal.
  • Client — assembles the AST from expression objects and triggers evaluation by calling interpret() on the root.
          Expression  (interface: interpret())
         /                    |
 NumberExpression      AddExpression
 (terminal — leaf)    SubtractExpression     ← non-terminals
                       MultiplyExpression    (hold left + right children)

Implementation: Math Expression Evaluator

We evaluate integer arithmetic expressions. Each operator becomes a non-terminal expression node; each integer is a terminal leaf.

package interpreter;

/**
 * AbstractExpression — every node in the AST implements this.
 */
public interface Expression {
    int interpret();
}
package interpreter;

/**
 * TerminalExpression — a number literal.
 * Leaf node: no children, returns its value directly.
 */
public class NumberExpression implements Expression {
    private final int number;

    public NumberExpression(int number) {
        this.number = number;
    }

    @Override
    public int interpret() {
        return number;
    }
}
package interpreter;

/** NonTerminalExpression — addition: left + right */
public class AddExpression implements Expression {
    private final Expression left;
    private final Expression right;

    public AddExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret() {
        return left.interpret() + right.interpret();
    }
}
package interpreter;

/** NonTerminalExpression — subtraction: left - right */
public class SubtractExpression implements Expression {
    private final Expression left;
    private final Expression right;

    public SubtractExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret() {
        return left.interpret() - right.interpret();
    }
}
package interpreter;

/** NonTerminalExpression — multiplication: left * right */
public class MultiplyExpression implements Expression {
    private final Expression left;
    private final Expression right;

    public MultiplyExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret() {
        return left.interpret() * right.interpret();
    }
}
package interpreter;

public class Main {
    public static void main(String[] args) {
        // (5 + 3) * 2  →  16
        Expression expr1 = new MultiplyExpression(
            new AddExpression(
                new NumberExpression(5),
                new NumberExpression(3)
            ),
            new NumberExpression(2)
        );
        System.out.println("(5 + 3) * 2 = " + expr1.interpret());

        // 10 - (4 + 2)  →  4
        Expression expr2 = new SubtractExpression(
            new NumberExpression(10),
            new AddExpression(
                new NumberExpression(4),
                new NumberExpression(2)
            )
        );
        System.out.println("10 - (4 + 2) = " + expr2.interpret());

        // (3 * 4) + (10 - 6)  →  16
        Expression expr3 = new AddExpression(
            new MultiplyExpression(
                new NumberExpression(3),
                new NumberExpression(4)
            ),
            new SubtractExpression(
                new NumberExpression(10),
                new NumberExpression(6)
            )
        );
        System.out.println("(3 * 4) + (10 - 6) = " + expr3.interpret());
    }
}

Console Output

# javac 03-behavioral/interpreter/*.java -d out/interpreter
# java -cp out/interpreter interpreter.Main

(5 + 3) * 2 = 16
10 – (4 + 2) = 4
(3 * 4) + (10 – 6) = 16

How the AST Evaluates

For (5 + 3) * 2, the client builds this tree:

         *          ← MultiplyExpression
        / 
       +   2        ← AddExpression, NumberExpression(2)
      / 
     5   3          ← NumberExpression(5), NumberExpression(3)

Calling interpret() on the root recursively traverses the tree bottom-up. Terminals return their value immediately. Non-terminals call interpret() on their children first, then apply their operator to the results. The value propagates up: 5+3=8, then 8*2=16.

💡 Interpreter + Visitor: Interpreter defines what the tree means (evaluation). Visitor defines operations on the tree (type-checking, code generation, pretty-printing). Production compilers use both: an Interpreter-style AST where each node has accept(), and a set of Visitors traversing it for each compiler phase. ANTLR generates this exact combination. If you need multiple operations on the same expression tree, reach for Visitor on top of Interpreter.

When to Use Interpreter

Interpreter is a good fit when: you have a simple, well-defined grammar that changes infrequently. Sentences can be naturally represented as a tree. You need to evaluate many sentences and want each grammar rule as a self-contained, testable class. The grammar has roughly 5–15 rules — small enough that one class per rule stays manageable.

Interpreter is the wrong tool when: the grammar is large or evolves frequently — every new rule means a new class, and changes ripple across the hierarchy. Efficiency is critical — recursive tree traversal adds overhead per evaluation. Your grammar is fixed and small — a hand-written recursive-descent parser or Java 21 switch pattern matching may be clearer and faster with less boilerplate.

✅ Interpreter in the JDK and Ecosystem: java.util.regex.Pattern compiles a regex string into an internal AST of Node subclasses, each with a match() method — textbook Interpreter. Spring SpEL evaluates expressions like #{order.total > 100 ? 'VIP' : 'standard'} by building an AST of Expression nodes. Apache Commons JEXL and MVEL embed Interpreter-pattern expression engines for rules engines and workflow systems. SQL parsers in Hibernate, jOOQ, and Calcite parse SQL into relational algebra expression trees before execution — all the same pattern at a larger scale.

Runnable Code on GitHub

Full source at ankurm.com/git.app/asmhatre/design-patterns under 03-behavioral/interpreter/.

javac 03-behavioral/interpreter/*.java -d out/interpreter
java -cp out/interpreter interpreter.Main

See Also

Further Reading

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.