When Marketing Changes Its Mind at 5 PM: Grule Rule Engine and the Promotion Problem
Tech Blog · Golang · Business Rule EngineReading 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:
- Dev opens a PR.
- Reviewer asks, “Can this stack with vouchers?”
- QA runs regression tests across the entire checkout flow.
- Build → Deploy.
- …all just to change
20to15.
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:
- Salience — higher-priority rules take precedence.
- Rule order.
- 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
Retractcalled?
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-elsechain.
- 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
ifstatements.
- 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?