🔤 Interpreter Design Pattern
Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
🌍 Real-World Analogy
Analogy — Musical Notation
A piece of sheet music is a language with its own grammar — notes, rests, time signatures, dynamics. A musician (Interpreter) reads the notation and converts it into sound. Different musicians (piano, violin, guitar) interpret the same notation differently, but the grammar remains the same. Each symbol in the score is an expression that gets interpreted.
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
Score["🎼 Sheet Music"] -->|"reads"| P["🎹 Pianist"]
P -->|"next"| V["🎻 Violinist"]
V -->|"next"| G["🎸 Guitarist"]
G -->|"produces"| Sound(["🎵 Music"])
style Score fill:#FFF8E1,stroke:#F9A825,stroke-width:2px,color:#000
style P fill:#E3F2FD,stroke:#1565C0,color:#000
style V fill:#FCE4EC,stroke:#C62828,color:#000
style G fill:#E8F5E9,stroke:#2E7D32,color:#000
style Sound fill:#FFF3E0,stroke:#E65100,color:#000 🏗️ Pattern Structure
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
Client["👤 Client"] -->|"builds"| Expr[["🔤 Expression"]]
Client -->|"provides"| Context(["📋 Context"])
Expr -->|"implements"| Terminal{{"🅰️ Terminal"}}
Expr -->|"implements"| NonTerminal{{"🔗 NonTerminal"}}
NonTerminal -.->|"contains"| Expr
style Client fill:#E8F5E9,stroke:#2E7D32,color:#000
style Expr fill:#FFF3E0,stroke:#E65100,color:#000
style Context fill:#FCE4EC,stroke:#C62828,color:#000
style Terminal fill:#E3F2FD,stroke:#1565C0,color:#000
style NonTerminal fill:#E3F2FD,stroke:#1565C0,color:#000 UML Class Diagram
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
classDiagram
class Expression {
<<interface>>
+interpret(RuleContext context) boolean
}
class RuleContext {
-variables: Map~String, Object~
+setVariable(String name, Object value) void
+getVariable(String name) Object
+getInt(String name) int
+getString(String name) String
}
class EqualsExpression {
-variable: String
-value: Object
+interpret(RuleContext context) boolean
}
class GreaterThanExpression {
-variable: String
-value: int
+interpret(RuleContext context) boolean
}
class ContainsExpression {
-variable: String
-substring: String
+interpret(RuleContext context) boolean
}
class AndExpression {
-left: Expression
-right: Expression
+interpret(RuleContext context) boolean
}
class OrExpression {
-left: Expression
-right: Expression
+interpret(RuleContext context) boolean
}
class NotExpression {
-expression: Expression
+interpret(RuleContext context) boolean
}
EqualsExpression ..|> Expression
GreaterThanExpression ..|> Expression
ContainsExpression ..|> Expression
AndExpression ..|> Expression
OrExpression ..|> Expression
NotExpression ..|> Expression
AndExpression o-- Expression : left/right
OrExpression o-- Expression : left/right
NotExpression o-- Expression : wraps
Expression ..> RuleContext : uses ❓ The Problem
You need to evaluate expressions or sentences in a simple language or DSL:
- SQL-like query filters for a search engine
- Mathematical expression evaluation
- Regular expression matching
- Configuration rules (if user.age > 18 AND user.country == "US")
- Business rule engines
Without the Interpreter pattern, you'd write monolithic parsers with complex conditional logic that's hard to extend with new grammar rules.
Without This Pattern
public class RuleEngine {
public boolean evaluate(String rule, Map<String, Object> context) {
// Monolithic parser with hardcoded grammar handling
if (rule.contains(" AND ")) {
String[] parts = rule.split(" AND ");
return evaluate(parts[0].trim(), context)
&& evaluate(parts[1].trim(), context);
} else if (rule.contains(" OR ")) {
String[] parts = rule.split(" OR ");
return evaluate(parts[0].trim(), context)
|| evaluate(parts[1].trim(), context);
} else if (rule.contains(" > ")) {
String[] parts = rule.split(" > ");
int val = (Integer) context.get(parts[0].trim());
return val > Integer.parseInt(parts[1].trim());
} else if (rule.contains(" == ")) {
String[] parts = rule.split(" == ");
return context.get(parts[0].trim()).equals(parts[1].trim());
}
// Nested expressions? Operator precedence? Good luck.
throw new IllegalArgumentException("Cannot parse: " + rule);
}
}
- Cannot handle nested expressions —
(age > 18 AND country == US) OR role == adminbreaks the naive string splitting approach - No operator precedence — AND/OR priority must be manually coded with fragile string manipulation
- Impossible to extend — adding a new operator (CONTAINS, NOT, BETWEEN) requires modifying the monolithic method
- Not composable — rules cannot be programmatically constructed, combined, or reused as building blocks
- Pain point: A business analyst requests a rule with 3 levels of nesting and mixed AND/OR. The string-parsing approach produces wrong results because
split(" AND ")greedily splits inside parenthesized groups — and fixing it means writing a full parser anyway
✅ The Solution
The Interpreter pattern models grammar rules as a class hierarchy (Abstract Syntax Tree):
- AbstractExpression — declares an
interpret()method - TerminalExpression — represents leaf nodes (literals, variables)
- NonTerminalExpression — represents composite rules (AND, OR, sequences) containing other expressions
- Context — holds global information needed during interpretation (variable values, input)
The client builds the AST and then calls interpret() on the root — evaluation cascades through the tree.
💻 Implementation
// Context — holds variable values
public class RuleContext {
private final Map<String, Object> variables = new HashMap<>();
public void setVariable(String name, Object value) {
variables.put(name, value);
}
public Object getVariable(String name) {
return variables.get(name);
}
public int getInt(String name) {
return (Integer) variables.get(name);
}
public String getString(String name) {
return (String) variables.get(name);
}
}
// Abstract Expression
public interface Expression {
boolean interpret(RuleContext context);
}
// Terminal Expressions
public class EqualsExpression implements Expression {
private final String variable;
private final Object value;
public EqualsExpression(String variable, Object value) {
this.variable = variable;
this.value = value;
}
@Override
public boolean interpret(RuleContext context) {
return value.equals(context.getVariable(variable));
}
@Override
public String toString() {
return variable + " == " + value;
}
}
public class GreaterThanExpression implements Expression {
private final String variable;
private final int value;
public GreaterThanExpression(String variable, int value) {
this.variable = variable;
this.value = value;
}
@Override
public boolean interpret(RuleContext context) {
return context.getInt(variable) > value;
}
@Override
public String toString() {
return variable + " > " + value;
}
}
public class ContainsExpression implements Expression {
private final String variable;
private final String substring;
public ContainsExpression(String variable, String substring) {
this.variable = variable;
this.substring = substring;
}
@Override
public boolean interpret(RuleContext context) {
String val = context.getString(variable);
return val != null && val.contains(substring);
}
@Override
public String toString() {
return variable + " contains '" + substring + "'";
}
}
// Non-Terminal Expressions (Composite)
public class AndExpression implements Expression {
private final Expression left;
private final Expression right;
public AndExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public boolean interpret(RuleContext context) {
return left.interpret(context) && right.interpret(context);
}
@Override
public String toString() {
return "(" + left + " AND " + right + ")";
}
}
public class OrExpression implements Expression {
private final Expression left;
private final Expression right;
public OrExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public boolean interpret(RuleContext context) {
return left.interpret(context) || right.interpret(context);
}
@Override
public String toString() {
return "(" + left + " OR " + right + ")";
}
}
public class NotExpression implements Expression {
private final Expression expression;
public NotExpression(Expression expression) {
this.expression = expression;
}
@Override
public boolean interpret(RuleContext context) {
return !expression.interpret(context);
}
@Override
public String toString() {
return "NOT(" + expression + ")";
}
}
// Usage — Business Rules Engine
public class Main {
public static void main(String[] args) {
// Rule: (age > 18 AND country == "US") OR role == "admin"
Expression rule = new OrExpression(
new AndExpression(
new GreaterThanExpression("age", 18),
new EqualsExpression("country", "US")
),
new EqualsExpression("role", "admin")
);
System.out.println("Rule: " + rule);
// Test with different contexts
RuleContext adultUS = new RuleContext();
adultUS.setVariable("age", 25);
adultUS.setVariable("country", "US");
adultUS.setVariable("role", "user");
System.out.println("Adult US user: " + rule.interpret(adultUS)); // true
RuleContext teenUK = new RuleContext();
teenUK.setVariable("age", 16);
teenUK.setVariable("country", "UK");
teenUK.setVariable("role", "user");
System.out.println("Teen UK user: " + rule.interpret(teenUK)); // false
RuleContext adminTeen = new RuleContext();
adminTeen.setVariable("age", 15);
adminTeen.setVariable("country", "JP");
adminTeen.setVariable("role", "admin");
System.out.println("Admin teen: " + rule.interpret(adminTeen)); // true
}
}
// Expression interface for math
public interface MathExpression {
double interpret();
}
// Terminal — Number literal
public class NumberExpression implements MathExpression {
private final double number;
public NumberExpression(double number) {
this.number = number;
}
@Override
public double interpret() { return number; }
@Override
public String toString() { return String.valueOf(number); }
}
// Non-Terminal — Binary operations
public class AddExpression implements MathExpression {
private final MathExpression left, right;
public AddExpression(MathExpression left, MathExpression right) {
this.left = left;
this.right = right;
}
@Override
public double interpret() {
return left.interpret() + right.interpret();
}
}
public class MultiplyExpression implements MathExpression {
private final MathExpression left, right;
public MultiplyExpression(MathExpression left, MathExpression right) {
this.left = left;
this.right = right;
}
@Override
public double interpret() {
return left.interpret() * right.interpret();
}
}
public class SubtractExpression implements MathExpression {
private final MathExpression left, right;
public SubtractExpression(MathExpression left, MathExpression right) {
this.left = left;
this.right = right;
}
@Override
public double interpret() {
return left.interpret() - right.interpret();
}
}
// Simple parser — builds AST from postfix notation
public class PostfixParser {
public static MathExpression parse(String expression) {
Deque<MathExpression> stack = new ArrayDeque<>();
String[] tokens = expression.split("\\s+");
for (String token : tokens) {
switch (token) {
case "+" -> {
MathExpression right = stack.pop();
MathExpression left = stack.pop();
stack.push(new AddExpression(left, right));
}
case "*" -> {
MathExpression right = stack.pop();
MathExpression left = stack.pop();
stack.push(new MultiplyExpression(left, right));
}
case "-" -> {
MathExpression right = stack.pop();
MathExpression left = stack.pop();
stack.push(new SubtractExpression(left, right));
}
default -> stack.push(new NumberExpression(Double.parseDouble(token)));
}
}
return stack.pop();
}
}
// Usage
// "3 4 + 2 *" = (3 + 4) * 2 = 14
MathExpression expr = PostfixParser.parse("3 4 + 2 *");
System.out.println("Result: " + expr.interpret()); // 14.0
🎯 When to Use
- When you have a simple grammar that can be represented as a class hierarchy
- When efficiency is not a primary concern (interpreter is not fast for complex grammars)
- When you need to evaluate rules, expressions, or queries dynamically
- When the grammar is stable but new sentences/expressions are added frequently
- When you want to build DSLs (Domain-Specific Languages) for configuration or business rules
🏭 Real-World Examples
| Framework/Library | Usage |
|---|---|
java.util.regex.Pattern | Regex is interpreted against input strings |
| Spring Expression Language (SpEL) | #{expression} evaluated at runtime |
| SQL parsers | SQL statements parsed into AST and interpreted |
| JEXL (Apache Commons) | Expression language for Java |
| Drools Rule Engine | Business rules interpreted at runtime |
| ANTLR | Parser generator that builds interpreter ASTs |
javax.el.ExpressionFactory | JSP/JSF expression language |
⚠️ Pitfalls
Common Mistakes
- Complex grammars — Interpreter pattern becomes unwieldy for grammars with many rules. Use parser generators (ANTLR) instead.
- Performance — Tree traversal for every interpretation is slow. For hot paths, compile to bytecode or use caching.
- Class explosion — Every grammar rule becomes a class. 20+ rule types means 20+ classes.
- No error handling — Simple interpreters often lack good error messages. Add validation before interpretation.
- Reinventing the wheel — For math, regex, SQL, or JSON path — use existing libraries. Only build custom interpreters for your own DSLs.
📝 Key Takeaways
Summary
- Interpreter maps grammar rules to a class hierarchy forming an Abstract Syntax Tree (AST)
- Terminal expressions are leaves (literals, variables); Non-terminal expressions are composites (AND, OR, +, *)
- Best for simple, stable grammars where new sentences are common but new rules are rare
- The pattern naturally combines with Composite (tree structure) and Iterator (traversal)
- For complex languages, use parser generators (ANTLR, JavaCC) — Interpreter is for simple DSLs
- Modern alternative: Java's Pattern Matching and sealed interfaces can simplify expression hierarchies