Stop Hardcoding Business Rules: Building Dynamic Rule Engines in Elixir with Excanon

5 min read Original article ↗

Juan Lovera

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"}
]
end

Then install:

mix deps.get

Creating 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!