skip to content

You expose a small rule language to customers whose rules are evaluated per request. What do you have to get right beyond the textbook Interpreter structure?

level: principalimportance: nice to knowfreq 9%

answer

  1. validate at save, not in the hot path
  2. cache compiled tree by content hash, bounded
  3. immutable nodes + per-call context = parallel-safe
  4. depth cap, step budget, deadline, no I/O, function allow-list
  5. stored rules = versioned data; explain mode + kill switch

basics

~20 s

Validate rules when they are saved, cache the parsed tree instead of re-parsing per request, keep nodes immutable so evaluation is thread-safe, and limit untrusted rules with depth, step, and time budgets so no rule can hang or crash the service.

solid answer

~50 s

Textbook Interpreter covers evaluation only; running customer-authored rules in production adds five concerns. **Validation at authoring time**: parse, type-check, and check referenced fields when the rule is saved, so failures surface in the UI rather than in the hot path. **Caching**: parsing and compiling are expensive relative to evaluating, so cache the compiled tree keyed by rule content hash with a bounded, evicting cache. **Immutability and concurrency**: nodes hold structure only, per-evaluation state lives in the context, so a cached tree is safely shared. **Resource safety**: untrusted rules need a parse-depth cap (recursion can blow the stack), a step/fuel budget or deadline in the context, no I/O or side effects inside nodes, and a whitelist of pure functions — otherwise a rule is a denial-of-service vector. **Evolution**: rules are persisted data, so grammar changes need versioning, a migration story, and a deprecation path. Add observability — which rules fired, evaluation latency percentiles, per-rule error rates — plus a kill switch for a misbehaving rule.

go deeper

for a junior

Mention validating rules before saving them and reusing the parsed tree instead of re-parsing every time.

for a middle

Add immutability for thread safety, caching keyed on rule content, and basic limits on rule size and nesting.

for a senior

Cover the full pipeline: authoring-time validation with good diagnostics, compiled-rule cache, step budget and deadline, no side effects in nodes, per-rule metrics.

for a principal

Frame it as owning a language product: sandbox threat model, grammar versioning and migration of persisted rules, explain mode and kill switch for support, cost at scale, and the build-versus-buy decision against existing engines.

## Why textbook Interpreter is not the whole system The pattern answers "how do I represent and evaluate a sentence?" A production rule engine also answers: who writes rules, when are they checked, how fast is evaluation, what stops a malicious rule, and what happens when the grammar changes under thousands of stored rules. ## 1. Shift validation left (authoring time, not evaluation time) Run the full pipeline when a rule is **saved**: lex/parse → build AST → type-check (a Visitor: comparing a date to a string is an error) → resolve schema references (does field `order.totl` exist?) → static limits (node count, nesting depth) → optional dry run against sample data. Return precise diagnostics with line/column. A rule that reaches evaluation should already be known-good; runtime errors then reduce to genuinely dynamic cases (null field, division by zero) with a documented policy — fail the rule, treat as no-match, or fail the request. ## 2. Cache compiled rules Parsing dominates evaluation for small trees, so per-request parsing is pure waste. Cache keyed by a **content hash** of the rule text (not the rule ID — IDs mutate when edited). Use a bounded LRU/size-aware cache; unbounded caches keyed by user input are themselves a memory-exhaustion vector. If you go further, compile the AST once into closures or bytecode and cache *that*. ## 3. Immutability, reentrancy, thread safety Nodes hold children and literals only. Everything per-evaluation — subject, bindings, step counter, output — lives in the context created per call. Then a single cached tree can be evaluated in parallel across request threads with zero synchronization. Any mutable node field (a memoized value, a "last result") silently breaks this and produces flaky, load-dependent bugs. ## 4. Resource safety for untrusted rules User-supplied rules are **untrusted input that you execute** — treat them as such: - **Depth limit at parse time**: a chain of thousands of operators would recurse the evaluator into a stack overflow, which typically kills the thread and can destabilize the process. Cap nesting and node count before you ever build the tree. - **Step budget / fuel**: keep a counter in the context, decrement per node evaluated, abort past the budget. Cheap and effective against pathological expressions (repeated string concatenation, nested loops if the language has them). - **Deadline**: a wall-clock check for anything that can block. - **No side effects**: nodes must not perform I/O, database, or network calls. Pre-load everything the rule can reference into the context. Otherwise one rule becomes an SSRF or an N+1 amplifier. - **Function whitelist**: if the language has functions, expose an explicit allow-list of pure functions; never reflection, file access, process spawning, or arbitrary method invocation. Many real incidents come from expression languages that quietly allow arbitrary method calls. - **Output bounds**: cap result size for anything that builds strings or collections. - **Memory**: cap intermediate collection sizes; a `cross join`-style construct in the language is an easy OOM. ## 5. Evolution and versioning Stored rules are **persisted data**, so the grammar becomes a compatibility surface: - Adding a construct is backward-compatible; removing or changing semantics is not. - Tag each stored rule with a grammar version; keep the ability to evaluate old versions or migrate them explicitly with a re-validation pass. - Renaming a referenced field breaks every rule using it — you need a dependency index from field to rules, or rules break silently at runtime. - Never change evaluation semantics of an existing operator in place; introduce a new operator. ## 6. Observability and operational control - Per-rule metrics: evaluation count, latency percentiles, error rate, budget-exceeded count, match rate. A rule that stops matching after a deploy is a real incident signal. - "Explain" mode — a Visitor that produces a trace of which subexpressions were true — makes customer support tractable. - A kill switch to disable a specific rule or version without a deploy. - Cardinality discipline: do not put raw rule text in metric labels. ## 7. Testing Unit-test each node type; property/fuzz-test the parser against depth and size limits; golden tests for compiled-form equivalence when you add a compiler; and a corpus of real customer rules replayed on every grammar change to catch semantic regressions. ## 8. Build-versus-buy Before owning a language: could the rules be translated into an existing engine (SQL `WHERE`, a search query, an established expression library or policy engine)? Owning a DSL means owning its parser, semantics, docs, diagnostics, sandbox, and migration path forever. That is often the right call for a core differentiator and the wrong call for a nice-to-have filter.

  • Why key the compiled-rule cache on a content hash instead of the rule's ID?
    Because a rule can be edited in place while keeping its ID; keying on the ID would serve a stale compiled tree after an edit. Hashing the rule text makes the key change exactly when the semantics change, and old entries age out naturally.
  • A customer's rule causes a stack overflow in production. What is the fix, and where does it belong?
    Cap nesting depth and node count at parse/validation time, before the tree exists, and reject the rule with a clear error at authoring time. Catching it during evaluation is too late — a stack overflow can leave the thread or process in an unreliable state. Optionally, switch to an explicit-stack evaluator so depth is bounded by heap rather than the call stack.
  • How do you evolve the grammar without breaking thousands of stored rules?
    Treat the grammar as a data contract: add constructs rather than changing existing ones, stamp each stored rule with a grammar version, keep a rules-by-referenced-field index so schema renames are detectable, and replay a corpus of real customer rules against every grammar change before shipping.

saying these in an interview costs you the question

  • Parsing the rule text on every request instead of caching the compiled tree
  • Allowing expression nodes to perform database or network calls during evaluation
  • Exposing arbitrary method invocation or reflection in the rule language
  • Relying on catching StackOverflow at evaluation time instead of capping depth at parse time
  • Treating stored rules as ephemeral rather than as versioned persisted data
  • Caching compiled rules in an unbounded map keyed by user-supplied text

context