Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
The Problem
An e-commerce platform needs a flexible discount rule engine where business users can define rules like “10% off orders above $100” without modifying code. Hard-coding each discount rule creates tight coupling and makes the system rigid.
The Solution
The Interpreter pattern defines a mini-language for discount rules (e.g., PERCENT 10 IF TOTAL_ABOVE 100) and builds an AST of expression objects. A parser service translates rule strings into composable expression objects that can be evaluated against any order context.
import { Injectable, BadRequestException } from '@nestjs/common';import { Expression } from './expression.interface';import { PercentDiscountExpression } from './percent-discount.expression';import { FlatDiscountExpression } from './flat-discount.expression';import { ConditionalExpression, ConditionType } from './conditional.expression';import { AndExpression } from './and.expression';@Injectable()export class DiscountParserService { parse(rule: string): Expression { const trimmed = rule.trim().toUpperCase(); // Handle AND combinator: "FLAT 5 AND PERCENT 10" const andIndex = trimmed.indexOf(' AND '); if (andIndex !== -1) { const leftRule = rule.trim().substring(0, andIndex); const rightRule = rule.trim().substring(andIndex + 5); return new AndExpression(this.parse(leftRule), this.parse(rightRule)); } // Handle conditional: "PERCENT 10 IF TOTAL_ABOVE 100" const ifIndex = trimmed.indexOf(' IF '); if (ifIndex !== -1) { const expressionPart = rule.trim().substring(0, ifIndex); const conditionPart = rule.trim().substring(ifIndex + 4).trim(); const innerExpression = this.parse(expressionPart); return this.parseCondition(conditionPart, innerExpression); } // Handle base expressions if (trimmed.startsWith('PERCENT ')) { const value = parseFloat(trimmed.substring(8)); if (isNaN(value)) { throw new BadRequestException(`Invalid percentage value in rule: "${rule}"`); } return new PercentDiscountExpression(value); } if (trimmed.startsWith('FLAT ')) { const value = parseFloat(trimmed.substring(5)); if (isNaN(value)) { throw new BadRequestException(`Invalid flat discount value in rule: "${rule}"`); } return new FlatDiscountExpression(value); } throw new BadRequestException( `Unknown rule format: "${rule}". Supported: "PERCENT <n>", "FLAT <n>", "<rule> IF <condition>", "<rule> AND <rule>"`, ); } private parseCondition(conditionPart: string, innerExpression: Expression): ConditionalExpression { const tokens = conditionPart.trim().split(/\s+/); if (tokens.length < 2) { throw new BadRequestException(`Invalid condition: "${conditionPart}"`); } const conditionType = tokens[0].toUpperCase() as ConditionType; const conditionValue = tokens[1]; const validConditions: ConditionType[] = ['TOTAL_ABOVE', 'ITEMS_ABOVE', 'CUSTOMER_IS']; if (!validConditions.includes(conditionType)) { throw new BadRequestException( `Unknown condition type: "${conditionType}". Supported: ${validConditions.join(', ')}`, ); } return new ConditionalExpression(conditionType, conditionValue, innerExpression); }}
NestJS Integration
The DiscountParserService is an @Injectable() singleton provider. It is stateless — it parses rule strings into expression trees on each call. The expression objects themselves are plain classes, created dynamically during parsing. This clean separation means the parser can be injected into any service that needs discount evaluation.
When to Use
You need to interpret a simple, well-defined language or set of rules.
The grammar is stable and doesn’t change frequently.
Business users need to define rules without code changes.
When NOT to Use
The grammar is complex — consider a proper parser generator instead.
Performance is critical and rules are evaluated millions of times — the tree-walking approach adds overhead.