Free Interactive Course · Design Patterns

Interpreter Design Pattern

Give a small language its own grammar and evaluator — and know exactly when to stop and reach for a real parser.

Behavioural Patternsharda.k.a. Little Language
ShareXLinkedIn
In one sentence

Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.

01

The problem Interpreter solves

The problem

Your monitoring product lets customers write alert rules. Version one supported one condition per rule, stored as three columns: metric, operator, threshold. cpu > 90. Simple.

Then customers wanted cpu > 90 AND region == "eu-west", so you added a second set of columns and an and_or flag. Then they wanted three conditions, so you added a third. Then they wanted (cpu > 90 OR memory > 85) AND NOT maintenance, and there is no number of columns that expresses brackets.

The tempting shortcut is to store the rule as a string and call eval() on it. That works on the first afternoon and hands every customer the ability to run arbitrary code inside your alerting service. What you actually need is a small language: a grammar you define, a representation you control, and an evaluator that can do precisely what you allow and nothing else.

02

How the Interpreter pattern works

Give every rule in the grammar its own class, and let them nest into a tree that evaluates itself:

  1. Write the grammar down first. expr := comparison | expr AND expr | expr OR expr | NOT expr | "(" expr ")".
  2. Each rule becomes a class implementing one method: interpret(context). Terminal expressions (a metric, a literal) return a value; non-terminal ones (And, Or, Not) hold sub-expressions and combine their results.
  3. A sentence in the language becomes a tree of those objects — an abstract syntax tree — which is a Composite with an evaluate method.
  4. Evaluating is a post-order walk: leaves produce values, branches combine them, and the root returns the answer. The context carries whatever the expression needs to look things up.
Parsing is not part of this pattern, and conflating the two is the usual mistake. Interpreter describes only the tree and its evaluation. Turning "cpu > 90 AND region == 'eu-west'" into that tree is a separate job, and GoF says so explicitly. Keeping them separate is what lets you validate a rule once at save time and evaluate it a million times, cache the parsed tree, render it back as text for the UI, and swap a hand-written parser for a generated one without touching evaluation.
«interface» Expressioninterpret(ctx) : ValueAndGreaterThanEqualsMetric("cpu")Literal(90)Tag("region")Literal("eu-west")green leaves are terminals; violet branches combine their children's values
Participants. The Abstract Expression declares interpret(context). Terminal Expressions (green) read a value from the context or return a literal. Non-terminal Expressions hold children and combine their results. The Context carries the data a sentence is evaluated against — here, one metric sample with its tags.
03

See it: evaluate the tree

One rule, evaluated against one metric sample. Step through it — the leaves resolve first, then each branch combines what its children returned, and the root produces the answer that decides whether anyone gets paged.

▶ Try it — walk the syntax tree

An interactive expression evaluator: the alert rule (cpu > 90 AND region == "eu-west") is a tree of Expression objects, evaluated leaf-first — Metric and Literal terminals resolve to values, GreaterThan and Equals compare them, and the And node combines the two booleans into the final result.

Two things this makes concrete. The tree is a Composite — And holds expressions and is an expression, which is why nesting brackets costs nothing. And a real implementation would short-circuit: if GreaterThan had returned false, And should never evaluate its right-hand side at all. That isn't an optimisation you can add later if the right side has side effects or costs a query.
04

Interpreter pattern code examples

A tiny alert-rule language: an AST that evaluates itself, with short-circuiting and no eval anywhere.

public interface Expression {
    Value interpret(Context context);
}

/** Terminal: reads from the context. */
public record Metric(String name) implements Expression {
    public Value interpret(Context ctx) { return Value.of(ctx.metric(name)); }
}

/** Terminal: a constant. */
public record Literal(Value value) implements Expression {
    public Value interpret(Context ctx) { return value; }
}

public record GreaterThan(Expression left, Expression right) implements Expression {
    public Value interpret(Context ctx) {
        return Value.of(left.interpret(ctx).asNumber() > right.interpret(ctx).asNumber());
    }
}

public record And(Expression left, Expression right) implements Expression {
    public Value interpret(Context ctx) {
        // Short-circuit: the right side may be an expensive lookup, and it must
        // not run when the left side already settled the answer.
        if (!left.interpret(ctx).asBoolean()) return Value.FALSE;
        return Value.of(right.interpret(ctx).asBoolean());
    }
}

// cpu > 90 AND region == "eu-west"
Expression rule = new And(
        new GreaterThan(new Metric("cpu"), new Literal(Value.of(90))),
        new Equals(new Tag("region"), new Literal(Value.of("eu-west"))));

boolean fires = rule.interpret(new Context(sample)).asBoolean();

// Parse ONCE at save time, evaluate millions of times. The tree is immutable
// and thread-safe, so it can be cached and shared across every evaluation.
Read across the tabs: the pattern is small and the surrounding decisions are not. Python's tab carries the one that matters most — never reach for eval(); there is no sanitisation that makes it safe, whereas an interpreter can only do what you gave it classes for. C#'s shows the industrial version, System.Linq.Expressions, where a LINQ provider walks the same kind of tree and emits SQL, with .Compile() as the escape hatch when interpreting is too slow. Go's (Value, error) forces you to decide what a missing metric means, instead of letting it quietly become false. And TypeScript's union gives you a grammar that the compiler checks every evaluator against.
05

How to implement Interpreter

  1. Write the grammar down before any code. If you can't write it in a dozen lines, this pattern is the wrong tool — use a parser generator.
  2. Define one type per grammar rule, with a single interpret(context) method. Terminals read or return values; non-terminals combine children.
  3. Keep parsing separate from evaluation. Parse once, validate at save time, evaluate many times.
  4. Make the tree immutable so it can be cached and shared across threads and evaluations.
  5. Implement short-circuiting in And/Or deliberately — it's semantics, not an optimisation, when the other side is expensive or has side effects.
  6. Decide what missing and wrong type mean, and make them explicit. Silently coercing an absent metric to false is how an alert stops firing unnoticed.
  7. Cap nesting depth at parse time, and never evaluate a tree from an untrusted source without one — deep recursion is a denial of service.
  8. Never use eval. The safety of this pattern is that it can only do what you implemented.
06

When to use Interpreter — and when not to

Use it when you have a simple grammar that recurs — alert rules, feature-flag conditions, search filters, permission expressions, pricing rules, spreadsheet formulas — and when efficiency is not the first concern. GoF is unusually direct that the pattern suits simple grammars and that a complex one calls for a parser generator instead.

Where it goes wrong

The grammar keeps growing. One class per rule is fine for a dozen rules and unmanageable at fifty. A real language needs a real parser — ANTLR, a PEG library, or a hand-written recursive-descent parser with a proper AST. Recognising the moment to switch is the skill.

Performance. Walking a tree of small objects is a virtual call and a cache miss per node. For rules evaluated millions of times a second, compile the tree — to a closure, to bytecode, or via Expression.Compile().

Reaching for eval() instead. The most dangerous shortcut in this entire catalogue. It converts a rule editor into remote code execution, and no amount of sanitising the string fixes it.

Unbounded recursion. A hostile or accidental deeply nested rule overflows the stack. Cap depth when parsing, not when evaluating.

No error model. A missing metric, a string compared to a number, a divide by zero — if these silently produce false, your alerting quietly stops working and nothing in the logs says so.

You want to…UseBecause
Evaluate a small, stable, custom language safelyInterpreterOne class per grammar rule; it can only do what you implemented.
Handle a real language with a large grammara parser generatorGoF says so explicitly — one class per rule doesn't scale to fifty rules.
Build the nested tree the interpreter walksCompositeAn AST is a composite; Interpreter adds evaluate() to it.
Add printing, optimising and type-checking over the same treeVisitorKeeps each operation in one class instead of adding methods to every node.
Share the many identical leaf nodes in a large treeFlyweightTerminals are immutable and repeat constantly — GoF suggests exactly this.
07

Quick check

🧠 Quick check
A colleague suggests storing alert rules as strings and evaluating them with eval(), arguing they'll strip dangerous keywords first. What's wrong with that?

In the wild

C#System.Linq.Expressions — an IQueryable provider interprets the expression tree into SQL, and Expression.Compile() turns it into IL when interpretation is too slow.
Javajava.util.regex.Pattern compiles a pattern into a node tree that matches text; the Java Expression Language in Jakarta EE is the same idea for templates.
PythonThe ast module, and SQLAlchemy's expression language, where Python operators build a tree that a dialect renders as SQL rather than evaluating in the process.
Gotext/template's action language and go/ast with go/types — the standard library interpreting trees it also defines.
JavaScriptJSONLogic and the query trees behind visual filter builders — the rule is data the UI, the audit log and the evaluator all share.
C++Boost.Spirit for the parser half, and std::regex's compiled matcher for the evaluating half.

Frequently asked questions

When should I use the Interpreter pattern instead of a parser generator?
Use Interpreter when the grammar is small, stable and yours — alert rules, feature-flag conditions, permission expressions, a filter language. A dozen rule types is comfortable. Switch to a parser generator (ANTLR, a PEG library) or a hand-written recursive-descent parser when the grammar grows past what one class per rule can express readably, when you need decent error messages with positions, or when you need operator precedence and associativity handled properly. GoF is explicit that this pattern is for simple grammars.
Is the Interpreter pattern the same as writing a parser?
No, and keeping them apart is the single most useful thing to know about it. Parsing turns text into a tree; interpreting walks that tree and produces a value. The pattern describes only the second half. Separating them lets you validate a rule once when it's saved, cache the parsed tree, evaluate it a million times, render it back into the UI, and replace the parser later without touching evaluation.
Why not just use eval() for a small rule language?
Because eval gives the rule's author everything your process can do — read files, open sockets, exfiltrate credentials — and every attempt at sandboxing it has been broken repeatedly. A blocklist has to catch every escape; the attacker needs one. An interpreter is a different kind of thing entirely: its node types are a complete enumeration of what a rule can express, so dangerous behaviour isn't filtered out, it was never built. The same argument applies to internal-only rule editors, which usually run with your service's full credentials.
How do I make an interpreter fast enough?
First, parse once and cache the tree — most systems that feel slow are re-parsing on every evaluation. Then make the tree immutable so it can be shared across threads without copying. If it's still too slow, compile rather than interpret: turn each node into a closure once (closure compilation, often a 5–10× win for very little code), or use a real compiler like Expression.Compile() in .NET to emit IL. Short-circuiting also matters more than it looks when a branch triggers a database lookup.
How should an interpreter handle missing values and type errors?
Deliberately and visibly. The tempting default — treat a missing metric as false, coerce a string to a number — makes an alert rule quietly stop firing, which is the worst possible failure for a monitoring system because nothing looks broken. Model the outcome as a result type or an error return (Go's tab shows this well), decide explicitly whether unknown propagates through AND and OR, and surface evaluation errors to whoever owns the rule rather than swallowing them.

Finish the handbook, earn the certificate

All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.

See all 23 patterns →
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.