Skip to content

Interpreter

Representing a small language as a tree of objects you evaluate — where a rules DSL is worth building, and the security and performance traps.

Advanced18 minUpdated 8 August 2026

Interpreter represents the grammar of a small language as a tree of objects, each of which knows how to evaluate itself. It is the least used pattern in the catalogue and the one whose decision — should this be a language at all? — has the largest consequences.

Most attempts fail not because the pattern is hard but because the language was a bad idea. This topic spends most of its time on that question.

The shape

A pricing rules language: amount > 100 AND customer.tier = "gold".

class Comparison {
  constructor(field, op, value) { /* … */ }
  evaluate(context) {
    const actual = context.get(this.field);
    switch (this.op) {
      case '>': return actual > this.value;
      case '=': return actual === this.value;
    }
  }
}

class And {
  constructor(left, right) { this.left = left; this.right = right; }
  evaluate(context) { return this.left.evaluate(context) && this.right.evaluate(context); }
}
Conceptual diagramAndComparisonamount > 100Comparisoncustomer.tier = goldContextamount, customer
Diagram, as text
  • And leads to Comparison amount > 100
  • And leads to Comparison customer.tier = gold
  • Comparison amount > 100 leads to Context amount, customer
  • Comparison customer.tier = gold leads to Context amount, customer

Each node evaluates itself and its children — Composite applied to a grammar. The pattern covers evaluation only; producing the tree from text is parsing, which the pattern says nothing about and which is most of the work.

When a small language is worth it

The bar is high, and three conditions should all hold.

The rules change far more often than the code. If pricing rules change weekly and deployments are fortnightly, the rules should not be in the deployment.

Someone other than an engineer needs to author or read them. This is the strongest justification and it needs testing rather than assuming — "the business team will write rules" is a prediction that is often wrong, and if only engineers ever touch them, a plain function was better.

The rule space is genuinely bounded. A language that only expresses comparisons and boolean combinations is reviewable and safe. One that grows loops, variables and function calls has become a programming language, and you now maintain a programming language.

If any of the three fails, write the rules as ordinary code and give them a good structure — Strategy or a small dispatch table.

What the pattern does not give you

Parsing. Turning text into a tree is a separate and larger job. For a small grammar a hand-written recursive-descent parser is fine; beyond that, use a parser generator. Many teams skip text entirely and let rules be authored as structured data — JSON or a form UI producing the tree directly. That is usually the right call: it removes the parser, removes syntax errors, and makes the rules editable in a UI.

Error reporting. A rule that fails must say why, with a position. Retrofitting this is painful, so nodes should carry their source location from the start.

Performance. A tree walk allocates and dispatches per node. For thousands of evaluations per second over a small tree that is irrelevant; for millions it is not, and the answer is to compile the tree into a closure or a bytecode form once rather than to walk it repeatedly.

Safety limits. A grammar with recursion needs a depth cap; one with iteration needs a step budget; anything user-supplied needs a timeout. A rules engine without limits is a denial-of-service surface.

Types and validation

The most common production failure is not a crash but a rule that is quietly wrong: comparing a string field to a number, referencing a field that no longer exists, or testing a nullable value without handling null.

Validate at authoring time, against a schema of the available fields and their types. A rule that references customer.tier should fail to save if that field does not exist or is not comparable to a string. Catching this when the rule is written — where there is a human to tell — is worth far more than catching it at evaluation, where the usual outcome is a silently skipped discount.

Check your understanding

Common mistakes

Building a language when a function would do. If only engineers author the rules, the language is overhead.

Using the host language's eval. It discards the pattern's only real safety property.

Letting the grammar grow. Loops, variables and function calls mean you now maintain a programming language.

No source positions on nodes. Error messages cannot say where, and retrofitting is painful.

No validation at authoring time. Broken rules are discovered as silently wrong output in production.

No limits. Recursion depth, step budgets and timeouts are required for anything user-supplied.

Walking the tree on a hot path. Compile once to a closure or bytecode rather than re-walking per evaluation.

Practice

A product team asks for a rules engine so that non-engineers can define promotion eligibility. Decide whether to build one, and design what you would build.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Test the assumption in the request before designing around it.

Show a strong answer

Test the premise first, because the request contains an assumption that is often wrong. "Non-engineers will define promotions" is a prediction. I would ask for the last twenty promotions and find out who actually specified them, in what form, and how often they changed after launch. Two outcomes:

  • If a marketing manager wrote them in a spreadsheet and an engineer transcribed them, the request is real and the transcription step is the waste worth removing.
  • If an engineer designed them from a one-line brief, the language will be built, used by engineers, and be strictly worse than a function. That is the common outcome and it is worth an hour of investigation to avoid.

Assuming the first case, what I would build:

  • Structured rules, not text. A JSON representation authored through a form UI: a field picker populated from the schema, an operator list constrained by the field's type, and a value input validated against it. This removes the parser entirely, removes syntax errors as a category, and makes a wrong rule mostly unexpressible rather than merely detectable.
  • A deliberately small grammar: comparisons over a fixed set of exposed fields, plus AND/OR/NOT. No arithmetic, no loops, no field-to-field comparison until someone demonstrates a real need. Every extension is a permanent maintenance commitment, so the default answer is no.
  • Evaluation as a tree walk over that structure — Interpreter proper — with the tree built directly from the stored JSON.

The parts that matter more than the evaluator:

  • A preview against real data. Before saving, show how many of last month's orders would have matched. This catches the rule that is syntactically fine and means something different from what the author intended, which is the failure mode that actually costs money.
  • Validation at save time against the field schema, so a renamed or removed field breaks at authoring rather than silently matching nothing.
  • Versioning and an audit trail. Who changed which rule, when, and what the previous version was. Promotions have financial consequences and someone will need to answer that question.
  • A kill switch per rule, because the first bad promotion needs to be stopped in seconds, not in a deploy.

Safety limits are easy here and I would still set them: a cap on rules evaluated per order and a bound on nesting depth, so a pathological rule set cannot degrade checkout.

What I would explicitly not build: a text syntax. It is the part everyone imagines when they hear "rules engine" and the part that adds a parser, an editor, syntax errors and a support burden. If authors are later demonstrably constrained by the form, that is the moment to add it — and by then the grammar's requirements are known rather than guessed.

An existing rules engine evaluates 50,000 rules per request by walking each rule's tree, and it has become the bottleneck. Propose optimisations in the order you would attempt them.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Before making evaluation faster, ask how many rules genuinely need evaluating.

Show a strong answer

First: reduce the number of rules evaluated, because this dominates everything else. Fifty thousand rules per request is almost never fifty thousand applicable rules.

  • Index rules by their most selective condition. Most promotion rules are scoped — to a product category, a customer segment, a region. Indexing on that and fetching only candidates typically turns 50,000 into dozens. This is a data-structure change, not an evaluation change, and it is usually worth two orders of magnitude where the other options are worth one.
  • Partition by rule type, so a request only considers rules that could possibly apply to it.
  • Short-circuit on the cheapest condition. Reorder each rule's conditions so the most selective and cheapest runs first — most rules should be rejected on their first comparison.

Second: make each evaluation cheaper, once the count is as low as it will go.

  • Compile the tree once into a closure at load time rather than walking nodes per evaluation. This removes the per-node dispatch and allocation and is usually a solid improvement with a contained change — the tree remains the authoring and storage format, and compilation happens on load.
  • Hoist the context lookups. If every rule reads customer.tier, resolve it once per request rather than per rule.
  • Cache per-request derived values that several rules compute independently.

Third: change when the work happens.

  • Precompute eligibility for rules whose conditions depend only on slowly changing data — customer segment, product category. That work does not belong in the request at all.
  • Evaluate expensive but non-blocking rules asynchronously, if the product can tolerate a promotion appearing a moment later.

Last resort: bytecode or a discrimination network. Compiling to a compact instruction form, or restructuring into a network that shares common sub-conditions across rules, is a real technique for large rule sets and a significant piece of work. I would only reach for it having exhausted the above, because it is a large increase in complexity for a system that non-engineers are supposed to be able to reason about.

What I would measure before and after each step: rules evaluated per request, and time per request at the tail rather than the mean. It is entirely possible that indexing alone ends the project, and continuing past that point would be optimising something that is no longer the bottleneck.

One thing I would check early: whether all 50,000 rules are live. Rule sets accumulate, and expired or disabled promotions are frequently still being evaluated. That is sometimes the entire fix.

Key takeaways

  • Interpreter represents a grammar as a tree of objects that evaluate themselves — Composite applied to a language.
  • It covers evaluation only. Parsing text into the tree is a separate, usually larger job.
  • Build a language only when rules change faster than code, non-engineers genuinely author them, and the rule space is bounded. Test the second assumption rather than believing it.
  • Prefer structured data to text. It removes the parser, removes syntax errors, and enables a UI, validation and diffing.
  • Never use the host language's eval. A restricted grammar's safety comes from things being unrepresentable, not blocked.
  • Validate at authoring time against a field schema — that is where a human can act on the feedback.
  • Set depth caps, step budgets and timeouts for anything user-supplied.
  • For performance, reduce the rules evaluated before making evaluation faster, then compile the tree once instead of walking it.

Further reading

  • Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
  • Martin Fowler, Domain-Specific Languages (Addison-Wesley, 2010) — internal and external DSLs, and when each is warranted.
  • Terence Parr, Language Implementation Patterns (Pragmatic Bookshelf, 2009).
  • Robert Nystrom, Crafting Interpreters (Genever Benning, 2021) — tree-walking versus bytecode interpretation.
  • Charles Forgy, "Rete: A Fast Algorithm for the Many Pattern/Many Object Pattern Match Problem" (Artificial Intelligence, 1982).

Check yourself

3 questions in this lesson. Answer each once to complete it.

Practice this

Next lessonProducer–Consumer
Lessons in this track