Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work
Interpreter
Represent the rules of a small language as a tree of objects, and evaluate the tree. This is useful for filters, pricing rules and other mini-languages.
The problem
A marketing team wants to configure discount rules without a deploy, for example: "10% off if the cart total is over 100 and the customer is a member, or the country is NZ". Developers keep hard-coding each new rule, and every campaign waits for a release.
The rules are really a tiny language with comparisons, and, or and not. You need a way to represent and evaluate expressions in it.
Real-world analogy
Musical notation. A score is a structured "sentence" in a small language; a musician (the interpreter) reads each symbol and turns it into sound. The same musician can play any score written in that language.
Without the pattern
def eligible(cart):
# a new campaign = a new hard-coded condition and a new deploy
return (cart.total > 100 and cart.member) or cart.country == "NZ"
The pattern
- Abstract expression: an interface with
interpret(context). - Terminal expressions: leaves of the grammar, such as numbers, variables and comparisons (
GreaterThan("total", 100)). - Non-terminal expressions: rules made of other expressions (
And(left, right),Or,Not). They interpret their children and combine the results. - Context: the input being evaluated (the cart).
The expression tree is a Composite; Interpreter adds the meaning. A parser (separate from the pattern) usually turns text into the tree.
The code
The rule is built as a tree of expression objects and evaluated against three carts. A describe() method turns the tree back into readable text.
class Var: # terminal: reads a value from the context
def __init__(self, name):
self.name = name
def interpret(self, ctx):
return ctx[self.name]
def describe(self):
return self.name
class Const:
def __init__(self, value):
self.value = value
def interpret(self, ctx):
return self.value
def describe(self):
return f"'{self.value}'" if isinstance(self.value, str) else str(self.value)
class Gt:
def __init__(self, left, right):
self.left, self.right = left, right
def interpret(self, ctx):
return self.left.interpret(ctx) > self.right.interpret(ctx)
def describe(self):
return f"{self.left.describe()} > {self.right.describe()}"
class Eq(Gt):
def interpret(self, ctx):
return self.left.interpret(ctx) == self.right.interpret(ctx)
def describe(self):
return f"{self.left.describe()} == {self.right.describe()}"
class And: # non-terminal: combines other expressions
def __init__(self, left, right):
self.left, self.right = left, right
def interpret(self, ctx):
return self.left.interpret(ctx) and self.right.interpret(ctx)
def describe(self):
return f"({self.left.describe()} and {self.right.describe()})"
class Or(And):
def interpret(self, ctx):
return self.left.interpret(ctx) or self.right.interpret(ctx)
def describe(self):
return f"({self.left.describe()} or {self.right.describe()})"
rule = Or(And(Gt(Var("total"), Const(100)), Eq(Var("member"), Const("yes"))),
Eq(Var("country"), Const("NZ")))
print("rule:", rule.describe())
carts = [{"total": 150, "member": "yes", "country": "US"},
{"total": 150, "member": "no", "country": "US"},
{"total": 20, "member": "no", "country": "NZ"}]
for cart in carts:
print(cart["total"], cart["member"], cart["country"], "->", "discount" if rule.interpret(cart) else "no discount")
Output
rule: ((total > 100 and member == 'yes') or country == 'NZ') 150 yes US -> discount 150 no US -> no discount 20 no NZ -> discount
type Value = number | string | boolean;
type Ctx = Record<string, Value>;
interface Expr {
interpret(ctx: Ctx): Value;
describe(): string;
}
class Var implements Expr { // terminal: reads a value from the context
constructor(private name: string) {}
interpret(ctx: Ctx) { return ctx[this.name]; }
describe() { return this.name; }
}
class Const implements Expr {
constructor(private value: Value) {}
interpret() { return this.value; }
describe() { return typeof this.value === "string" ? `'${this.value}'` : String(this.value); }
}
class Gt implements Expr {
constructor(protected left: Expr, protected right: Expr) {}
interpret(ctx: Ctx): Value { return this.left.interpret(ctx) > this.right.interpret(ctx); }
describe() { return `${this.left.describe()} > ${this.right.describe()}`; }
}
class Eq extends Gt {
interpret(ctx: Ctx): Value { return this.left.interpret(ctx) === this.right.interpret(ctx); }
describe() { return `${this.left.describe()} == ${this.right.describe()}`; }
}
class And implements Expr { // non-terminal: combines other expressions
constructor(protected left: Expr, protected right: Expr) {}
interpret(ctx: Ctx): Value { return Boolean(this.left.interpret(ctx)) && Boolean(this.right.interpret(ctx)); }
describe() { return `(${this.left.describe()} and ${this.right.describe()})`; }
}
class Or extends And {
interpret(ctx: Ctx): Value { return Boolean(this.left.interpret(ctx)) || Boolean(this.right.interpret(ctx)); }
describe() { return `(${this.left.describe()} or ${this.right.describe()})`; }
}
const rule = new Or(new And(new Gt(new Var("total"), new Const(100)), new Eq(new Var("member"), new Const("yes"))),
new Eq(new Var("country"), new Const("NZ")));
console.log("rule:", rule.describe());
const carts: Ctx[] = [{ total: 150, member: "yes", country: "US" },
{ total: 150, member: "no", country: "US" },
{ total: 20, member: "no", country: "NZ" }];
for (const cart of carts) {
console.log(`${cart.total} ${cart.member} ${cart.country} -> ${rule.interpret(cart) ? "discount" : "no discount"}`);
}
Output
rule: ((total > 100 and member == 'yes') or country == 'NZ') 150 yes US -> discount 150 no US -> no discount 20 no NZ -> discount
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Expr rule = new Or(new And(new Gt(new Var("total"), new Const(100)), new Eq(new Var("member"), new Const("yes"))),
new Eq(new Var("country"), new Const("NZ")));
System.out.println("rule: " + rule.describe());
List<Map<String, Object>> carts = List.of(
Map.of("total", 150, "member", "yes", "country", "US"),
Map.of("total", 150, "member", "no", "country", "US"),
Map.of("total", 20, "member", "no", "country", "NZ"));
for (Map<String, Object> cart : carts) {
boolean ok = (Boolean) rule.interpret(cart);
System.out.println(cart.get("total") + " " + cart.get("member") + " " + cart.get("country") + " -> "
+ (ok ? "discount" : "no discount"));
}
}
}
interface Expr {
Object interpret(Map<String, Object> ctx);
String describe();
}
record Var(String name) implements Expr { // terminal: reads a value from the context
public Object interpret(Map<String, Object> ctx) { return ctx.get(name); }
public String describe() { return name; }
}
record Const(Object value) implements Expr {
public Object interpret(Map<String, Object> ctx) { return value; }
public String describe() { return value instanceof String ? "'" + value + "'" : String.valueOf(value); }
}
record Gt(Expr left, Expr right) implements Expr {
public Object interpret(Map<String, Object> ctx) { return (Integer) left.interpret(ctx) > (Integer) right.interpret(ctx); }
public String describe() { return left.describe() + " > " + right.describe(); }
}
record Eq(Expr left, Expr right) implements Expr {
public Object interpret(Map<String, Object> ctx) { return left.interpret(ctx).equals(right.interpret(ctx)); }
public String describe() { return left.describe() + " == " + right.describe(); }
}
record And(Expr left, Expr right) implements Expr { // non-terminal: combines other expressions
public Object interpret(Map<String, Object> ctx) { return (Boolean) left.interpret(ctx) && (Boolean) right.interpret(ctx); }
public String describe() { return "(" + left.describe() + " and " + right.describe() + ")"; }
}
record Or(Expr left, Expr right) implements Expr {
public Object interpret(Map<String, Object> ctx) { return (Boolean) left.interpret(ctx) || (Boolean) right.interpret(ctx); }
public String describe() { return "(" + left.describe() + " or " + right.describe() + ")"; }
}
Output
rule: ((total > 100 and member == 'yes') or country == 'NZ') 150 yes US -> discount 150 no US -> no discount 20 no NZ -> discount
Run this on your computer — see “Get Started” for setup.
<?php
interface Expr {
public function interpret(array $ctx): mixed;
public function describe(): string;
}
class VarExpr implements Expr { // terminal: reads a value from the context
public function __construct(private string $name) {}
public function interpret(array $ctx): mixed { return $ctx[$this->name]; }
public function describe(): string { return $this->name; }
}
class Constant implements Expr {
public function __construct(private mixed $value) {}
public function interpret(array $ctx): mixed { return $this->value; }
public function describe(): string { return is_string($this->value) ? "'{$this->value}'" : (string) $this->value; }
}
class Gt implements Expr {
public function __construct(protected Expr $left, protected Expr $right) {}
public function interpret(array $ctx): mixed { return $this->left->interpret($ctx) > $this->right->interpret($ctx); }
public function describe(): string { return "{$this->left->describe()} > {$this->right->describe()}"; }
}
class Eq extends Gt {
public function interpret(array $ctx): mixed { return $this->left->interpret($ctx) === $this->right->interpret($ctx); }
public function describe(): string { return "{$this->left->describe()} == {$this->right->describe()}"; }
}
class AndExpr implements Expr { // non-terminal: combines other expressions
public function __construct(protected Expr $left, protected Expr $right) {}
public function interpret(array $ctx): mixed { return $this->left->interpret($ctx) && $this->right->interpret($ctx); }
public function describe(): string { return "({$this->left->describe()} and {$this->right->describe()})"; }
}
class OrExpr extends AndExpr {
public function interpret(array $ctx): mixed { return $this->left->interpret($ctx) || $this->right->interpret($ctx); }
public function describe(): string { return "({$this->left->describe()} or {$this->right->describe()})"; }
}
$rule = new OrExpr(new AndExpr(new Gt(new VarExpr("total"), new Constant(100)), new Eq(new VarExpr("member"), new Constant("yes"))),
new Eq(new VarExpr("country"), new Constant("NZ")));
echo "rule: ", $rule->describe(), "\n";
$carts = [["total" => 150, "member" => "yes", "country" => "US"],
["total" => 150, "member" => "no", "country" => "US"],
["total" => 20, "member" => "no", "country" => "NZ"]];
foreach ($carts as $cart) {
$result = $rule->interpret($cart) ? "discount" : "no discount";
echo "{$cart['total']} {$cart['member']} {$cart['country']} -> $result\n";
}
Output
rule: ((total > 100 and member == 'yes') or country == 'NZ') 150 yes US -> discount 150 no US -> no discount 20 no NZ -> discount
In a real system the rule would come from a database or an admin screen as text, and a small parser would build this tree. The interpreter part, the tree evaluating itself, stays the same.
When to use it
- You have a small, stable language that users or configuration need to express: discount and pricing rules, search filters (
status:open author:ada), feature-flag targeting, alert conditions, permission policies. - The grammar is simple enough that one class per rule is manageable.
When not to use it
- The language is large or complex. Use a parser generator (ANTLR, Lark, PEG.js) or an existing engine instead.
- Performance matters for heavy evaluation. Tree-walking interpreters are slow compared with compiled code.
- You could use an existing safe expression language (JSONLogic, CEL, a SQL
WHEREclause) instead of inventing one. - Never interpret untrusted input with
eval(). A small interpreter with a fixed set of operations is the safe alternative.
You've already used it
- Regular expressions are a mini-language compiled to an internal tree or automaton.
- SQL
WHEREclauses, Elasticsearch query DSL and MongoDB query documents are expression trees evaluated by an engine. - Template engines (Jinja, Blade, Handlebars) parse templates into trees and interpret them.
- Feature-flag rules and policy engines (Open Policy Agent, AWS IAM policy evaluation).
Key points
- Interpreter represents each grammar rule as a class and evaluates a tree of expressions against a context.
- The tree is a Composite; a separate parser usually builds it from text.
- Use it for small, stable rule languages; use real parsers or existing expression engines for anything bigger.
- It is a safe alternative to
eval()for user-defined rules. - Further reading: Refactoring Guru: design pattern catalog and Interpreter pattern.
Practice questions
What does the Interpreter pattern do?
Answer
It represents a small language's grammar as classes: terminal expressions (numbers, variables) and non-terminal expressions (and, or, comparisons) that combine others. A sentence becomes a tree of these objects, and calling interpret(context) on the root evaluates it.
When should you not use the Interpreter pattern?
Answer
When the language is large or complex (use a parser generator or an existing engine), when evaluation performance is critical, or when an existing expression language (SQL, CEL, JSONLogic) already does the job.
Why is a small interpreter safer than `eval()` for user-defined rules?
Answer
eval() runs arbitrary code, so a malicious rule can do anything the program can. A small interpreter supports only a fixed set of operations (comparisons, and/or), so user input can't escape that sandbox.
Open in the app to mark lessons complete, take notes and track your progress.