README
ยถ
๐ Go Rules Engine
A powerful and flexible business rules engine for Go, inspired by json-rules-engine. Evaluate complex conditions and trigger events based on dynamic facts.
โจ Features
- ๐ฏ JSON or Code-defined Rules - Load rules from JSON files or create them directly in Go
- ๐ Complex Conditions - Support
all,any, andnoneoperators with infinite nesting - ๐ Rich Operators - 11 built-in operators including
equal,greater_than,contains,regexand more - ๐ช Event System - Custom callbacks and global handlers to react to results
- ๐พ Dynamic Facts - Compute values on-the-fly with callbacks
- ๐งฎ JSONPath Support - Access nested data with
$.path.to.value - โก Rule Priorities - Control evaluation order with configurable priority sorting (ASC/DESC)
- โก High Performance - Condition caching, pre-calculation of cache keys, and smart skipping of rules
- ๐ Audit Trace - Full evaluation tree with fact values, compatible with caching and JSON serialization
- ๐ Thread-safe - Protected by mutexes for concurrent usage
- ๐ฅ Hot-reload Support - Update rules from remote sources (HTTP) without restarting
- โ 100% Test Coverage - Robust and thoroughly tested code
๐ฆ Installation
go get github.com/deadelus/go-rules-engine
๐ Quick Start
Find all our detailed examples in the docs/examples folder.
๐ Documentation
Architecture
For a complete visual architecture, see the detailed architecture documentation with Mermaid diagrams.
System Overview

The rules engine is composed of several key components working together:

1. Engine - The main engine
// Default engine (with descending priority sorting)
engine := gre.NewEngine()
// Engine with custom sorting
sortOrder := gre.SortRuleASC
engine := gre.NewEngine(gre.WithPrioritySorting(&sortOrder))
// Engine without priority sorting (insertion order)
engine := gre.NewEngine(gre.WithoutPrioritySorting())
Configuration Options:
WithPrioritySorting(*SortRule)- Enable priority sorting (default: DESC)SortRuleASC- Sort by ascending priority (lower first)SortRuleDESC- Sort by descending priority (higher first, default)
WithoutPrioritySorting()- Disable priority sorting (evaluate rules in insertion order)WithParallelExecution(workers)- Enable parallel evaluation with N workersWithoutParallelExecution()- Disable parallel evaluation (sequential order)WithConditionCaching()- Enable condition results cachingWithoutConditionCaching()- Disable condition results cachingWithSmartSkip()- Enable skipping rules with missing factsWithAuditTrace()- Enable detailed audit traceWithoutAuditTrace()- Disable detailed audit trace
Methods:
AddRule(rule *Rule)- Add a rule to the engineRegisterEvent(event Event)- Register a named event (with its action and mode)SetEventHandler(handler EventHandler)- Set a global event handler for all eventsRun(almanac *Almanac) (*Engine, error)- Execute all rules (returns engine for logical chaining)Results() map[string]*RuleResult- Get detailed results of the last executionReduceResults() map[string]bool- Get pass/fail results for each ruleGenerateResponse() *EngineResponse- Get a consolidated, JSON-marshalable response
2. Rule - A business rule
rule := &gre.Rule{
Name: "adult-check",
Priority: 10, // Higher = executed first
Conditions: conditionSet,
OnSuccess: []gre.RuleEvent{{Name: "approve-user"}, {Name: "send-welcome-email"}},
OnFailure: []gre.RuleEvent{{Name: "reject-user"}},
}
3. Condition - A condition to evaluate

condition := &gre.Condition{
Fact: "age",
Operator: "greater_than",
Value: 18,
Path: "$.user.age", // Optional: JSONPath for nested data
}
Available Operators:
equal- Equalitynot_equal- Not equal togreater_than- Greater thangreater_than_inclusive- Greater than or equal toless_than- Less thanless_than_inclusive- Less than or equal toin- In the listnot_in- Not in the listcontains- Contains (for strings and arrays)not_contains- Does not containregex- Matches a regular expression pattern (string values only)
4. ConditionSet - Condition grouping
// All conditions must be true (AND)
conditionSet := gre.ConditionSet{
All: []gre.ConditionNode{
{Condition: &condition1},
{Condition: &condition2},
},
}
// At least one condition must be true (OR)
conditionSet := gre.ConditionSet{
Any: []gre.ConditionNode{
{Condition: &condition1},
{Condition: &condition2},
},
}
// Nesting (AND of OR)
conditionSet := gre.ConditionSet{
All: []gre.ConditionNode{
{Condition: &condition1},
{
SubSet: &gre.ConditionSet{
Any: []gre.ConditionNode{
{Condition: &condition2},
{Condition: &condition3},
},
},
},
},
}
5. Almanac - Facts storage

almanac := gre.NewAlmanac()
// Add simple facts
almanac.AddFact("age", 25)
almanac.AddFact("country", "FR")
// Add dynamic facts
almanac.AddFact("temperature", func(params map[string]interface{}) interface{} {
// Custom calculation logic
return fetchTemperature()
})
// Retrieve a fact
value, err := almanac.GetFactValue("age", nil)
6. Event - Triggered event
event := gre.Event{
Name: "user-approved",
Params: map[string]interface{}{
"userId": 123,
"reason": "All conditions met",
},
Mode: gre.EventModeSync, // or EventModeAsync
Action: func(ctx gre.EventContext) error {
fmt.Printf("Action triggered for rule: %s\n", ctx.RuleName)
return nil
},
}
Callbacks and Handlers System

The engine provides two ways to handle results:
1. Named Events (Registered in Engine)
Events are registered in the engine and referenced by rules via their OnSuccess or OnFailure fields.
engine := gre.NewEngine()
// Register the event
engine.RegisterEvent(gre.Event{
Name: "sendEmail",
Mode: gre.EventModeSync,
Action: func(ctx gre.EventContext) error {
fmt.Printf("Sending email for rule: %s\n", ctx.RuleName)
return nil
},
})
// Rule referencing the event
rule := &gre.Rule{
Name: "email-rule",
OnSuccess: []gre.RuleEvent{{Name: "sendEmail"}},
// ...
}
2. Global Event Handler
You can set a global handler that will be called for EVERY event triggered by the engine. This is useful for logging, metrics, or centralized processing.
type MyGlobalHandler struct{}
func (h *MyGlobalHandler) Handle(event gre.Event, ctx gre.EventContext) error {
fmt.Printf("๐ Global handler: Event %s triggered by rule %s (Result: %v)\n",
event.Name, ctx.RuleName, ctx.Result)
return nil
}
engine.SetEventHandler(&MyGlobalHandler{})
Synchronous vs Asynchronous Execution
You can control whether an event is executed synchronously (blocking the engine's Run loop) or asynchronously (in a separate goroutine).
// Synchronous event (default)
syncEvent := gre.Event{
Name: "sync-event",
Mode: gre.EventModeSync,
Action: func(ctx gre.EventContext) error {
// Blocks the engine until finished
return nil
},
}
// Asynchronous event
asyncEvent := gre.Event{
Name: "async-event",
Mode: gre.EventModeAsync,
Action: func(ctx gre.EventContext) error {
// Runs in a background goroutine
return nil
},
}
JSONPath Support
Access nested data in your facts:
almanac := gre.NewAlmanac([]*gre.Fact{})
almanac.AddFact("user", map[string]interface{}{
"profile": map[string]interface{}{
"age": 25,
"address": map[string]interface{}{
"city": "Paris",
},
},
})
// Use JSONPath in conditions
condition := &gre.Condition{
Fact: "user",
Path: "$.profile.address.city",
Operator: "equal",
Value: "Paris",
}
Regex Pattern Matching
Use the regex operator to match string values against regular expression patterns:
engine := gre.NewEngine()
// Rule to validate email format
emailRule := &gre.Rule{
Name: "validate-email",
Priority: 10,
Conditions: gre.ConditionSet{
All: []gre.ConditionNode{
{
Condition: &gre.Condition{
Fact: "email",
Operator: "regex",
Value: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
},
},
},
},
OnSuccess: []gre.RuleEvent{{Name: "valid-email-event"}},
}
engine.AddRule(emailRule)
almanac := gre.NewAlmanac([]*gre.Fact{})
almanac.AddFact("email", "user@example.com")
results, _ := engine.Run(almanac)
// Will match if email is valid
๐ Formatted API Response
The engine provides a GenerateResponse() method that aggregates all rule results into a single, clean structure designed for API responses. It consolidates the decision, reasons (audit trace), triggered events, and fact metadata.
1. Define Rules in JSON
[
{
"name": "premium-access",
"priority": 100,
"conditions": {
"all": [
{ "fact": "user_status", "operator": "equal", "value": "vip" },
{ "fact": "age", "operator": "greater_than_inclusive", "value": 18 }
]
},
"onSuccess": [
{ "name": "grant-access", "params": { "tier": "platinum" } }
],
"onFailure": [
{ "name": "restrict-access" }
]
}
]
2. Get the Response
engine := gre.NewEngine(gre.WithAuditTrace())
// ... load rules and facts ...
e, _ := engine.Run(almanac)
response := e.GenerateResponse()
// Marshalling to JSON
jsonOutput, _ := json.MarshalIndent(response, "", " ")
fmt.Println(string(jsonOutput))
3. Output Example
{
"decision": "authorize",
"reason": {
"type": "all",
"result": true,
"results": [
{
"condition": {
"fact": "user_status",
"operator": "equal",
"value": "vip",
"factValue": "vip",
"result": true
}
},
{
"condition": {
"fact": "age",
"operator": "greater_than_inclusive",
"value": 18,
"factValue": 25,
"result": true
}
}
]
},
"events": [
{
"type": "grant-access",
"params": { "tier": "platinum" }
}
],
"metadata": {
"user_status": { "source": "db", "cached": true }
}
}
Audit Trace & Result Serialization
The engine can collect a detailed "trace" of the evaluation process. This includes not just the final result, but the outcome of every single condition, including the actual fact values retrieved from the Almanac.
Note: Our implementation is fully compatible with caching. When caching is enabled, the engine stores the entire result tree, ensuring that Audit Traces remain complete and detailed even for cached results.
This data is fully serializable to JSON.
Enable Audit Trace
engine := gre.NewEngine(
gre.WithAuditTrace(),
)
Extract Detailed Results
e, _ := engine.Run(almanac)
// Get detailed trace for all rules
results := e.Results()
// Serialize to JSON
jsonData, _ := json.MarshalIndent(results, "", " ")
fmt.Println(string(jsonData))
// Or get simple pass/fail map
simpleResults := e.ReduceResults()
๐ฅ Hot-reload of Rules
The engine supports dynamic reloading of rules from external sources (like an HTTP API or S3) without stopping evaluation.
engine := gre.NewEngine()
// 1. Create a provider (HTTP source)
provider := gre.NewHTTPRuleProvider("https://api.myapp.com/rules")
// 2. Create and start the reloader
reloader := gre.NewHotReloader(engine, provider, 5 * time.Minute)
// Optional: monitor updates or errors
reloader.OnUpdate(func(rules []*gre.Rule) {
fmt.Printf("Updated %d rules!\n", len(rules))
})
reloader.Start(context.Background())
Condition Results Caching
Optimize performance by caching the results of condition evaluations. This is particularly useful when multiple rules share identical conditions or when working with expensive dynamic facts.
Enable globally via the Engine
// Enabled for all rules and all Almanacs passed to this engine
engine := gre.NewEngine(gre.WithConditionCaching())
Enable per Almanac
// Enabled only for this specific Almanac
almanac := gre.NewAlmanac(gre.WithAlmanacConditionCaching())
Error Handling
The engine uses a typed error system for better traceability:
results, err := engine.Run(almanac)
if err != nil {
var ruleErr *gre.RuleEngineError
if errors.As(err, &ruleErr) {
fmt.Printf("Type: %s, Message: %s\n", ruleErr.Type, ruleErr.Msg)
}
}
Error Types:
ErrEngine- General engine errorErrAlmanac- Error related to facts (almanac)ErrFact- Fact calculation errorErrRule- Error in rule definitionErrCondition- Condition evaluation errorErrOperator- Invalid or not found operatorErrEvent- Error related to eventsErrJSON- JSON parsing error
โก Advanced Optimizations
The engine includes several advanced performance features for high-throughput environments:
1. Condition Caching
Enable caching to reuse results of identical conditions or subtrees (ConditionSets) within a single engine run. This is extremely effective for overlapping conditions across multiple rules.
Unlike simple boolean caches, we store the full ConditionResult and ConditionSetResult objects, which means Audit Traces remain fully detailed even when using cached values.
engine := gre.NewEngine(
gre.WithConditionCaching(),
)
2. Smart Skip (Dependency Tracking)
The engine can map fact dependencies of rules and skip evaluation if the required facts are not present in the Almanac. This prevents expensive condition evaluations when data is missing.
engine := gre.NewEngine(
gre.WithSmartSkip(),
)
3. Rule Compilation
When rules are added to the engine, they are automatically "compiled" (pre-calculating condition keys and dependency maps). This moves processing once from evaluation time to registration time.
4. Short-circuit Reordering
The evaluation engine reorders nodes within All, Any, or None condition sets to evaluate nodes with cached results first. This maximizes short-circuit opportunities and minimizes redundant fact fetches.
Note: This optimization is applied automatically whenever condition caching is enabled.
๐ Performance & Benchmarks
The engine is optimized for high-throughput environments. Below are the benchmarks executed on Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz (macOS).
Execution Speed
| Scenario | Mode | Rules Count | Time per Op | Memory | Allocs |
|---|---|---|---|---|---|
| Simple Run | Sequential | 1 | 769 ns | 552 B | 8 |
| Sequential | 10 | 5.9 ยตs | 3.6 KB | 56 | |
| Sequential | 100 | 58.2 ยตs | 36.6 KB | 512 | |
| Caching Impact | No Cache | 50 | 38.9 ยตs | 18.3 KB | 262 |
| With Cache | 50 | 14.5 ยตs | 8.7 KB | 62 | |
| Smart Skip | No Skip | 1 | 6.9 ยตs | 3.7 KB | 40 |
| With Skip | 1 | 0.8 ยตs | 0.4 KB | 7 |
Key Takeaways
- Caching: Reduces execution time by ~63% and allocations by ~76% for shared conditions.
- Smart Skip: Avoids unnecessary computations, making evaluations ~8x faster when facts are missing.
- Complex Nesting: Evaluating complex nested conditions (5 levels deep) takes only ~1.6 ยตs.
๐งช Tests
The project has 100% test coverage:
# Run all tests
go test ./src -v
# With coverage
go test ./src -coverprofile=coverage.out
go tool cover -html=coverage.out
# See summary
go tool cover -func=coverage.out | tail -1
# Output: total: (statements) 100.0%
๐ Code Quality
The code follows all Go conventions and passes linters without warnings:
# go vet (static analysis)
go vet ./src/...
# golint (Go style)
golint ./src/...
# Code formatting
go fmt ./src/...
Standards Enforced:
- โ Go naming conventions (CamelCase, no ALL_CAPS)
- โ Complete GoDoc documentation on all exports
- โ Appropriate error handling
- โ Thread-safe code with mutexes
- โ Comprehensive tests with 100% coverage
๐บ๏ธ Roadmap
โ Completed Phases
- Phase 1: Basic structures (Condition, Rule, Fact)
- Phase 2: Almanac and facts management
- Phase 3: Operators (equal, greater_than, less_than, etc.)
- Phase 4: Condition evaluation (all/any, nesting)
- Phase 5: Engine with event system
- Phase 6: JSON support and deserialization
- Phase 7: Advanced features (callbacks, handlers, JSONPath)
- Phase 8: Configurable priority sorting (ASC/DESC/disabled)
- Phase 9: Regex operator for pattern matching
- Phase 10: Ergonomic API and builders
- Phase 11: Performance and optimization
- Phase 12: Metrics, Audit Trace, Hot-reload & API Wrapper
- Complete tests with 100% coverage
๐ค Contributing
Contributions are welcome! To contribute:
- Fork the project
- Create a branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Guidelines:
- Write tests for all new features
- Maintain 100% coverage
- Follow Go conventions (gofmt, golint)
- Document your public functions
๐ License
This project is licensed under the MIT License. See the LICENSE file for more details.
Copyright (c) 2026 Geoffrey Trambolho (@deadelus)
๐ Acknowledgments
Inspired by json-rules-engine by CacheControl.
๐ Contact
Created by @deadelus
โญ Don't forget to star if this project helps you!
Directories
ยถ
| Path | Synopsis |
|---|---|
|
docs
|
|
|
examples/advanced
command
|
|
|
examples/api
command
|
|
|
examples/audit-trace
command
|
|
|
examples/basic
command
|
|
|
examples/builder
command
|
|
|
examples/custom-operator
command
|
|
|
examples/hot-reload
command
|
|
|
examples/json
command
|
|
|
examples/metrics
command
|
|
|
examples/parallel
command
|
|
|
package gorulesengine provides a powerful and flexible rules engine for Go.
|
package gorulesengine provides a powerful and flexible rules engine for Go. |