Interpreter Pattern
Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language. The foundation of expression evaluators, rule engines, query parsers, and DSLs.
Why Interpreter Exists
You’re building a business rules engine. Users define rules like: "age > 18 AND (country == 'US' OR country == 'UK')". These rules are stored as strings in a database and evaluated at runtime against customer data. The naive approach: parse the string with a giant if/else chain or regex.
What goes wrong:
- No formal grammar — the parsing logic is ad-hoc; adding
NOT,>=, or nested expressions requires rewriting the parser - Operator precedence ignored —
split("AND")doesn’t respect parentheses or precedence - Can’t compose expressions — no tree structure; complex rules can’t be built from simpler ones
- No reuse — each new operator/literal requires duplicating parsing logic
- Untestable — the monolithic evaluator can’t be unit-tested at the expression level
This is the problem Interpreter solves — define a formal grammar for the language, represent each grammar rule as a class, and build a tree (AST) of expression objects that can interpret() themselves. Adding a new operator = adding one class.
What Is Interpreter?
Interpreter is a behavioral pattern that defines a representation for a grammar and an interpreter that evaluates sentences in that language. Each grammar rule becomes a class. Terminal symbols (literals, variables) are leaf nodes; non-terminal symbols (operators, combinators) are composite nodes containing sub-expressions. The resulting tree structure is evaluated recursively.
— Gamma, Helm, Johnson, Vlissides (1994)
3 + 4 * 2. A human parses this into a tree: the root is +, the left child is 3, the right child is * (4, 2). To evaluate: the * node interprets itself (4 × 2 = 8), then the + node interprets itself (3 + 8 = 11). Each node is an “expression object” that knows how to evaluate itself. Numbers are terminal expressions (leaves); operators are non-terminal expressions (composites). The entire tree is the “interpreter.”
Key insight: Interpreter = Composite pattern applied to a grammar. Each grammar rule produces a class. The class hierarchy mirrors the grammar’s BNF. Sentences in the language are represented as trees of expression objects. Evaluation is a recursive tree traversal where each node “interprets” itself.
Expr = Number | Expr '+' Expr | Expr '*' Expr | '(' Expr ')'NumberExpr, AddExpr, MulExpr. The grammar is the class hierarchy."3 + 4 * 2" becomes: AddExpr(NumberExpr(3), MulExpr(NumberExpr(4), NumberExpr(2)))interpret(context) methodSubtractExpr) = adding one classParticipants & Structure
| Participant | Role | In the Analogy |
|---|---|---|
| AbstractExpression (interface) | Declares the interpret(Context) method. All grammar rules implement this interface. Forms the common type for the AST tree nodes. | The “mathematical entity” contract — every number and operator can be “evaluated.” |
| TerminalExpression | Implements interpret() for terminal symbols (literals, variables). Leaf nodes of the AST. E.g., NumberExpr(5) returns 5; VarExpr("x") looks up x in the context. | Numbers in a math expression — they just are their value. |
| NonTerminalExpression | Implements interpret() for grammar rules involving other expressions. Contains references to sub-expressions (like Composite). E.g., AddExpr(left, right) evaluates left + right. | Operators (+, *, /) — they combine the values of their operands. |
| Context | Contains global information the interpreter needs during evaluation — variable bindings, function definitions, configuration. Passed to every interpret() call. | The “variable table” — mapping variable names to values (x=5, y=3). |
| Client | Builds (or receives) the AST and calls interpret(context) on the root. May include a parser that converts string input into the AST. | You writing 3 + x on paper and asking “evaluate this with x=7.” |
- Terminal expressions are leaves; non-terminal expressions are composites containing sub-expressions.
- The
interpret()method is the equivalent of Composite’s recursiveoperation(). - If you understand Composite, you understand Interpreter’s structure.
- The difference is that Interpreter defines a language with grammar rules, while Composite defines a part-whole hierarchy.
The Pattern In Motion
Scenario: Evaluate the expression "3 + x * 2" with the context {x = 5}. The expression is parsed into an AST, then the root’s interpret() is called recursively.
"3 + x * 2" into an ASTAddExpr( NumberExpr(3), MulExpr( VarExpr("x"), NumberExpr(2) ) ). Operator precedence handled during parsing — * binds tighter than +.root.interpret(ctx) where ctx = {x:5}AddExpr.interpret(ctx) is called. It must evaluate its left and right children first.NumberExpr(3).interpret(ctx)3. Terminal expression — just returns its value. No recursion needed.MulExpr.interpret(ctx)VarExpr("x").interpret(ctx) β looks up x in context β 5. NumberExpr(2).interpret(ctx) β 2. Returns 5 × 2 = 10.AddExpr combines: left(3) + right(10)13. The entire expression is evaluated by recursive tree traversal. Each node only knows its own operation.- The Interpreter pattern focuses on the evaluation of the AST, not the parsing of the input string.
- In practice, you need a parser (recursive descent, Pratt parser, or a library like ANTLR) to convert
"3 + x * 2"into the AST. - The pattern handles what happens after parsing.
You Already Use This
Interpreter appears wherever a “mini-language” is parsed and evaluated — regex engines, expression languages, SQL parsers, and template systems.
java.util.regex.Pattern — compiles a regex string ("[a-z]+\\d{2,3}") into an internal AST of expression nodes (character classes, quantifiers, alternations). Matcher interprets this AST against input strings. Each regex construct is a node type. javax.el.ExpressionFactory (JSP/JSF Expression Language) — ${user.name} is parsed into an AST and interpreted against a context. ValueExpression and MethodExpression are the expression interfaces. Spring SpEL (Spring Expression Language) — "#root.name?.toUpperCase()" is parsed by SpelExpressionParser into an AST of SpelNode objects. getValue(context) interprets the tree. SpEL supports method calls, operators, collections, and ternary expressions. java.text.Format subclasses — MessageFormat.format("Hello 0, you have 1 items", name, count) parses the pattern string into format segments (literal text + placeholders). Each segment is interpreted against the arguments array. - SQL parsers (H2, JOOQ), JPQL (Hibernate), Cron expressions (Quartz), JSON Path (Jayway), XPath, CSS selectors — all parse a string into an AST and evaluate it.
- The Interpreter pattern is the theoretical foundation.
- In practice, most use parser generators (ANTLR, JavaCC) rather than hand-coding the pattern.
Build It Once
Domain: Arithmetic Expression Evaluator with variables. Grammar: Expr = Number | Variable | Expr '+' Expr | Expr '-' Expr | Expr '*' Expr. Each rule is a class with interpret(Map<String,Integer>).
- Java records give you immutable fields, equals/hashCode, and toString for free — ideal for AST nodes which are value objects.
- The
interpret()method is the only behavior. This makes the pattern extremely concise in modern Java.
Common Mistakes
- Naive recursive interpretation can be extremely slow for deeply nested expressions or repeated subexpressions.
- Each
interpret()call walks the tree from scratch. - Fix: Cache results (memoization) if expressions contain repeated sub-trees. For hot paths, compile the AST to bytecode (
MethodHandle) or use a stack-based evaluator instead of recursive tree walking.
- If a
VarExprreferences an undefined variable, or division by zero occurs, the interpreter crashes with a cryptic exception. - Fix: Validate the AST before interpretation. Throw domain-specific exceptions (
UndefinedVariableException,DivisionByZeroException) with information about which expression failed. Include the position in the original input if available.
- Developers sometimes put string parsing inside the expression constructor.
- This couples the AST to a specific input format.
- Fix: Keep parsing separate from interpretation. The parser produces the AST; the expression classes only know how to
interpret(). This allows the same AST to be built from different input formats (JSON, XML, string syntax).
- If the context is a single flat
Mapand expressions can modify it (e.g. assignment), you lose track of scope. - A variable defined in a sub-expression leaks into parent scope.
- Fix: Use a scoped context (chain of maps or a linked stack). When entering a new scope (function call, let binding), push a new frame. When exiting, pop it.
When To Use Interpreter
- You have a simple, well-defined grammar (5-15 rules) that users specify as strings evaluated at runtime
- The language is stable — grammar rules don’t change frequently (adding operators is occasional)
- You need user-configurable rules — business rules, filters, validation expressions stored in a database or config file
- Expressions need to be composed from simpler parts (AND/OR chains, arithmetic from +/*/- building blocks)
- You want runtime evaluation with different contexts (same expression, different variable bindings)
- The grammar is complex (30+ rules, ambiguous, context-sensitive) — use ANTLR or a parser generator
- Performance is critical — recursive tree walking is slow; compile to bytecode or use a stack VM instead
- A standard language already exists for the job — don’t reinvent SQL, regex, or cron expressions
- The expressions are static (known at compile time) — just write Java code; no need for runtime interpretation
| Approach | Best For | Complexity |
|---|---|---|
| Interpreter ← this | Simple DSLs, rule engines, expression evaluators (5-15 rules) | Low β one class per rule |
| ANTLR / Parser Generator | Complex grammars (SQL, programming languages, config formats) | Medium β grammar file generates parser + visitor |
| Scripting Engine (GraalVM, Nashorn) | Full scripting (JavaScript, Python, Ruby embedded in Java) | High β full language runtime |
| Strategy + Chain of Responsibility | Conditional logic without a grammar (simple if/else chains) | Minimal β no parsing at all |
Problems To Solve
Interpreter problems test whether you can define a grammar, build an AST, implement recursive evaluation, handle contexts, and understand the tradeoff between simplicity and scalability.
| Difficulty | Problem | Key Insight |
|---|---|---|
| Easy | Boolean Expression Evaluator Grammar: Expr = true | false | Var | Expr AND Expr | Expr OR Expr | NOT Expr. Build expression classes: BoolLiteral, VarExpr, AndExpr, OrExpr, NotExpr. Context maps variable names to boolean values. Evaluate: "isAdmin AND (hasPermission OR isSuperUser)" with given context. | Tests basic Interpreter with boolean domain. NotExpr is a unary non-terminal (one child). AndExpr/OrExpr are binary. The context maps strings to booleans. Short-circuit evaluation is optional but demonstrates optimization. |
| Medium | SQL WHERE Clause Interpreter Mini-grammar: Condition = field op value | Condition AND Condition | Condition OR Condition. Operators: =, !=, >, <, LIKE. Build an AST that evaluates rows (Map<String, Object>) against the condition. Example: "age > 18 AND name LIKE 'J%'" evaluated against {age:25, name:"John"} β true. | Tests Interpreter with mixed types (int comparison, string pattern matching). The LIKE operator requires simple wildcard handling (% = any). The context is a row (Map of fieldβvalue). This mirrors how query engines filter rows β each row is passed through the condition AST. |
| Medium | Arithmetic with Functions + Infix Parser Extend the basic arithmetic interpreter with: functions ( max(a,b), min(a,b), abs(x)), parenthesised expressions, and a proper infix parser (recursive descent or Pratt). Input: "max(3 + x, y * 2) - abs(z)". Context: {x:4, y:3, z:-5}. | Tests combining Interpreter with a real parser. Functions are non-terminal expressions with N children. The parser must handle arity (how many arguments?). Demonstrates that the pattern is about evaluation; parsing is a separate concern but must produce the correct AST. |
| Hard | Mini Rule Engine with Assignment + Conditions Grammar: Program = Statement*, Statement = Assignment | IfStatement, Assignment = var '=' Expr, IfStatement = 'if' Condition 'then' Statement*. Build: program AST, scoped context (assignment modifies context for subsequent statements), conditional execution. Example: "x = 5; y = x * 2; if y > 8 then z = y + 1" β context: {x:5, y:10, z:11}. | Tests Interpreter for an imperative mini-language with mutable context. Assignments modify the context (side effects); if conditionally executes sub-statements. This requires a scoped, mutable context and sequential statement execution. Demonstrates where Interpreter overlaps with a tree-walking interpreter for a programming language. Key: statements return void but modify context; expressions return values. |
- When asked about Interpreter, the interviewer wants to see: (1) grammar β class hierarchy mapping (terminal vs. non-terminal); (2) recursive
interpret(context)evaluation; (3) separation of parsing from interpretation; (4) when NOT to use it (complex grammars β ANTLR, performance-critical β compiler). - Stand-out answers mention: regex
Patternas JDK example, Spring SpEL, the connection to Composite pattern, and that modern alternatives (sealed classes + pattern matching, or embedded scripting via GraalVM) often replace hand-rolled Interpreter in production.