Rule Engine Examples
This page collects end-to-end scenarios that demonstrate how to describe, validate, and execute rules. Each example builds on the core concepts covered in the other guides.
Example 1 — Basic pass with DSL and Java
DSL definition
{
"id": "orders",
"rules": [
{
"id": "active-order",
"name": "Active order",
"salience": 10,
"stages": [
{ "$match": { "status": "active" } }
]
}
]
}
Parsing & validation
String json = Files.readString(Path.of("orders-rule.json"));
RuleDslParser parser = new RuleDslParser();
RuleParseResult result = parser.parseWithLints(json);
if (result.hasLints()) {
throw new IllegalStateException("Rule issues: " + result.lints());
}
RuleSet ruleSet = result.ruleSet();
Runtime evaluation
RuleEngine engine = new RuleEngine();
Document doc = new Document(Map.of("status", "active"));
List<RuleEvaluationResult> results = engine.evaluate(List.of(doc), ruleSet);
boolean passed = !results.getFirst().passes().isEmpty();
Example 8 — DSL rule with actions and hooks
You can declare actions and hooks directly in the DSL as long as they are registered at runtime. The actions array references action names, while the optional hooks arrays reference rule and rule-set hooks by name (resolved from RuleHookRegistry).
{
"id": "loyalty-offers",
"name": "Loyalty Offers",
"rules": [
{
"id": "gold-upgrade",
"name": "Upgrade loyal customers",
"description": "Grant gold tier when spend exceeds threshold",
"salience": 500,
"stages": [
{ "$match": { "loyaltyTier": "silver" } },
{ "$match": { "lifetimeSpend": { "$gte": 2500 } } }
],
"actions": ["grant-gold-tier", "notify-upgrade"],
"hooks": ["audit-loyalty"]
}
],
"hooks": ["audit-ruleset"]
}
Minimal bootstrap code:
@Configuration
class LoyaltyRuleConfig {
@PostConstruct
void registerActsAndHooks() {
RuleActionRegistry.register("grant-gold-tier", ctx -> ctx.putSharedAttribute("upgrade", true));
RuleActionRegistry.register("notify-upgrade", ctx -> notificationClient.send(ctx.rule().name()));
RuleHookRegistry.registerRuleHook("audit-loyalty", new LoyaltyAuditHook());
RuleHookRegistry.registerRuleSetHook("audit-ruleset", new LoyaltyRuleSetHook());
}
}
After registration, the DSL snippet above can reference those names. The parser leaves actions and hooks untouched; at runtime, the rule builder resolves them and attaches the implementations before evaluation.
Example 9 — DSL with staged metadata and custom attributes
Metadata travels with the rule and is exposed via RuleExecutionContext.metadata() and RuleDefinition.metadata(). This is helpful for downstream logging, tracing, or business logic.
{
"id": "orders",
"version": "1.4.0",
"metadata": {
"owner": "payments-risk",
"runbook": "https://wiki/internal/runbooks/order-risk"
},
"rules": [
{
"id": "high-risk-order",
"name": "High risk order",
"salience": 800,
"metadata": {
"category": "fraud",
"severity": "high"
},
"stages": [
{ "$match": { "status": "pending" } },
{ "$match": { "riskScore": { "$gte": 0.9 } } }
],
"actions": ["flag-order"],
"hooks": ["audit-order"]
}
]
}
When evaluating a document:
RuleEvaluationResult result = engine.execute(List.of(orderDoc), ruleSet).getFirst();
RulePass pass = result.passes().getFirst();
Map<String, Object> metadata = pass.rule().metadata();
log.info("Rule {} fired with severity {}", pass.rule().id(), metadata.get("severity"));
This keeps governance data alongside the rule definition and makes it available to actions, hooks, and observability pipelines.
Example 2 — Actions and shared attributes
This scenario flags suspicious orders and records the decision in the shared context.
RuleActionRegistry.register("flag-order", context -> {
context.putAttribute("decision", "flagged");
context.putSharedAttribute("latestDecision", context.rule().name());
});
RuleDefinition suspicious = RuleDefinition.builder("Suspicious order")
.salience(100)
.condition(RuleCondition.pipeline(List.of(
new Stage(Map.of("$match", Map.of("status", "pending"))),
new Stage(Map.of("$match", Map.of("total", Map.of("$gte", 1000))))
)))
.addAction(RuleActionRegistry.resolve("flag-order").orElseThrow())
.build();
RuleSet ruleSet = RuleSet.builder().addRule(suspicious).build();
List<RuleEvaluationResult> results = new RuleEngine().execute(
List.of(new Document(Map.of("status", "pending", "total", 1500))),
ruleSet
);
RuleExecutionContext ctx = results.getFirst().passes().getFirst().context();
assert "flagged".equals(ctx.attributes().get("decision"));
assert "Suspicious order".equals(results.getFirst().sharedAttributes().get("latestDecision"));
When this runs in production, both shared and per-rule attributes can be logged or forwarded downstream.
Example 3 — Hooks for auditing
Register hook providers via the SPI (see Extensions & SPIs), then reference them by name:
RuleDefinition rule = RuleDefinition.builder("Audited rule")
.condition(RuleCondition.pipeline(List.of(
new Stage(Map.of("$match", Map.of("status", "active")))
)))
.addHookByName("audit-before")
.build();
RuleSet ruleSet = RuleSet.builder()
.addHookByName("audit-ruleset")
.addRule(rule)
.build();
The contributed hooks can log the evaluation timeline, enrich shared attributes, or emit metrics.
Example 4 — Debugging unexpected drops
Enable debug mode to discover which stage filtered a document out.
Document doc = new Document(Map.of("status", "inactive"));
RuleEvaluationResult result = new RuleEngine()
.evaluate(List.of(doc), ruleSet, true)
.getFirst();
RuleExecutionContext context = result.ruleContexts().getFirst();
RuleDebugStageTrace trace = context.debugTrace().getFirst();
System.out.printf("Stage %s filtered document (outputs=%d)\n",
trace.operator(), trace.outputs().size());
Each RuleDebugStageTrace contains snapshots of input/out documents, so you can attach them to support tickets or dashboards.
Example 5 — Lint feedback for authoring tools
Provide rich feedback to rule authors before deployment:
RuleParseResult result = new RuleDslParser().parseWithLints(json);
if (result.hasLints()) {
for (RuleLint lint : result.lints()) {
ui.notify(lint.type(), lint.message(), lint.context());
}
}
Typical lint payload:
{
"type": "UNSUPPORTED_OPERATOR",
"ruleName": "Inactive rule",
"message": "Rule 'Inactive rule' uses unsupported operator '$foo' at position 1",
"context": {
"stageIndex": 0,
"operator": "$foo"
}
}
Helping authors correct rules early prevents surprises during runtime deployment.
Example 6 — Combining multiple rules with salience bands
RuleDefinition allowLoyalty = RuleDefinition.builder("Allow loyalty member")
.salience(200)
.condition(RuleCondition.pipeline(List.of(
new Stage(Map.of("$match", Map.of("loyalty", true)))
)))
.build();
RuleDefinition escalateFraud = RuleDefinition.builder("Escalate fraud")
.salience(900)
.condition(RuleCondition.pipeline(List.of(
new Stage(Map.of("$match", Map.of("flags", Map.of("$in", List.of("fraud")))))
)))
.build();
RuleSet ruleSet = RuleSet.builder()
.addRule(allowLoyalty)
.addRule(escalateFraud)
.build();
Because salience values differ, the escalateFraud rule executes first. When both pass, two RulePass objects are returned, in salience order.
Example 7 — Rule-set hooks for cross-document context
RuleSetHook auditHook = new RuleSetHook() {
@Override
public void beforeRules(Document doc, Map<String, Object> shared) {
shared.put("auditStart", Instant.now());
}
@Override
public void afterRules(Document doc, Map<String, Object> shared, List<RulePass> passes) {
Duration elapsed = Duration.between((Instant) shared.get("auditStart"), Instant.now());
metrics.record("rules.elapsed", elapsed.toMillis());
}
};
RuleSet ruleSet = RuleSet.builder()
.addHook(auditHook)
.addRule(rule)
.build();
Hook output (e.g. metrics recording) applies once per document regardless of how many rules passed.