Press enter or click to view image in full size
Software systems inevitably accumulate business logic.
Today’s “simple if statement” becomes tomorrow’s pricing engine, fraud detection system, discount calculator, policy evaluator, or workflow orchestrator.
At some point, you face a difficult question:
How do you allow business rules to evolve without redeploying your application every time?
This is precisely the problem that inspired Excanon, a JSON-driven rule engine for Elixir that allows you to define, load, and execute business rules dynamically.
In this article, we’ll explore:
- Why rule engines matter
- The challenges of hardcoded business logic
- How Excanon approaches dynamic rules in Elixir
- A real-world example using an order processing system
The Problem with Hardcoded Rules
Most applications start with business rules embedded directly in code:
def calculate_discount(order, customer) do
cond do
order.quantity >= 5 ->
order.subtotal * 0.05 customer.tier == "premium" and order.total > 50 ->
10
true ->
0
end
end
This works initially, but eventually:
- Rules become numerous
- Requirements change frequently
- Product teams want faster iteration
- Rules need configuration rather than deployment
Before long, your application becomes a collection of nested conditionals that are difficult to maintain and nearly impossible for non-developers to understand.
What we really want is this:
{
"conditions": {
"gte": [
{"obj": "order.quantity"},
5
]
},
"actions": [
{
"set": [
"order.discount_percent",
5
]
}
]
}That is exactly what Excanon provides.
What is Excanon?
Excanon is a flexible rule engine for Elixir that evaluates business logic defined in JSON format.
It supports:
- JSON-based rule definitions
- Logical operations
- Arithmetic expressions
- Nested object access
- Stateful rule engines
- Dynamic fact mutation
- JSON Pointer-style lookups
This makes it useful for:
- Pricing engines
- Discount systems
- Policy evaluators
- Workflow automation
- Decision engines
- Eligibility checks
- Feature flag logic
- Business process orchestration
Installing Excanon
Add the dependency:
def deps do
[
{:excanon, "~> 0.1.0"}
]
endThen install:
mix deps.getCreating a Rule Engine
Excanon provides a stateful rule engine backed by Elixir agents.
Create an engine:
{:ok, _pid} =
StatefulRuleEngine.start_link(
:order_engine,
[]
)This process maintains your rule set and allows multiple evaluations without repeatedly loading rules.
Defining Rules in JSON
Suppose we have two business requirements:
1. Orders with 5 or more items receive a 5% discount.
2. Premium customers receive loyalty points.
We can define these rules entirely in JSON:
[
{
"name": "bulk_discount",
"description": "Applies a 5% discount for orders with quantity of 5 or more.",
"conditions": {
"gte": [
{"obj": "order.quantity"},
5
]
},
"actions": [
{
"set": [
"order.discount_percent",
5
]
},
{
"set": [
"order.discount_amount",
{
"mult": [
{"obj": "order.subtotal"},
0.05
]
}
]
}
]
},
{
"name": "loyalty_bonus",
"description": "Adds 10 loyalty points for premium customers with orders over $50.",
"conditions": {
"and": [
{
"eq": [
{"obj": "customer.tier"},
"premium"
]
},
{
"gt": [
{"obj": "order.total"},
50
]
}
]
},
"actions": [
{
"set": [
"customer.loyalty_points",
{
"plus": [
{"obj": "customer.loyalty_points"},
10
]
}
]
}
]
}
]No recompilation.
No deployment.
Just configuration.
Loading Rules
Load the rules into the engine:
:ok =
StatefulRuleEngine.load_rules(
:order_engine,
rules_json
)Once loaded, the engine is ready to evaluate incoming facts.
Evaluating Facts
Facts are the information inputs and outputs that go in or out the rule engine. It can be seen as the “context” of the rule engine. Let’s go back to our example by providing some business data:
facts = %{
"order" => %{
"quantity" => 6,
"subtotal" => 120.0,
"total" => 120.0
},
"customer" => %{
"tier" => "premium",
"loyalty_points" => 50
}
}Evaluate:
{:ok, result} =
StatefulRuleEngine.evaluate(
:order_engine,
facts
)The result becomes:
%{
"order" => %{
"quantity" => 6,
"subtotal" => 120.0,
"total" => 120.0,
"discount_percent" => 5,
"discount_amount" => 6.0
},
"customer" => %{
"tier" => "premium",
"loyalty_points" => 60
}
}The rules transformed the data without any application-specific code.
Be mindful that even though rules are stateful, which means that they stay the same from execution to execution, the facts are independent and stateless. This means that each execution only modifies the facts at hand and are not influenced by previous fact executions.
Building Complex Logic
Excanon supports a rich collection of operators.
Logical Operations
{
"and": [
{"gt": [10, 5]},
{"lt": [20, 30]}
]
}Available operators:
- eq (equality ==)
- neq (inequality !=)
- and
- or
- gt (greater than >)
- gte (greater than or equal ≥)
- lt (less than <)
- lte (less than or equal ≤)
Arithmetic Operations
{
"plus": [10, 20, 30]
}Supported operations:
- plus
- minus
- mult
- div
- mod (modulo %)
Data Access
Excanon can traverse nested structures:
{
"obj": "user.profile.name"
}Even arrays:
{
"obj": "orders[0].total"
}For example:
facts = %{
"user" => %{
"profile" => %{
"name" => "John"
},
"orders" => [
%{"total" => 100},
%{"total" => 200}
]
}
}Then:
{"obj": "user.orders[1].total"}returns 200
Why Elixir is a Great Fit for Rule Engines
Rule engines benefit tremendously from Elixir’s strengths:
- Lightweight processes
- Fault tolerance
- Immutable data
- Pattern matching
- Functional composition
- Supervision trees
A rule engine becomes just another supervised service in your application:
children = [
{Excanon.StatefulRuleEngine, :order_engine}
]This allows business logic evaluation to scale naturally with your system.
Where Excanon Fits
Excanon is especially useful when:
✅ Business rules change frequently
✅ Rules should be configurable
✅ Product teams define policies
✅ Multiple applications share logic
✅ Logic needs to be stored externally
It may not be necessary when:
❌ Business rules rarely change
❌ Performance at nanosecond scale is critical
❌ The domain logic is extremely simple
Future Possibilities
Some interesting directions for rule Excanon include:
- Rule versioning
- Rule dependency graphs
- Event-driven execution
- DSL generation
- Rule visualization
- Hot rule reloading
- Distributed evaluation
- Explainable decision trees
Because Excanon represents rules as data, these capabilities become much easier to build.
Conclusion
Business logic changes.
Applications that hardcode every decision eventually become difficult to evolve.
Excanon attempts to solve this problem by moving business rules out of source code and into structured, executable data.
The result is an Elixir-native rule engine that is:
- Dynamic
- Configurable
- Stateful
- Extensible
- Production-friendly
If you’ve ever found yourself adding yet one more `if`, one more `case`, or one more feature flag, it may be time to consider a rule engine instead.
Repository:
Hex Package:
If you are interested, feel free to contribute to this project!