Skip to main content

·

Everything about Grule Engine

When Marketing Changes Its Mind at 5 PM: Grule Rule Engine and the Promotion Problem

Tech Blog · Golang · Business Rule Engine
Reading time: ~10 minutes

a848995a-2918-4b48-b428-5487b0edf503.png
Today, VIPs get a 20% discount.
Tomorrow, it's down to 15%.
Next week, Flash Sale. Next month, Birthday Deals, Member Day, Brand Deals, voucher stacking… and the same familiar question during standup:
“We're just changing a number. Why do we need to deploy?”
That's when you realize: your business logic is living in the wrong place.

It All Starts with an if Statement

Initially, a Promotion Engine seems pretty “simple”:
if user.IsVIP && order.Amount > 1_000_000 { discount = 20 } else if order.Amount > 500_000 { discount = 10 } if campaign == "SUMMER" { discount += 5 } if user.NewUser { discount += 10 } if payment == "VISA" { discount += 5 }
Six months later, that code is no longer just a piece of code. It's become a plate of spaghetti.
Every time the business changes a rule:
  1. Dev opens a PR.
  1. Reviewer asks, “Can this stack with vouchers?”
  1. QA runs regression tests across the entire checkout flow.
  1. Build → Deploy.
  1. …all just to change 20 to 15.
The failed formula:
Business changes one number → Engineering has to run the entire pipeline.
When promotion logic is tightly coupled to application code, you're no longer just building a service. More accurately, you're building a CMS in Go.
image.png

Grule Enters the Scene as a “Judge”

Grule isn't a library for “writing prettier if statements.” It's a Business Rule Engine (BRE) for Golang: you provide facts, and the engine matches them against rules, prioritizes them, and fires the applicable ones.
Instead of hardcoding logic in Go:
rule VIPDiscount "VIP Discount" salience 100 { when User.IsVIP == true && Order.Amount > 1000000 then Discount.Value = 20; Retract("VIPDiscount"); }
rule NewUser { when User.NewUser == true then Discount.Value += 10; }
rule VisaPayment { when Payment.Method == "VISA" then Discount.Value += 5; }
The application is left with just:
engine.Execute(dataContext)
You don't invoke each rule individually. You don't write an 800-line else if chain. You provide the data. Grule determines which rules should fire.

A Common Promotion Engine Architecture

image.png
The key point: the service doesn't need to know the specifics of each business rule. It simply provides the data and receives the result.

Why Is a Rule Engine a Good Fit for Constantly Changing Systems?

1. Separate the “Rules of the Game” from the “Playing Field”

The application handles:
  • APIs, databases, message queues, and caches
  • Logging, monitoring, quotas, and lifecycle management
Business rules live separately:
promotion.grl voucher.grl shipping.grl member.grl
When Marketing wants to introduce a ShopeePay Promotion, you add a new rule without having to dissect the existing VIP rules.
image.png

2. Move at the Speed of Business, Not the Release Cycle

Hardcoded Logic
Grule
Change code → build → deploy
Update rules → reload
Every campaign requires a PR
Every campaign can be a rule
Classic review conflicts
Built-in conflict resolution (salience)

3. Manage Hundreds of Rules Without Losing Your Mind

A real-world Promotion Engine might support Flash Sales, Vouchers, Brands, Categories, Birthdays, Payment Methods, Membership Levels, Campaigns, Suppliers, Shipping, and more.
If everything lives in a single Go file, maintainability stops being an engineering discipline and becomes a survival challenge.
With salience, VIP rules (100) can take precedence over Voucher rules (50). The engine uses priorities rather than the principle of “whoever wrote the code first wins.”

4. Forward Chaining: One Rule Triggers Another

This is the most interesting part, and also the easiest to misunderstand.
image.png
You don't invoke every step manually. When Working Memory changes, the engine reevaluates the relevant conditions, and other rules can become eligible to fire automatically. That's why Grule is called an Inference Engine, not just a DSL parser.

Inside the Black Box

If you think of Grule as nothing more than a collection of .grl files, you're missing the most interesting part: it is based on the Rete Algorithm, the same family of rule-matching techniques used by Drools, but implemented for Go.

High-Level Architecture

image.png

The Five Main Components

Component
Brief Description
Rule DSL
Rules written using when / then syntax rather than Go
RuleBuilder
The rule “compiler”: Lexer → Parser → AST
KnowledgeBase
The rule repository — contains logic, not application data
DataContext
The bridge between User.IsVIP in the DSL and user.IsVIP in Go
Engine
Matches rules → manages the Agenda → executes actions

The Rule DSL Looks Like a Miniature SQL

rule VIPDiscount "VIP Discount" salience 100 { when User.IsVIP && Order.Total > 1000000 then Discount.Value += 20; Retract("VIPDiscount"); }
Its structure is roughly:
rule when then
The DSL is not Go code. Grule parses it into an AST before execution.

DataContext & Working Memory

dataCtx.Add("User", user) dataCtx.Add("Order", order) dataCtx.Add("Discount", discount)
All facts are added to Working Memory. Rules read from this data rather than querying the database or calling APIs directly. This helps keep rule evaluation fast and predictable.
image.png

Deep Dive: How Does the Engine Actually Run?

1. Rule Matching → Agenda

The engine evaluates facts against rule conditions. Rules whose conditions are satisfied are added to the Agenda, a queue of pending rule activations. They don't necessarily fire immediately.
image.png
For example, the Agenda might look like this after matching:
VIP salience 100 Birthday salience 80 Voucher salience 50 Visa salience 40

2. Salience & Conflict Resolution

When multiple rules are eligible to fire, the engine uses a conflict-resolution strategy involving:
  1. Salience — higher-priority rules take precedence.
  1. Rule order.
  1. Insertion order.
This can help produce consistent results, provided priorities and conflict-resolution behavior are deliberately designed.

3. Retract: The Emergency Brake Against Infinite Loops

After a rule fires, its condition may still be true, allowing it to be activated again and potentially causing an infinite loop.
Retract("VIPDiscount")
Retract isn't just decorative syntax. Without proper reactivation control and a well-designed salience scheme, you could end up with a “dead inside” debugging session at 2 AM.

4. The Rete Algorithm — The Heart of Grule

Suppose you have:
  • 1,000 rules
  • 1,000 facts
A naive approach might repeatedly evaluate every rule against every relevant combination of facts, leading to substantial redundant work.
Rete builds a network of conditions. When a fact such as User changes, the engine can reuse previously evaluated conditions and update the relevant parts of the network instead of recomputing everything from scratch.
image.png
Node
What It Does
Alpha
Evaluates conditions on individual facts (VIP == true, Age > 18)
Beta
Combines facts and evaluates relationships (User.ID == Order.UserID)
Terminal
Represents a completed rule match that can activate a rule

5. The Execution Loop

Conceptually, the process looks like this:
for { matchedRules := Match() if len(matchedRules) == 0 { break } highest := Agenda.Pop() Execute(highest) UpdateWorkingMemory() }
The loop continues until no more rules are eligible to fire.
image.png

Lessons from Production

The theory looks great. Production is a little messier.

1. Business Users Usually Don't Want to Write .grl

They want forms in a Portal or CMS. A common pattern looks like this:
image.png
The DSL is still a “machine language.” The UI is the “human language.”

2. Compilation Isn't Free

Parsing rules and building the Rete network have a cost. For a hot path like checkout, cache the compiled KnowledgeBase, invalidate it when rules change, and avoid recompiling everything on every request.

3. Priority in the Database = Salience in the Engine

One number, two worlds. If the mapping is wrong, “VIP before Voucher” on your slide becomes “Voucher before VIP” in production.

4. Independent Rules on Paper Can Still Conflict in Real Life

Two rules may not modify each other in the source code, yet still conflict when they both write to Discount.
You need clear conventions:
  • Which rules can stack?
  • Which rules are exclusive?
  • Which rules need to call Retract?

5. Debugging Is Harder Than with if Statements, but You Can Learn from It

When multiple rules modify the same fact, log the following:
  • Which rules matched?
  • What was each rule's salience?
  • Which facts changed after each rule fired?
  • Was Retract called?
Without observability into the Agenda, debugging becomes guesswork.

Pros & Cons — Let's Be Honest

Advantages

  • Separates business logic from source code — cleaner application code and easier maintenance.
  • Extends through new rules rather than modifying a massive if-else chain.
  • Provides priority, conflict resolution, and forward chaining out of the box.
  • Can perform well with large rule sets by reusing matching results through Rete.
  • Encourages more independent rules, reducing the risk that changing A will break B.

Disadvantages

  • Steep learning curve — Working Memory, Agenda, and Rete aren't concepts you master in an afternoon.
  • Harder to debug than reading sequential if statements.
  • Overkill when you only have a few fixed conditions.
  • Poor rule design (missing retraction or chaotic salience values) can lead to loops or unexpected results.
  • 500 rules without conventions can create a new maze; you've just changed the language.
Grule doesn't eliminate complexity. It moves complexity to a place where it can be managed — provided you design the system properly.

Conclusion: Choose the Tool, Not the Trend

Grule isn't magic that lets the business change rules at 5 PM without involving anyone. Nor is it something you need to force into every Go service.
Its greatest value isn't in the syntax:
rule ... when ... then ...
It's in having the courage to acknowledge:
“Business rules don't belong in the binary we deployed last week.”
A good fit when:
  • Business rules change frequently: Promotions, Pricing, Eligibility, Fraud, Policies, and Approval Workflows.
  • The number of rules is large and continues to grow.
  • You want to decouple campaign releases from engineering release cycles.
Not a good fit when:
  • You only have a few fixed conditions that rarely change.
  • Your team isn't ready for the learning curve.
  • You haven't established conventions for naming, grouping, and reviewing rules.
For promotion systems where today's discount is 20%, tomorrow's is 15%, and next month's campaign count reaches 50, a Rule Engine can help your codebase remain maintainable beyond its second year.
For a form that validates three fields?
if-else is still your most loyal friend.
Choosing the right tool for the right problem will always matter more than adopting a technology simply because it sounds “enterprise-grade.”

References

  • A comparison of concepts with Drools / BRMS for developers coming from the Java ecosystem

If you're considering introducing a Rule Engine into a Golang service, the most important question isn't “Is Grule powerful?” It's: How often do your business rules change, and who is allowed to change them?

Subscribe to the newsletter

Get new posts on Backend, Blockchain, DevOps, and life notes.

Loading Comments...