Interpreter Pattern
Concept
Interpreter defines a small grammar for a domain-specific language and represents each rule of that grammar as its own class with an interpret() method. Simple expressions (terminal symbols — a literal number, a field name) and composite expressions (non-terminal symbols — AND, OR, +) both implement the same interface, so a complex expression is built by composing simple ones into a tree, and evaluating the whole tree just means calling interpret() on the root, which recursively calls interpret() on its children. It's the Composite pattern applied specifically to grammar: the tree structure is identical, but Interpreter's nodes each know how to evaluate, not just how to be traversed.
The pattern's classic textbook examples — a calculator that parses "3 + 4 * 2", a regex engine — are honest but slightly misleading, because nobody hand-rolls a production regex engine with GoF classes. The realistic case for Interpreter today is a small, stable, internal rules language: a pricing-rule evaluator ("if region == 'EU' and cart_total > 100 then discount = 0.1"), a permission-expression evaluator ("role == 'admin' or (role == 'editor' and resource.owner == user.id)"), or a search-filter query language — cases where the "grammar" is a handful of rule types that rarely grow, and representing them as composable objects beats an ever-expanding if/elif chain that re-parses the same conditions ad hoc at every call site.
Tradeoffs
| Approach | Benefit | Cost |
|---|---|---|
| Interpreter (grammar as composable classes) | Rules become data — composable, testable in isolation, and inspectable (you can print the tree); adding a new expression type doesn't touch existing ones | Class-per-grammar-rule causes real class proliferation once the grammar grows past a handful of rule types; still requires someone to write a parser that turns text into the expression tree, which Interpreter itself doesn't provide |
| Hardcoded conditional logic | No extra abstraction, fastest to write for a truly fixed, small rule set | Every new rule combination means editing and re-testing existing conditional code; rules can't be introspected, serialized, or built dynamically from user/admin input |
| Parser generator / existing rules-engine or scripting library (ANTLR, a JSON-logic library, embedding a scripting language) | Battle-tested parsing and evaluation, handles genuinely complex or evolving grammars without hand-rolled class trees | Adds a real dependency and a learning curve; overkill for a grammar with five rule types that never changes |
The pattern's own creators, and most modern treatments, are explicit that Interpreter doesn't scale to real programming-language-sized grammars — that's what parser generators and compiler-compiler tools exist for. Interpreter earns its place specifically in the gap between "too dynamic for hardcoded conditionals" and "too small to justify a parser-generator dependency."
When to use / when not to
- Use for a small, well-defined, relatively stable grammar that's evaluated repeatedly and benefits from being represented as data rather than procedural code — pricing rules, permission expressions, simple search-filter DSLs.
- Especially valuable when the rules need to be built or modified without a deploy — e.g. an admin UI that lets non-engineers compose filter conditions, where each condition maps directly to an expression object.
- Don't use it for expressions that are simple and unlikely to grow — a handful of boolean checks read more clearly as a plain conditional than as five classes each implementing
interpret(). - Don't use it for a grammar that's large, evolving, or genuinely complex — that's the point at which class-per-rule proliferation and hand-rolled recursive evaluation become a maintenance burden a parser generator or embedded scripting library avoids.
- Don't use it where performance under high call volume matters — recursive tree-walking interpretation is slower than a compiled or pre-optimized evaluation path, which matters if the expression runs on every request in a hot path.
Common pitfall
Reaching for Interpreter for a grammar that keeps growing past its original "five simple rules" scope, ending up with dozens of expression classes, a hand-rolled recursive-descent parser nobody fully understands, and no error recovery — essentially building an ad hoc, worse-tested compiler front end one class at a time. The moment a rules language needs real error messages, operator precedence beyond a couple of levels, or a formal spec, it's usually cheaper to adopt an existing parser generator or embed a small scripting language than to keep extending a hand-built Interpreter tree.
Engineering Lens
The useful design-review question isn't "did we use the Interpreter pattern" — it's "is this rules language actually staying small, and do we have a plan for the day it doesn't." Interpreter is a reasonable choice precisely because it's honest about that boundary: the pattern's own structure (one class per grammar rule) makes the cost of growth visible in the codebase itself, as an ever-longer list of expression classes, rather than hidden inside a single sprawling function. That visibility is the real value — it turns "is our rules engine getting too complex" from a subjective judgment call into something you can literally count.
Related
- Chain of Responsibility Pattern — both build a request-processing structure out of composable objects, but Interpreter's objects evaluate a grammar while Chain of Responsibility's route a request to one handler