Skip to content
DevPedia

Behavioral patternsGuide 19 of 28

Interpreter

How to represent the rules of a simple language as objects and combine them to interpret expressions.

Updated 9 min read

Interpreter is a behavioral pattern that represents the grammar of a small language as a class hierarchy, where each rule is a class that knows how to evaluate itself.

The problem

AndesShop wants the marketing team to build its own promotions without asking anyone for a deploy. The rules they need look like this:

total > 50000 AND category = "trekking"
loyalty_member OR total > 100000
NOT uses_coupon AND units >= 3

The first version is usually an improvised interpreter: a giant if that parses the string every time, or worse, an expression stored in a database and evaluated with eval. Both break as soon as the rule gets complicated, and the second is a security hole.

The underlying problem is that there’s a language — small, but a language nonetheless — with a grammar that repeats: comparisons, boolean conditions, and nested combinations of both. And the structure of that language is recursive: A AND (B OR C) has the same shape as A, only composed.

The solution

Interpreter proposes modeling each grammar rule as a class with a single evaluation method, and composing them into a tree. A composite expression evaluates its children and combines the results; a terminal expression reads a piece of data from the context and returns a value.

The structure of Interpreter: terminal and non-terminal expressions share the same interface, and the non-terminal ones contain other expressions.

Example in Java

// Abstract Expression: every rule knows how to evaluate itself against a cart
interface PromotionRule {
    boolean interpret(Cart cart);
}

// Terminal expressions: they read a piece of the context and decide
record MinimumTotalRule(Money threshold) implements PromotionRule {
    @Override
    public boolean interpret(Cart cart) {
        return cart.total().isGreaterThan(threshold);
    }
}

record CategoryRule(String category) implements PromotionRule {
    @Override
    public boolean interpret(Cart cart) {
        return cart.lines().stream()
                .anyMatch(line -> line.category().equals(category));
    }
}

// Non-terminal expressions: they combine other rules
record AndRule(PromotionRule left, PromotionRule right) implements PromotionRule {
    @Override
    public boolean interpret(Cart cart) {
        return left.interpret(cart) && right.interpret(cart);
    }
}

record OrRule(PromotionRule left, PromotionRule right) implements PromotionRule {
    @Override
    public boolean interpret(Cart cart) {
        return left.interpret(cart) || right.interpret(cart);
    }
}

record NotRule(PromotionRule inner) implements PromotionRule {
    @Override
    public boolean interpret(Cart cart) {
        return !inner.interpret(cart);
    }
}
// Client code: "total > $50,000 AND there's something from trekking, or the customer is a loyalty member"
PromotionRule freeShipping = new OrRule(
    new AndRule(
        new MinimumTotalRule(Money.ars(50_000)),
        new CategoryRule("trekking")
    ),
    new LoyaltyTierRule(Tier.GOLD)
);

boolean applies = freeShipping.interpret(cart);

The object tree and the expression’s syntax tree are the same thing. Adding a new operator — XOR, or a BetweenDatesRule — means adding a class, without touching the ones that already work.

When to use it

  • When you have a small and stable language or rule set, with a grammar you can write in a few productions.
  • When efficiency isn’t the priority and what matters is that the grammar stays explicit and can be extended without touching the rules that already work.
  • When the people defining the rules aren’t the people deploying the system: configurable business rules, search filters, access policies, declarative validations.

When to avoid it

Interpreter is the pattern in the catalog most often reached for when it isn’t needed. Three signs:

  • The grammar is large or going to grow. Each grammar rule means at least one class; a real grammar with twenty productions becomes unmanageable. That’s what parser generators (ANTLR and company) exist for, and GoF says so explicitly.
  • Two or three fixed conditions are enough. If there are three possible promotions and the same team that writes the code defines them, one Strategy per promotion is simpler and more direct.
  • Performance matters. Interpreting an object tree on every evaluation is noticeably slower than compiling the rule once.

Benefits and drawbacks

BenefitsDrawbacks
The grammar stays explicit in the code: each rule is a named classOne class per grammar rule; it scales badly as the grammar grows
Adding a new operator doesn’t modify the existing ones (Open/Closed)Evaluating an object tree is slow compared to a compiled expression
It bounds what can be expressed, which makes it safe for rules of external originIt doesn’t solve parsing, which almost always has to be written separately

Relationship with other patterns

  • Composite is the structure it rests on: the syntax tree is a composite where the non-terminal expressions are the nodes and the terminal ones the leaves.
  • Visitor lets you add new operations over the tree — printing it, optimizing it, validating it — without touching each expression class.
  • Flyweight helps share the terminal expressions that repeat many times within the same tree.
  • Iterator lets you traverse the tree without exposing its shape.
  • It gets confused with Strategy, and the difference is one of scale: Strategy swaps a complete algorithm; Interpreter composes a behavior out of grammatical pieces. If the possible rules are a closed set, it’s Strategy; if they combine with each other in ways you can’t enumerate in advance, it’s Interpreter.

Share this guide

Search by concept, pattern or practice.