Many applications need rules that change more often than the deployment cycle. A monitoring product needs customer-specific alert thresholds, an ingestion pipeline needs configurable validation checks, a workflow engine needs routing predicates, and an API gateway may need conditional policy logic. Hard-coding those expressions into application code makes every rule change a software release.
An embedded expression evaluator solves that problem by accepting a small, bounded expression language at runtime, parsing it into a reusable program representation, and evaluating it against request, event, metric, or document parameters. The govaluate library demonstrates this pattern in Go with C-like arithmetic, comparison, boolean, regex, ternary, null-coalescing, array-membership, accessor, and custom-function support.
The transferable design is not the exact syntax. It is the boundary between configurable user-authored logic and trusted host application code. The host owns parsing, type checks, parameter binding, function registration, performance budgets, and error reporting. The user controls only the expression text and input values that the host explicitly exposes.
An expression evaluator is useful when the rule is business- or tenant-specific but the data model and execution point are stable. Instead of shipping a new binary for response_time <= 100 or requests_succeeded / requests_made >= 0.99, the application stores the expression as configuration and supplies parameters at evaluation time.
Parsing is the expensive phase. The expression text should be parsed and validated when a rule is created or updated, then reused for many events. govaluate exposes an EvaluableExpression that can be evaluated repeatedly with different parameter maps, making it suitable for alert checks, validation rules, and routing predicates that run in hot paths.
Custom functions allow the host application to expose narrowly scoped behavior such as string length, numeric aggregation, or domain-specific lookups. Functions are registered during parsing and cannot be added to an already parsed expression, which keeps the execution environment stable for the lifetime of that compiled rule.
The expression language should be powerful enough for configurable decisions, but not powerful enough to become arbitrary code execution. Keep the grammar small, bind only approved parameters, register only safe functions, and make unsupported type/operator combinations fail instead of guessing.
The application accepts expression text at configuration time, parses it, and stores the compiled object with the rest of the rule metadata. If parsing fails, reject the rule before it can enter production evaluation. The runtime path should only bind values and evaluate.
package rules
import (
"fmt"
"github.com/casbin/govaluate"
)
type Rule struct {
Name string
Expr *govaluate.EvaluableExpression
}
func CompileRule(name, expressionText string) (*Rule, error) {
expr, err := govaluate.NewEvaluableExpression(expressionText)
if err != nil {
return nil, fmt.Errorf("invalid expression for %s: %w", name, err)
}
return &Rule{Name: name, Expr: expr}, nil
}
func (r *Rule) Matches(params map[string]interface{}) (bool, error) {
value, err := r.Expr.Evaluate(params)
if err != nil {
return false, err
}
matched, ok := value.(bool)
if !ok {
return false, fmt.Errorf("rule %s returned %T, want bool", r.Name, value)
}
return matched, nil
}
Parameters are the contract between host code and rule authors. The evaluator should receive simple, documented names such as response_time_ms, status_code, tenant_plan, or requests_succeeded. Avoid passing entire request objects when a small flattened map is enough.
rule, err := CompileRule(
"healthy-success-rate",
"(requests_succeeded / requests_made) >= 0.90 && response_time_ms <= 250",
)
if err != nil {
return err
}
params := map[string]interface{}{
"requests_made": 100.0,
"requests_succeeded": 97.0,
"response_time_ms": 180.0,
}
ok, err := rule.Matches(params)
When operators are not enough, register a finite function map. This is safer than exposing a general scripting runtime because every function is named, typed by convention, and owned by host code. Treat these functions as API surface: version them carefully and keep side effects out of hot-path evaluators.
Monitoring systems can let teams define threshold logic without changing the collector. A rule such as error_rate > 0.02 || p95_latency_ms > 500 can be parsed once when the alert is saved and evaluated whenever new metrics arrive.
Data pipelines often need configurable checks before accepting records. Expression evaluators let operators write validations over extracted fields, such as required confidence scores, format checks, or tenant-specific limits, while the ingestion service remains generic.
API platforms can use expressions to select workflows, features, or downstream queues. For example, a rule might route enterprise tenants with large payloads to a high-memory worker pool, while routing smaller requests through the default path.
Store rules in a registry that validates expressions before activation. Keep both the original text and the compiled expression. The original text is needed for audit logs and editing; the compiled expression is needed for efficient evaluation.
type Registry struct {
rules map[string]*Rule
}
func (r *Registry) Put(name, expressionText string) error {
compiled, err := CompileRule(name, expressionText)
if err != nil {
return err
}
r.rules[name] = compiled
return nil
}
func (r *Registry) Evaluate(name string, params map[string]interface{}) (bool, error) {
rule, ok := r.rules[name]
if !ok {
return false, fmt.Errorf("unknown rule %q", name)
}
return rule.Matches(params)
}
Expression grammars often treat hyphens, slashes, and operators as syntax. govaluate supports bracketed names such as [response-time] < 100 and backslash escaping. A more robust production pattern is to normalize externally visible fields into expression-safe parameter names before evaluation.
Accessors over structs or maps are convenient, but govaluate documents that they use reflection and can be several times slower than direct parameter lookup. For frequently evaluated rules, extract required values into a flat parameter map or implement a custom parameter provider that avoids reflective traversal.
float64. Be careful with currency, counters that exceed exact float precision, and bitwise operations over very large numbers.bool and reject numeric or string results where a decision is required.