Interpreter Design Pattern in C#: Complete Guide with Examples
When you need to evaluate expressions, parse grammars, or build a small domain-specific language inside your application, the interpreter design pattern in C# is the behavioral pattern designed for exactly that. It defines a representation for a language's grammar and provides an interpreter that uses that representation to process sentences in the language. Each rule in the grammar becomes a class, and evaluating an expression becomes a matter of walking a tree of those classes and asking each node for its result.
In this complete guide, we'll walk through everything you need to know about the interpreter pattern -- from the core participants and their roles to practical C# implementations covering arithmetic expressions, boolean logic, a simple rule engine, and expression trees that combine the interpreter with the composite design pattern. By the end, you'll have working code examples and a clear understanding of when this pattern fits your problem.
What Is the Interpreter Design Pattern?
The interpreter design pattern is a behavioral design pattern from the Gang of Four (GoF) catalog that provides a way to evaluate sentences in a language by defining a class for each grammar rule. Given a language, you define its grammar as a set of rules. Each rule maps to a class. Then you build a tree structure that represents a specific sentence in the language, and you evaluate it by traversing that tree.
Think about a calculator that processes arithmetic like 3 + 5 - 2. The grammar has two kinds of elements: numbers (terminal symbols) and operators like addition and subtraction (nonterminal symbols that combine other expressions). The interpreter pattern turns each of these into a class with an Interpret method. A number node returns its value. An addition node asks its left and right children for their values, then adds them. The entire expression becomes a tree that evaluates itself recursively.
This pattern shines when you have a well-defined grammar that doesn't change often and the sentences you need to process are relatively simple. Configuration languages, rule engines, mathematical expression evaluators, query builders, and template systems are all candidates. The grammar stays stable, the sentences vary, and the interpreter pattern gives you a clean, extensible structure for handling them.
Core Participants of the Interpreter Pattern
The interpreter pattern involves five key participants that work together to define and evaluate a language. Understanding each role is essential before writing any code.
AbstractExpression
The AbstractExpression declares an Interpret method that every node in the grammar tree must implement. This is the shared interface or abstract class that makes polymorphic evaluation possible. Every expression in the tree -- whether it's a literal value or a complex operation -- implements this contract.
TerminalExpression
A TerminalExpression represents the leaves of the grammar tree. These are the basic symbols in the language that don't break down any further. In an arithmetic language, numbers are terminal expressions. In a boolean language, literal true and false values are terminal expressions. A terminal expression's Interpret method returns a value directly without delegating to children.
NonterminalExpression
A NonterminalExpression represents a grammar rule that combines other expressions. It holds references to child expressions and implements its Interpret method by evaluating those children and combining the results. An addition operation is a nonterminal expression: it has a left child and a right child, and it evaluates by adding the results of interpreting both. Nonterminal expressions can nest arbitrarily, which creates the recursive tree structure that gives the pattern its power.
Context
The Context contains the global information that the interpreter needs during evaluation. It might hold variable values, state, input data, or anything else that expression nodes need to reference during interpretation. Not every interpreter needs a complex context -- sometimes a simple dictionary of variable bindings is enough.
Client
The Client builds the abstract syntax tree from the grammar and invokes interpretation. The client is responsible for parsing or constructing the tree of expression objects and then calling Interpret on the root node. In simple cases, you build the tree by hand. In more sophisticated implementations, a parser constructs the tree from a string input.
When to Use the Interpreter Pattern
The interpreter pattern is valuable in specific scenarios. Knowing when to apply it -- and when to reach for something else -- keeps your designs clean.
Use it when you have a simple grammar that doesn't change often. The interpreter pattern maps grammar rules to classes. If the grammar is stable, the class hierarchy stays stable. Adding a new rule means adding a new class, which follows the open/closed principle.
Use it when you need to evaluate or transform structured input. Configuration languages, validation rules, mathematical expressions, and filter conditions are all natural fits. The interpreter pattern gives you a structured, testable way to process these inputs.
Avoid it when the grammar is complex or changes frequently. If your language has dozens of rules or evolves rapidly, the number of classes grows quickly and becomes hard to manage. In those cases, parser generators or full language toolkits are better choices.
Avoid it when performance is critical for large inputs. Walking a tree of objects and making virtual method calls at every node introduces overhead compared to a compiled or bytecode-based approach. For small grammars and short expressions, this is negligible. For processing millions of expressions per second, you'll want a different strategy.
Benefits and Drawbacks
Benefits
- Easy to extend the grammar. Adding a new expression type means creating a new class that implements the expression interface. Existing code doesn't change.
- Each grammar rule is its own class. This gives you a clean separation of concerns where each class handles one rule and nothing else.
- Expression trees are composable. You can build complex expressions by combining simpler ones, which pairs naturally with the composite design pattern.
- Testable in isolation. Each expression class can be unit tested independently, verifying that it interprets its inputs correctly.
Drawbacks
- Class explosion for complex grammars. Every rule in the grammar becomes a class. A grammar with 30 rules means 30 expression classes, which can be difficult to manage.
- Performance overhead for large expressions. Tree traversal with virtual dispatch adds overhead compared to iterative or compiled approaches.
- Parsing is a separate concern. The interpreter pattern handles evaluation, not parsing. You still need to build the tree from raw input, which can be the harder problem.
Example 1: Arithmetic Expression Interpreter
Let's build an interpreter that evaluates simple arithmetic expressions like 3 + 5 - 2. This is the classic demonstration of the interpreter pattern and the most intuitive starting point.
Defining the Expression Interface
public interface IExpression
{
int Interpret();
}
Every node in the expression tree can be asked for its integer result. Terminal nodes return a literal value. Nonterminal nodes evaluate their children and combine the results.
Terminal Expression: Numbers
public sealed class NumberExpression : IExpression
{
private readonly int _value;
public NumberExpression(int value)
{
_value = value;
}
public int Interpret() => _value;
}
A NumberExpression is a leaf. It holds a value and returns it. No children, no delegation.
Nonterminal Expressions: Add and Subtract
public sealed class AddExpression : IExpression
{
private readonly IExpression _left;
private readonly IExpression _right;
public AddExpression(IExpression left, IExpression right)
{
_left = left;
_right = right;
}
public int Interpret() =>
_left.Interpret() + _right.Interpret();
}
public sealed class SubtractExpression : IExpression
{
private readonly IExpression _left;
private readonly IExpression _right;
public SubtractExpression(IExpression left, IExpression right)
{
_left = left;
_right = right;
}
public int Interpret() =>
_left.Interpret() - _right.Interpret();
}
Each operator node delegates to its children and combines the results. AddExpression adds. SubtractExpression subtracts. The pattern is consistent and predictable.
Parsing and Evaluating
public static class ArithmeticParser
{
public static IExpression Parse(string input)
{
string[] tokens = input.Split(' ');
IExpression result = new NumberExpression(int.Parse(tokens[0]));
for (int i = 1; i < tokens.Length; i += 2)
{
string op = tokens[i];
IExpression right = new NumberExpression(
int.Parse(tokens[i + 1]));
result = op switch
{
"+" => new AddExpression(result, right),
"-" => new SubtractExpression(result, right),
_ => throw new InvalidOperationException(
$"Unknown operator: {op}")
};
}
return result;
}
}
// Usage
IExpression expression = ArithmeticParser.Parse("3 + 5 - 2");
int result = expression.Interpret();
Console.WriteLine($"Result: {result}"); // Output: Result: 6
The parser reads tokens left to right, building the tree incrementally. Each operator wraps the previous result as its left child and a new number as its right child. The final Interpret() call walks the tree recursively and produces the answer. Notice how the parser and interpreter are separate concerns -- the parser builds the tree, and the interpreter evaluates it.
Example 2: Boolean Logic Interpreter
The interpreter pattern works just as well for boolean expressions. Let's build an interpreter that handles AND, OR, and NOT operations -- useful for filtering rules or condition evaluation.
Expression Interface and Terminal
public interface IBooleanExpression
{
bool Interpret(Dictionary<string, bool> context);
}
public sealed class VariableExpression : IBooleanExpression
{
private readonly string _name;
public VariableExpression(string name)
{
_name = name;
}
public bool Interpret(Dictionary<string, bool> context)
{
if (!context.TryGetValue(_name, out bool value))
{
throw new KeyNotFoundException(
$"Variable '{_name}' not found in context.");
}
return value;
}
}
This time the Interpret method takes a context -- a dictionary of variable bindings. A VariableExpression looks up its name in the context and returns the corresponding boolean value.
Nonterminal Expressions: AND, OR, NOT
public sealed class AndExpression : IBooleanExpression
{
private readonly IBooleanExpression _left;
private readonly IBooleanExpression _right;
public AndExpression(
IBooleanExpression left,
IBooleanExpression right)
{
_left = left;
_right = right;
}
public bool Interpret(Dictionary<string, bool> context) =>
_left.Interpret(context) && _right.Interpret(context);
}
public sealed class OrExpression : IBooleanExpression
{
private readonly IBooleanExpression _left;
private readonly IBooleanExpression _right;
public OrExpression(
IBooleanExpression left,
IBooleanExpression right)
{
_left = left;
_right = right;
}
public bool Interpret(Dictionary<string, bool> context) =>
_left.Interpret(context) || _right.Interpret(context);
}
public sealed class NotExpression : IBooleanExpression
{
private readonly IBooleanExpression _operand;
public NotExpression(IBooleanExpression operand)
{
_operand = operand;
}
public bool Interpret(Dictionary<string, bool> context) =>
!_operand.Interpret(context);
}
NotExpression is a unary operator -- it has one child instead of two. The structure follows the same interpreter pattern, but the grammar is different. Each class maps to one boolean operation. Adding XOR or NAND later means adding one new class each without changing existing code.
Evaluating Boolean Expressions
// Expression: (isAdmin OR isEditor) AND NOT isBanned
var isAdmin = new VariableExpression("isAdmin");
var isEditor = new VariableExpression("isEditor");
var isBanned = new VariableExpression("isBanned");
IBooleanExpression expression = new AndExpression(
new OrExpression(isAdmin, isEditor),
new NotExpression(isBanned));
var context = new Dictionary<string, bool>
{
["isAdmin"] = false,
["isEditor"] = true,
["isBanned"] = false
};
bool result = expression.Interpret(context);
Console.WriteLine($"Access granted: {result}"); // Output: Access granted: True
The context drives the evaluation. You can reuse the same expression tree with different contexts to evaluate the same rule against different users or scenarios. This is a common pattern in authorization systems and rule engines where the rules stay fixed but the data changes per request.
Example 3: Simple Rule Engine / DSL Interpreter
Let's take the interpreter pattern further by building a small rule engine that evaluates conditions against a data object. This simulates a domain-specific language (DSL) where rules like "Age >= 18" and "Country == US" get parsed and evaluated dynamically.
The Data Context and Expression Interface
public sealed class UserProfile
{
public string Name { get; init; } = string.Empty;
public int Age { get; init; }
public string Country { get; init; } = string.Empty;
}
public interface IRuleExpression
{
bool Evaluate(UserProfile profile);
}
The context here is a UserProfile object instead of a generic dictionary. This gives us type safety and makes the rule engine specific to a domain.
Terminal Expressions: Comparison Rules
public sealed class AgeRule : IRuleExpression
{
private readonly string _operator;
private readonly int _threshold;
public AgeRule(string op, int threshold)
{
_operator = op;
_threshold = threshold;
}
public bool Evaluate(UserProfile profile) => _operator switch
{
">=" => profile.Age >= _threshold,
"<=" => profile.Age <= _threshold,
">" => profile.Age > _threshold,
"<" => profile.Age < _threshold,
"==" => profile.Age == _threshold,
_ => throw new InvalidOperationException(
$"Unsupported operator: {_operator}")
};
}
public sealed class CountryRule : IRuleExpression
{
private readonly string _expectedCountry;
public CountryRule(string expectedCountry)
{
_expectedCountry = expectedCountry;
}
public bool Evaluate(UserProfile profile) =>
string.Equals(
profile.Country,
_expectedCountry,
StringComparison.OrdinalIgnoreCase);
}
Each rule type handles its own comparison logic. AgeRule supports multiple comparison operators. CountryRule does a case-insensitive string match. Adding new rule types for other properties -- like EmailDomainRule or MembershipTierRule -- means adding new classes. This extensibility is where the interpreter pattern pays off.
Composite Rules: AND and OR
public sealed class AndRule : IRuleExpression
{
private readonly IRuleExpression[] _rules;
public AndRule(params IRuleExpression[] rules)
{
_rules = rules;
}
public bool Evaluate(UserProfile profile) =>
_rules.All(rule => rule.Evaluate(profile));
}
public sealed class OrRule : IRuleExpression
{
private readonly IRuleExpression[] _rules;
public OrRule(params IRuleExpression[] rules)
{
_rules = rules;
}
public bool Evaluate(UserProfile profile) =>
_rules.Any(rule => rule.Evaluate(profile));
}
These composite rules accept any number of child rules, not just two. This makes them flexible for combining multiple conditions in a single AND or OR grouping. The iterator design pattern underpins the traversal via LINQ's All and Any methods here.
Building and Evaluating Rules
// Rule: (Age >= 18 AND Country == "US") OR (Age >= 21)
IRuleExpression eligibilityRule = new OrRule(
new AndRule(
new AgeRule(">=", 18),
new CountryRule("US")),
new AgeRule(">=", 21));
var user1 = new UserProfile
{
Name = "Alice",
Age = 19,
Country = "US"
};
var user2 = new UserProfile
{
Name = "Bob",
Age = 17,
Country = "CA"
};
Console.WriteLine(
$"{user1.Name}: {eligibilityRule.Evaluate(user1)}"); // True
Console.WriteLine(
$"{user2.Name}: {eligibilityRule.Evaluate(user2)}"); // False
The rule tree is built once and evaluated against multiple profiles. You could load these rules from a configuration file or database, build the tree at startup, and evaluate thousands of profiles against the same rule set. The interpreter pattern separates the rule definition from the data it operates on, which is the hallmark of a clean DSL. This approach also plays well with dependency injection when you need to register rule sets as services in your application.
Example 4: Expression Trees with the Composite Pattern
The interpreter pattern and the composite design pattern are natural partners. Expression trees are a specific form of composite structure where every node is an expression that can be evaluated. Let's build a more feature-rich arithmetic interpreter that supports multiplication, division, and variable substitution -- all structured as a composite expression tree.
Expression Interface with a Shared Context
public sealed class ExpressionContext
{
private readonly Dictionary<string, double> _variables = new();
public void SetVariable(string name, double value) =>
_variables[name] = value;
public double GetVariable(string name)
{
if (!_variables.TryGetValue(name, out double value))
{
throw new KeyNotFoundException(
$"Variable '{name}' is not defined.");
}
return value;
}
}
public interface ITreeExpression
{
double Interpret(ExpressionContext context);
}
The ExpressionContext holds variable bindings. Every expression node receives the context during interpretation, which allows variable references to resolve dynamically.
Terminal Expressions: Literals and Variables
public sealed class LiteralExpression : ITreeExpression
{
private readonly double _value;
public LiteralExpression(double value)
{
_value = value;
}
public double Interpret(ExpressionContext context) => _value;
}
public sealed class VariableReferenceExpression : ITreeExpression
{
private readonly string _variableName;
public VariableReferenceExpression(string variableName)
{
_variableName = variableName;
}
public double Interpret(ExpressionContext context) =>
context.GetVariable(_variableName);
}
LiteralExpression ignores the context and returns a constant. VariableReferenceExpression uses the context to look up a named variable's value.
Nonterminal Expressions: Four Operations
public abstract class BinaryExpression : ITreeExpression
{
protected readonly ITreeExpression Left;
protected readonly ITreeExpression Right;
protected BinaryExpression(
ITreeExpression left,
ITreeExpression right)
{
Left = left;
Right = right;
}
public abstract double Interpret(ExpressionContext context);
}
public sealed class AddTreeExpression : BinaryExpression
{
public AddTreeExpression(
ITreeExpression left,
ITreeExpression right)
: base(left, right) { }
public override double Interpret(ExpressionContext context) =>
Left.Interpret(context) + Right.Interpret(context);
}
public sealed class SubtractTreeExpression : BinaryExpression
{
public SubtractTreeExpression(
ITreeExpression left,
ITreeExpression right)
: base(left, right) { }
public override double Interpret(ExpressionContext context) =>
Left.Interpret(context) - Right.Interpret(context);
}
public sealed class MultiplyTreeExpression : BinaryExpression
{
public MultiplyTreeExpression(
ITreeExpression left,
ITreeExpression right)
: base(left, right) { }
public override double Interpret(ExpressionContext context) =>
Left.Interpret(context) * Right.Interpret(context);
}
public sealed class DivideTreeExpression : BinaryExpression
{
public DivideTreeExpression(
ITreeExpression left,
ITreeExpression right)
: base(left, right) { }
public override double Interpret(ExpressionContext context)
{
double divisor = Right.Interpret(context);
if (divisor == 0)
{
throw new DivideByZeroException(
"Division by zero in expression tree.");
}
return Left.Interpret(context) / divisor;
}
}
The BinaryExpression base class eliminates the duplication of storing left and right children. Each concrete operator class adds only the interpretation logic specific to its operation. This approach follows the template method design pattern -- the base class defines the structure and subclasses fill in the specifics.
Notice the DivideTreeExpression adds a guard against division by zero. Each expression class owns its validation and error handling, keeping the logic localized and easy to find.
Building and Evaluating the Expression Tree
// Expression: (x + 3) * (y - 2)
var context = new ExpressionContext();
context.SetVariable("x", 5);
context.SetVariable("y", 8);
ITreeExpression tree = new MultiplyTreeExpression(
new AddTreeExpression(
new VariableReferenceExpression("x"),
new LiteralExpression(3)),
new SubtractTreeExpression(
new VariableReferenceExpression("y"),
new LiteralExpression(2)));
double result = tree.Interpret(context);
Console.WriteLine($"(x + 3) * (y - 2) = {result}"); // Output: 48
// Change variable values and re-evaluate
context.SetVariable("x", 10);
context.SetVariable("y", 4);
double newResult = tree.Interpret(context);
Console.WriteLine(
$"(x + 3) * (y - 2) = {newResult}"); // Output: 26
The expression tree is built once and evaluated against different variable bindings. Changing x and y produces a different result without rebuilding the tree. This is the power of separating the expression structure from the evaluation context. The tree represents the formula. The context provides the data. You can swap one without touching the other.
This composite-based approach also makes it straightforward to implement features like expression printing, optimization (constant folding), or serialization. Each feature becomes a new method on the expression interface or a visitor that walks the tree -- which is how the interpreter pattern often evolves in more complex systems.
Interpreter Pattern and Related Patterns
Understanding how the interpreter pattern relates to other design patterns helps you combine them effectively.
Interpreter and Composite: Expression trees are composite structures. Every nonterminal expression is a composite node holding child expressions. The composite design pattern provides the structural foundation, and the interpreter pattern adds the evaluation behavior. They're almost always used together.
Interpreter and Strategy: If you have multiple ways to interpret the same grammar -- for example, evaluating versus pretty-printing -- you can use the strategy design pattern to swap interpretation strategies at runtime. Each strategy provides a different implementation of the Interpret method's logic.
Interpreter and Flyweight: When your expression tree has many identical terminal nodes (like the same variable referenced hundreds of times), the flyweight design pattern helps by sharing those instances instead of creating duplicates. This reduces memory usage in large expression trees.
Interpreter and Command: The command design pattern encapsulates a request as an object. The interpreter pattern does something similar for expressions -- each expression node is an object that encapsulates an operation. The key difference is intent: commands represent actions to execute, while interpreter expressions represent values to compute.
Best Practices and Common Pitfalls
The interpreter pattern is conceptually simple, but production implementations require thoughtful design. Keep these guidelines in mind.
Keep expression classes small and focused. Each class should represent one grammar rule and nothing else. If an expression class is handling parsing, validation, and interpretation, split those responsibilities. The interpreter pattern is about evaluation -- parsing and validation are separate concerns.
Use interfaces, not concrete types, for expression references. Expression nodes should hold references to IExpression, not to specific expression classes. This follows inversion of control principles and makes the tree flexible enough to hold any combination of expression types.
Consider immutability. Expression trees work best when they're immutable. Once built, a tree should not change. This makes them safe to share across threads and easy to reason about. Use readonly fields and constructor injection to enforce this.
Watch out for stack overflow with deeply nested trees. Recursive interpretation works well for moderately sized trees. If your expressions can nest hundreds or thousands of levels deep, consider converting the recursive interpretation to an iterative approach using an explicit stack.
Separate parsing from interpretation. The interpreter pattern doesn't tell you how to build the tree -- only how to evaluate it. Keep your parser (the code that turns strings into expression trees) in a separate class or module. This gives you the flexibility to swap parsers without affecting the expression classes, similar to how the facade design pattern provides a clean entry point that hides complex subsystem logic.
Test each expression class independently. One of the biggest advantages of the interpreter pattern is testability. Each expression class is a small, isolated unit with a clear contract. Write unit tests that verify each expression in isolation, then integration tests that verify complete expression trees.
Frequently Asked Questions
What is the interpreter design pattern used for?
The interpreter design pattern is used to define a grammar for a language and provide a mechanism to evaluate sentences in that language. It maps each rule in the grammar to a class, and evaluating an expression means traversing a tree of those classes. Common use cases include mathematical expression evaluators, boolean logic engines, configuration parsers, validation rule engines, and domain-specific languages embedded within larger applications.
When should I use the interpreter pattern instead of a full parser?
Use the interpreter pattern when the grammar is simple, stable, and consists of a small number of rules. If you're evaluating arithmetic expressions, boolean conditions, or a handful of domain-specific rules, the interpreter pattern keeps things clean and extensible. Switch to a full parser generator (like ANTLR) or a parser combinator library when the grammar is complex, has many rules, requires precedence handling, or is likely to evolve frequently. The interpreter pattern handles evaluation well but doesn't scale to complex language grammars.
How does the interpreter pattern relate to the composite pattern?
The interpreter pattern and the composite pattern are tightly connected. Every expression tree built with the interpreter pattern is a composite structure -- nonterminal expressions are composites that contain child expressions, and terminal expressions are leaves. The composite design pattern provides the structural framework for building trees, and the interpreter pattern adds the Interpret method that gives each node the ability to evaluate itself. In practice, you almost always use both patterns together.
What are the performance limitations of the interpreter pattern?
The interpreter pattern evaluates expressions by walking an object tree with virtual method calls at every node. This introduces overhead compared to compiled or bytecode-based approaches. For small expressions evaluated occasionally, the overhead is negligible. For hot paths where millions of evaluations happen per second, the tree-walking approach becomes a bottleneck. In those cases, consider compiling the expression tree into a delegate using System.Linq.Expressions or generating IL directly. The interpreter pattern prioritizes clarity and extensibility over raw speed.
Can I combine the interpreter pattern with dependency injection?
Yes. You can register expression factories or rule builders with your dependency injection container and resolve them at runtime. This is especially useful in rule engine scenarios where the set of available rule types is configured at startup. The expression classes themselves are typically simple value objects that don't need DI, but the parsers and rule builders that construct expression trees benefit from having their dependencies injected.
How do I handle errors in an interpreter implementation?
Handle errors at the expression level where possible. Each expression class should validate its own inputs during interpretation and throw meaningful exceptions. For example, a divide expression should check for division by zero, and a variable reference expression should throw a clear error if the variable isn't found in the context. Avoid swallowing exceptions silently -- let errors propagate with enough context for the caller to understand what went wrong and where in the expression tree the failure occurred.
What is the difference between the interpreter and strategy patterns?
The interpreter pattern defines a grammar as a set of classes and evaluates sentences by traversing a tree of those classes. The strategy design pattern encapsulates interchangeable algorithms behind a common interface. They solve different problems, but they complement each other well. You might use the strategy pattern to provide different interpretation strategies for the same expression tree -- for example, one strategy that evaluates the expression and another that pretty-prints it. The interpreter pattern defines the structure, and the strategy pattern varies the behavior.
Wrapping Up the Interpreter Design Pattern in C#
The interpreter design pattern in C# is a powerful behavioral pattern for evaluating structured expressions and building small domain-specific languages within your applications. By mapping each grammar rule to its own class and organizing expressions into a tree, you get a design that's modular, extensible, and straightforward to test. Whether you're building an arithmetic calculator, a boolean logic engine, a rule evaluator, or a composite expression tree with variable substitution, the interpreter pattern gives you a clean foundation.
Start by identifying places in your codebase where you're parsing and evaluating structured input -- configuration rules, filter conditions, mathematical formulas, or validation logic. If the grammar is small and stable, the interpreter pattern is a strong fit. Keep your expression classes focused, separate parsing from interpretation, and lean on the composite pattern for tree structure. You can find the interpreter pattern alongside other behavioral patterns in the complete list of design patterns.

