Introduction & Overview
Introduction to Axiom Business Rules
What is Axiom?
Axiom is a lightweight rule engine designed to simplify complex βif-this-then-thatβ business logic. In real-world applicationsβespecially microservices and modular systemsβdevelopers often end up with conditionals scattered throughout their codebase. Over time, these if-else chains become fragile, difficult to maintain, and nearly impossible to extend without risking regressions. Larger, enterprise-grade rule engines can solve this, but theyβre often too heavyweight for everyday needs.
How Axiom Helps
-
Lightweight, Focused Rule Engine
Axiom doesnβt pretend to solve every rule-related problem but focuses on making common patterns like βif X then do Yβ easier and more maintainable, without requiring hours of configuration or specialized DSLs. -
Separation of Concerns
By extracting business rules into discrete βaxioms,β you keep your logic out of tangled if-else blocks. That separation makes rules easier to read, reason about, and modifyβno more rummaging through complex conditional trees. -
Extensible & Modular
Axiomβs approach to rules is modular: you can add, remove, or update them without rewriting core code. This makes it simpler to support new features or business changes. -
Simplicity
You donβt have to install a large, enterprise-grade rules server or learn a complicated syntax. Axiomβs API aims for clarity and minimal overhead, so teams can adopt it quickly.
Where Axiom Fits In Your Architecture
Consider how often you need to write logic like:
if (order.isPriority() && !order.isFlagged()) {
// expedite shipping
} else if (order.isInternational()) {
// handle customs
} else if (order.hasGiftWrap()) {
// add gift wrap processing
}
With Axiom, you extract these conditions into reusable business checks and actions, making your code more maintainable and testable.
Key Benefits
Separation of Concerns
Business rules are maintained separately from application code, allowing business analysts and developers to work collaboratively. Rules are stored in YAML files that can be managed independently of the application code.
rules:
- name: "High Risk Score Rule"
description: "Block requests with very high risk scores"
expression: hasRiskScore(90) then blockRequest()
priority: 80
effectiveFrom: "2023-05-15T00:00:00Z"
Flexibility
Rules can be modified without redeploying the entire application. The rule engine can load rule definitions from various sources including file systems and databases. You can update business logic by simply modifying YAML files.
Traceability
Each decision made by the system can be traced back to specific rules. Axiom provides detailed execution results including which rules were evaluated and why they matched or didn't match.
RuleExecutionResult<TestCtxKey> result = orchestrator.executeFirstMatchingRule(context);
if (result.hasMatch()) {
BusinessRule<TestCtxKey> matchedRule = result.getMatchedRule();
System.out.println("Rule applied: " + matchedRule.getName());
}
Maintainability
Rules are expressed in a clear, readable format using a business-friendly syntax. This makes them easier to understand, audit, and maintain over time.
Testability
Rules can be tested independently from the application logic. Axiom provides a comprehensive testing framework to verify rule behavior.
@Test
void testFraudSignalsRule() {
RuleContext<TestCtxKey> context = new RuleContext<>(TestCtxKey.class);
context.add(TestCtxKey.HAS_FRAUD_SIGNALS, true);
RuleExecutionResult<TestCtxKey> result =
fraudDetectionOrchestrator.executeFirstMatchingRule(context);
assertThat(result.hasMatch()).isTrue();
assertThat(result.getMatchedRule().getName()).isEqualTo("Fraud Detection Rule");
}
Getting Started with Axiom Business Rules
This guide aims to provide you with:
- A clear understanding of Axiom business rules concepts
- Step-by-step instructions for creating and testing rules
- Best practices for rule development
- Real-world examples based on actual implementations
- Troubleshooting tips
Architecture Overview
Business Rule
A βruleβ is a small, self-contained piece of logic that says: βWhen certain conditions are met, perform these action(s).β Each rule has two main parts: - Condition(s) β A Boolean check (or set of checks combined using logical operators) that determins if the rule should fire. - Action(s) β One or more pieces of code to execute when the conditions evaluate to true.
Rule Sets
Collections of business rules combined with metadata and priority. Rule-sets group related rules together and maintain rule priority ordering. By default loaded from YAML files.
Rule Parser
Converts rule definitions to executable objects. The rule parser interprets the rule expressions and creates the appropriate Java objects.
Business Checks
Functions that represent declared conditions and are implemented as the BusinessCheck<K>
interface. They can take (0..N) parameters and return Value
objects of the type Value.Type.BOOLEAN
indicating whether a condition is met.
@RuleMetadata(name = "hasRiskScore", description = "Checks if the risk score is above a specified threshold")
public class HasRiskScoreCheck implements BusinessCheck<ContextKey> {
public Value execute(RuleContext<ContextKey> context, @Arg("threshold") Value threshold) {
Integer riskScore = context.getRequired(ContextKey.RISK_SCORE, Integer.class);
Integer thresholdValue = threshold.asNumber().intValue();
return Value.of(riskScore >= thresholdValue);
}
}
Business Actions
Functions that perform actions when rules match. Business actions are implemented as Java classes that implement the BusinessAction<K>
interface. They are executed when a rule's condition is met.
@RuleMetadata(name = "blockRequest", description = "Blocks the suspension request entirely")
public class BlockRequestAction implements BusinessAction<ContextKey> {
@Override
public Value execute(RuleContext<ContextKey> context) {
context.add(ContextKey.REQUEST_BLOCKED, true);
return Value.of(true);
}
}
Rule Context
A thread-safe container for data being processed by rules. The rule context provides a type-safe way to store and retrieve data during rule evaluation.
RuleContext<ContextKey> context = new RuleContext<>(ContextKey.class);
context.add(ContextKey.CUSTOMER_ID, "C12345");
context.add(ContextKey.TRANSACTION_AMOUNT, 9999.99);
Rule Orchestrator
Coordinates rule evaluation and execution. The rule orchestrator applies rules from a rule set against a given context and provides methods to execute rules and retrieve results.
RuleOrchestrator<ContextKey> orchestrator = new RuleOrchestrator<>(ruleSet);
RuleExecutionResult<ContextKey> result = orchestrator.executeFirstMatchingRule(context);
This modular design allows for flexible integration with existing Java applications using dependency injection frameworks like Guice.
How to Use This Guide
The guide is organized in a logical progression, starting with basic concepts and moving toward more advanced topics. If you're new to Axiom, we recommend starting with the Getting Started section. Experienced users may want to jump directly to specific topics using the navigation menu.
Each section includes practical examples based on real-world use cases and actual implementations of the Axiom framework.
Let's begin your journey with Axiom Business Rules!
Prerequisites
Before you begin, ensure you have the following:
- Java 8 or higher
- Maven or Gradle for dependency management
- Basic understanding of Java and dependency injection (Guice is used in this guide)
Adding Axiom to Your Project
Maven
Add the Axiom dependency to your pom.xml
:
<dependency>
<groupId>com.lyxtera</groupId>
<artifactId>axiom-rules</artifactId>
<version>1.0.1</version>
</dependency>
<!-- For Spring Boot integration (optional) -->
<dependency>
<groupId>com.lyxtera</groupId>
<artifactId>axiom-spring-boot-starter</artifactId>
<version>1.0.1</version>
</dependency>
<!-- Guice for dependency injection -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>5.1.0</version>
</dependency>
Gradle
Add the Axiom dependency to your build.gradle
:
dependencies {
implementation 'com.lyxtera:axiom-rules:1.0.1'
// For Spring Boot integration (optional)
implementation 'com.lyxtera:axiom-spring-boot-starter:1.0.1'
implementation 'com.google.inject:guice:5.1.0'
}
Basic Setup Steps
Setting up Axiom involves the following steps:
- Define your context keys
- Create business checks and actions
- Create rule set YAML files
- Configure the Axiom module
- Create and use rule orchestrators
Let's go through each step in detail.
1. Define Your Context Keys
Create an enum to define the keys for your rule context:
public enum OrderContextKey {
CUSTOMER_ID,
ORDER_AMOUNT,
PRODUCT_IDS,
IS_REPEAT_CUSTOMER,
HAS_DISCOUNT_APPLIED,
SHIPPING_COUNTRY
}
2. Create Business Checks and Actions
Create implementations of BusinessCheck
and BusinessAction
interfaces:
Business Check Example
@RuleMetadata(
name = "isHighValueOrder",
description = "Checks if the order value exceeds a threshold"
)
public class HighValueOrderCheck implements BusinessCheck<OrderContextKey> {
@Override
public Value execute(RuleContext<OrderContextKey> context, @Arg("threshold") Value threshold) {
Double orderAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
Double thresholdValue = threshold.asNumber().doubleValue();
return Value.of(orderAmount > thresholdValue);
}
}
Business Action Example
@RuleMetadata(
name = "applyDiscount",
description = "Applies a percentage discount to the order"
)
public class ApplyDiscountAction implements BusinessAction<OrderContextKey> {
@Override
public Value execute(RuleContext<OrderContextKey> context, @Arg("percent") Value percent) {
Double orderAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
Double discountPercent = percent.asNumber().doubleValue();
Double discountedAmount = orderAmount * (1 - (discountPercent / 100));
context.add(OrderContextKey.ORDER_AMOUNT, discountedAmount);
context.add(OrderContextKey.HAS_DISCOUNT_APPLIED, true);
return Value.of(true);
}
}
3. Create Rule Set YAML Files
Create a YAML file to define your rule set:
# src/main/resources/order_discount_rules.yaml
rulesetName: "Order Discount Rules"
rulesetDescription: "Rules for applying discounts to orders"
businessChecks:
- name: isHighValueOrder
description: Checks if the order value exceeds a threshold
params:
- threshold
- name: isRepeatCustomer
description: Checks if the customer has previous orders
businessActions:
- name: applyDiscount
description: Applies a percentage discount to the order
params:
- percent
rules:
- name: "High Value Order Discount"
description: "Apply 10% discount to orders over $1000"
expression: isHighValueOrder(1000) then applyDiscount(10)
priority: 10
effectiveFrom: "2023-01-01T00:00:00Z"
effectiveTo: "2025-04-01T00:00:00Z"
- name: "Repeat Customer Discount"
description: "Apply 5% discount to repeat customers"
expression: isRepeatCustomer() then applyDiscount(5)
priority: 20
effectiveFrom: "2023-01-01T00:00:00Z"
4. Configure the Axiom Module
You can incorporate Axiom directly into your main application Guice module using the builder pattern:
public class YourApplicationGuiceModule extends AbstractModule {
@Override
protected void configure() {
// Install Axiom module directly using the builder
install(AxiomModule.buildForKey(OrderContextKey.class)
.withRuleLoaders(loaders -> loaders
.loader("order_discount", new YamlRuleSetLoader<>("order_discount_rules.yaml"))
)
.withChecks(checks -> checks
.check("isHighValueOrder", HighValueOrderCheck.class)
.check("isRepeatCustomer", RepeatCustomerCheck.class)
)
.withActions(actions -> actions
.action("applyDiscount", ApplyDiscountAction.class)
)
.build());
// Your application bindings
bind(OrderService.class);
// ... other bindings
}
}
This approach has several advantages: - Integrates Axiom directly into your application's module structure - Avoids creating a separate configuration class or method - Keeps all module configuration in one place - Allows for more complex dependency relationships between Axiom and your application - Provides cleaner access to RuleOrchestrators and other Axiom components
5. Create and Use Rule Orchestrators
Now you can use the rule orchestrator in your application:
public class OrderService {
private final RuleOrchestrator<OrderContextKey> discountOrchestrator;
@Inject
public OrderService(@Named("order_discount") RuleOrchestrator<OrderContextKey> discountOrchestrator) {
this.discountOrchestrator = discountOrchestrator;
}
public Order processOrder(Order order) {
// Create a rule context with order data
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
context.add(OrderContextKey.CUSTOMER_ID, order.getCustomerId());
context.add(OrderContextKey.ORDER_AMOUNT, order.getTotalAmount());
context.add(OrderContextKey.IS_REPEAT_CUSTOMER, isRepeatCustomer(order.getCustomerId()));
// Execute all matching rules
RuleExecutionResult<OrderContextKey> result = discountOrchestrator.executeAllMatchingRules(context);
// Update the order with the potentially modified amount
if (result.hasMatches()) {
Double discountedAmount = result.getContext().getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
order.setTotalAmount(discountedAmount);
// Log which rules were applied
result.getMatchedRules().forEach(rule ->
System.out.println("Applied rule: " + rule.getName()));
}
return order;
}
private boolean isRepeatCustomer(String customerId) {
// Implementation to check if this is a repeat customer
return true; // Simplified for this example
}
}
Setting Up the Application
Finally, set up your application with Guice using your main application module:
public class Application {
public static void main(String[] args) {
// Create the Guice injector with your main application module
Injector injector = Guice.createInjector(new YourApplicationGuiceModule());
// Get the order service
OrderService orderService = injector.getInstance(OrderService.class);
// Create and process an order
Order order = new Order("CUST-12345", 1500.0);
Order processedOrder = orderService.processOrder(order);
System.out.println("Original amount: $1500.00");
System.out.println("Processed amount: $" + processedOrder.getTotalAmount());
}
}
This approach simplifies your application bootstrap process and follows standard Guice practices for modular applications.
Next Steps
Now that you have a basic understanding of how to set up and use Axiom, you can explore more advanced topics:
- Rule Sets Overview - Learn more about rule sets and their structure
- Rule Context Overview - Understand how to work with rule contexts
- Business Actions & Checks Overview - Dive deeper into business actions and checks
- Rule Orchestrators Overview - Explore advanced orchestrator features
By following this guide, you should now have a working Axiom integration in your application. You can expand on this foundation by adding more complex rules, checks, and actions to meet your business requirements.
Axiom Code Classes Overview
This section provides a comprehensive overview of the key classes in the Axiom framework and how they interact with each other.
Core Classes Diagram
Below is a conceptual diagram showing the main components of the Axiom framework and their relationships:
βββββββββββββββββββ βββββββββββββββββββββ ββββββββββββββββββββββ
β RuleSetLoader βββββββΆβ RuleSet ββββββββ BusinessRule β
βββββββββββββββββββ βββββββββββββββββββββ ββββββββββββββββββββββ
β² β²
β β
β β
βββββββββββββββββββ βββββββββββββββββββββ ββββββββββββββββββββββ
β RuleOrchestratorβββββββΆβ RuleContext ββββββββ RuleExpression β
βββββββββββββββββββ βββββββββββββββββββββ ββββββββββββββββββββββ
β² β² β²
β β β
β β β
βββββββββββββββββββ βββββββββββββββββββββ ββββββββββββββββββββββ
β AxiomModule β β BusinessCheck β β BusinessAction β
βββββββββββββββββββ βββββββββββββββββββββ ββββββββββββββββββββββ
Package Structure
The Axiom framework is organized into the following main packages:
Package | Description | Key Classes |
---|---|---|
com.lyxtera.axiom.api.model |
Core API interfaces and models | BusinessRule , BusinessCheck , BusinessAction , Value |
com.lyxtera.axiom.engine |
Rule execution engine components | RuleSet , RuleOrchestrator , RuleContext , RuleSetLoader |
com.lyxtera.axiom.parser |
Rule expression parsing | RuleExpressionParser , RuleExpression , ConditionVisitor |
com.lyxtera.axiom.config |
Configuration and dependency injection | AxiomModule , RuleMetadata |
com.lyxtera.axiom.api.exception |
Exception classes | AxiomEngineException , RuleLoadException |
Key Classes Overview
Rule Modeling Classes
- BusinessRule\<K>: The core interface for business rules, with methods for rule metadata, condition evaluation, and action execution.
- BusinessCheck\<K>: Interface for implementing condition evaluation functions.
- BusinessAction\<K>: Interface for implementing action execution functions.
- Value: A class representing values passed to and from business checks and actions, with support for different types (Boolean, Number, String, etc.).
Rule Execution Classes
- RuleContext\<K>: A thread-safe container for data being processed by rules, using an enum for keys.
- RuleSet\<K>: A collection of business rules, prioritized and ordered.
- RuleOrchestrator\<K>: The main entry point for rule execution, which evaluates and executes rules based on context.
- RuleExecutionResult\<K>: Captures the result of rule execution, including the executed rule and success status.
Rule Loading and Parsing
- RuleSetLoader\<K>: Abstract class for loading rule sets from various sources.
- YamlRuleSetLoader\<K>: Implementation of RuleSetLoader that loads rules from YAML files.
- RuleExpressionParser\<K>: Parses rule expressions in the Axiom DSL format.
- RuleExpression\<K>: Represents a parsed and executable rule expression.
Configuration Classes
- AxiomModule\<K>: Base Guice module for configuring Axiom components.
- RuleMetadata: Annotation for business checks and actions to provide metadata.
- RuleSetDescriptor: Describes a rule set and its components in YAML.
Class Inheritance Hierarchy
The following diagram illustrates the inheritance hierarchy of the main Axiom classes:
BusinessRule<K> (Interface)
β²
β
β
βββββββββββββββ΄βββββββββββββββ
β β
DefaultBusinessRule<K> CompoundBusinessRule<K>
BusinessCheck<K> (Interface)
β²
β
β
βββββββββββββ΄βββββββββββββ
β β
User Implementations AbstractParameterizedCheck<K>
BusinessAction<K> (Interface)
β²
β
β
βββββββββββββ΄βββββββββββββ
β β
User Implementations AbstractParameterizedAction<K>
RuleSetLoader<K> (Abstract)
β²
β
β
YamlRuleSetLoader<K>
AxiomModule<K> (Abstract)
β²
β
β
User Implementation
Typical Workflow
The typical workflow when using Axiom involves:
- Configuration: Create an
AxiomModule<K>
to register rule sets, business checks, and business actions. - Rule Definition: Define rules in YAML files with conditions and actions.
- Rule Loading: Use
RuleSetLoader<K>
to load rule sets from YAML files. - Context Preparation: Create a
RuleContext<K>
with the necessary data for rule evaluation. - Rule Execution: Use
RuleOrchestrator<K>
to evaluate and execute rules based on the context. - Result Processing: Handle the
RuleExecutionResult<K>
to determine what action was taken.
Related Sections
For more detailed information about each component, refer to the following sections:
- Rule Sets - Detailed information about rule set organization and structure
- Rules - In-depth explanation of rule definition and properties
- Rule Context - How to work with the rule context
- Business Actions & Checks - Creating condition and action components
- Rule Orchestrators - Using the orchestrator to execute rules
Development Guide
RuleSets
Rule Sets Overview
Rule Sets are collections of related business rules organized for a specific purpose. This section explains how rule sets are structured, loaded, and managed in the Axiom framework.
What is a Rule Set?
A Rule Set (RuleSet<K>
) in Axiom is a container for business rules that:
- Groups related rules together
- Maintains rule priority ordering
- Handles effective date filtering
- Provides metadata about the contained rules
Rule Sets are typically loaded from YAML files that define the ruleset name, description, business checks, business actions, and the rules themselves.
Key Responsibilities of a Rule Set
Rule Storage
The Rule Set maintains a collection of business rules, each with its own priority and effective date range. This collection is managed internally and can be accessed via methods like getRulesInPriorityOrder()
.
Rule Ordering
Rules are ordered by priority, with lower numbers indicating higher priority. For example, a rule with priority 10 will be considered before a rule with priority 20. This ordering is maintained internally by the Rule Set.
// The rule with priority 10 will be evaluated before the rule with priority 20
ruleSet.addRule(highPriorityRule, 10, effectiveFrom);
ruleSet.addRule(lowerPriorityRule, 20, effectiveFrom);
Effective Date Filtering
Rule Sets filter rules based on their effective date ranges. This allows rules to be time-bound, activating and deactivating automatically based on the current date.
// This rule is only effective from January 1, 2023 to December 31, 2023
ZonedDateTime effectiveFrom = ZonedDateTime.parse("2023-01-01T00:00:00Z");
ZonedDateTime effectiveTo = ZonedDateTime.parse("2023-12-31T23:59:59Z");
ruleSet.addRule(seasonalRule, 30, effectiveFrom, effectiveTo);
// This rule is effective from January 1, 2023 with no end date
ruleSet.addRule(permanentRule, 40, effectiveFrom);
Rule Selection
Rule Sets provide methods to retrieve rules for evaluation based on their priority and effective date range.
// Get all rules that are currently effective, in priority order
List<BusinessRule<MyContextKey>> activeRules = ruleSet.getRulesInPriorityOrder();
Creating a Rule Set
Rule Sets are typically created by a RuleSetLoader
, but can also be created programmatically:
// Create an empty rule set
RuleSet<MyContextKey> ruleSet = new RuleSet<>();
// Add a rule to the rule set
BusinessRule<MyContextKey> rule = createRule();
ruleSet.addRule(rule, 10, ZonedDateTime.parse("2023-01-01T00:00:00Z"));
// Add a rule with both effective from and to dates
BusinessRule<MyContextKey> anotherRule = createAnotherRule();
ruleSet.addRule(
anotherRule,
20,
ZonedDateTime.parse("2023-01-01T00:00:00Z"),
ZonedDateTime.parse("2023-12-31T23:59:59Z")
);
Rule Set Loaders
The more common way to create Rule Sets is using a RuleSetLoader
, which loads rule definitions from an external source. The standard implementation is YamlRuleSetLoader
:
// Create a loader for a YAML file
RuleSetLoader<MyContextKey> loader =
new YamlRuleSetLoader<>("order_approval_rules.yaml");
// Load the rule set
RuleSet<MyContextKey> ruleSet = loader.loadRuleSet();
The YamlRuleSetLoader
is the standard implementation, but you can create custom loaders for other sources like databases, remote APIs, or other file formats by implementing the RuleSetLoader
interface.
Rule Set YAML Format
Rule Sets are typically defined in YAML files, which provide a human-readable format for expressing rules. Here's an example of a complete ruleset YAML file:
rulesetName: "Fraud Detection Ruleset"
rulesetDescription: "Rules for detecting fraudulent transactions"
businessChecks:
- name: hasFraudSignals
description: Determines if the transaction contains signals that indicate potential fraudulent activity
- name: hasRiskScore
description: Checks if the risk score is above a specified threshold
params:
- threshold
- name: isNewCustomer
description: Checks if the customer is new (less than 30 days)
businessActions:
- name: blockTransaction
description: Blocks the transaction entirely
- name: flagForReview
description: Flags the transaction for manual review
- name: addVerificationStep
description: Adds an additional verification step to the transaction process
rules:
- name: "Known Fraud Signals Rule"
description: "Block transactions with known fraud signals"
expression: hasFraudSignals() then blockTransaction()
priority: 10 # Highest priority
effectiveFrom: "2023-01-01T00:00:00Z"
- name: "High Risk Score Rule"
description: "Flag transactions with very high risk scores for review"
expression: hasRiskScore(90) then flagForReview()
priority: 20
effectiveFrom: "2023-01-01T00:00:00Z"
- name: "New Customer with High Value Rule"
description: "Add verification for new customers with large transactions"
expression: isNewCustomer() and hasTransactionAmount(1000) then addVerificationStep()
priority: 30
effectiveFrom: "2023-01-01T00:00:00Z"
effectiveTo: "2023-12-31T23:59:59Z" # This rule expires at the end of 2023
The YAML file consists of:
- Metadata: Rule set name and description
- Business Checks: Definitions of the checks that can be used in rule expressions
- Business Actions: Definitions of the actions that can be performed when rules match
- Rules: Definitions of the actual business rules, including their expressions, priorities, and effective dates
Rule Set Metadata
Rule Sets contain metadata that describes the contents of the rule set, including the business checks, business actions, and rules. This metadata is useful for documentation, validation, and introspection.
// Access the metadata of a rule set
RuleSet.Metadata metadata = ruleSet.getMetadata();
// Get the name and description of the rule set
String name = metadata.getRuleSetName();
String description = metadata.getRuleSetDescription();
// Get information about a business check
BusinessCheckDescriptor checkDescriptor = metadata.getBusinessCheckDescriptor("hasRiskScore");
if (checkDescriptor != null) {
String checkDescription = checkDescriptor.getDescription();
List<String> paramNames = checkDescriptor.getParams();
}
// Get information about a business action
BusinessActionDescriptor actionDescriptor = metadata.getBusinessActionDescriptor("blockTransaction");
if (actionDescriptor != null) {
String actionDescription = actionDescriptor.getDescription();
}
Integration with Rule Orchestrator
Rule Sets are typically used with a Rule Orchestrator, which handles the evaluation of rules against a given context:
// Create a rule orchestrator for a rule set
RuleOrchestrator<MyContextKey> orchestrator = new RuleOrchestrator<>(ruleSet);
// Create a context for rule evaluation
RuleContext<MyContextKey> context = new RuleContext<>(MyContextKey.class);
context.add(MyContextKey.TRANSACTION_AMOUNT, 5000.0);
context.add(MyContextKey.RISK_SCORE, 95);
// Execute the first matching rule
RuleExecutionResult<MyContextKey> result = orchestrator.executeFirstMatchingRule(context);
if (result.hasMatch()) {
BusinessRule<MyContextKey> matchedRule = result.getMatchedRule();
System.out.println("Rule applied: " + matchedRule.getName());
}
Best Practices
-
Organize Rules by Domain: Create separate rule sets for different domains or aspects of your application.
-
Use Clear Priorities: Assign clear priorities to rules, with lower numbers for more important rules.
-
Leverage Effective Dates: Use effective date ranges to automatically activate and deactivate rules based on time.
-
Provide Clear Metadata: Include clear descriptions for your rule set, business checks, business actions, and rules.
-
Validate Rule Sets: Validate rule sets at load time to ensure they are well-formed and reference valid business checks and actions.
-
Keep Rule Sets Focused: Each rule set should have a clear, focused purpose. Avoid creating "catch-all" rule sets.
-
Consider Versioning: If you need to maintain multiple versions of rules, consider using separate rule set files or effective dates.
Related Sections
- Rule Set Minimal Structure - Details on the minimal structure required for rule sets
- Rule Set Validations - Information about validating rule sets
- Rule Context Overview - How rule contexts are used with rule sets
- Rule Orchestrators Overview - How rule orchestrators use rule sets to evaluate rules
Rule Set Structure
Rule sets are the core configuration unit in Axiom, defining the collection of business rules that operate together. This document explains the structure of rule set configuration files and provides guidance on creating well-structured rule sets.
YAML Structure Overview
Axiom rule sets are defined in YAML files with a specific structure. Here's a comprehensive overview of a rule set YAML file:
rulesetName: "Example Rule Set"
rulesetDescription: "A comprehensive example of a rule set structure"
businessChecks:
- name: checkOne
description: "Description of the first check"
params:
- paramOne
- paramTwo
- name: checkTwo
description: "Description of the second check"
params:
- threshold
businessActions:
- name: actionOne
description: "Description of the first action"
params:
- amount
- name: actionTwo
description: "Description of the second action"
params:
- reason
- severity
rules:
- name: "Rule One"
description: "Description of the first rule"
expression: checkOne(paramOne, paramTwo) then actionOne(100)
priority: 10
effectiveFrom: "2023-01-01T00:00:00Z"
effectiveTo: "2025-01-01T00:00:00Z"
- name: "Rule Two"
description: "Description of the second rule"
expression: checkTwo(500) then actionTwo("reason", "HIGH")
priority: 20
effectiveFrom: "2023-02-01T00:00:00Z"
Required and Optional Fields
Rule Set Level Fields
Field | Required | Description |
---|---|---|
rulesetName |
Required | The name of the rule set. Should be descriptive and unique within your application. |
rulesetDescription |
Optional | A detailed description of the rule set's purpose and function. |
Business Check Definition Fields
Field | Required | Description |
---|---|---|
name |
Required | The identifier used to reference this check in rule expressions. Must match the name used in the Java implementation. |
description |
Required | A description of what the check evaluates. |
params |
Optional | A list of parameter names that the check accepts. These must match the parameter names used in the Java implementation. |
Business Action Definition Fields
Field | Required | Description |
---|---|---|
name |
Required | The identifier used to reference this action in rule expressions. Must match the name used in the Java implementation. |
description |
Required | A description of what the action does. |
params |
Optional | A list of parameter names that the action accepts. These must match the parameter names used in the Java implementation. |
Rule Definition Fields
Field | Required | Description |
---|---|---|
name |
Required | A descriptive name for the rule. Should be unique within the rule set. |
description |
Required | A detailed description of the rule's purpose and conditions. |
expression |
Required | The rule expression that defines the check and action parts. More details below. |
priority |
Required | A numeric value determining the rule's execution priority. Lower numbers indicate higher priority. |
effectiveFrom |
Optional | The ISO-8601 date-time from which the rule becomes active. If omitted, the rule is active immediately. |
effectiveTo |
Optional | The ISO-8601 date-time after which the rule becomes inactive. If omitted, the rule never expires. |
tags |
Optional | A list of string tags for categorizing and organizing rules. |
Rule Expressions
Rule expressions follow a specific syntax:
check_condition(parameters) then action(parameters)
The expression consists of two parts: 1. Check condition: Evaluates to true or false, determining if the rule matches 2. Action: The action to perform when the check condition is true
Multiple check conditions can be combined using logical operators:
checkOne(param1) AND checkTwo(param2) then actionOne(100)
checkOne(param1) OR checkTwo(param2) then actionOne(100)
NOT checkOne(param1) then actionTwo("reason")
Minimal Valid Rule Set Example
Here's an example of a minimal valid rule set:
rulesetName: "Minimal Rule Set"
businessChecks:
- name: isTrue
description: "Always returns true"
businessActions:
- name: doNothing
description: "Does nothing"
rules:
- name: "Simple Rule"
description: "A simple rule that always triggers"
expression: isTrue() then doNothing()
priority: 10
Best Practices for Rule Set Structure
-
Use Descriptive Names: Give your rule set, checks, actions, and rules clear, descriptive names that indicate their purpose.
-
Organize by Business Domain: Group rules that relate to the same business domain in the same rule set.
-
Keep Rule Sets Focused: Each rule set should have a single responsibility or domain focus.
-
Document Thoroughly: Use the description fields to thoroughly document the purpose and behavior of each component.
-
Use Consistent Priority Schemes: Establish a consistent approach to rule priorities. For example:
- Use priority bands (e.g., 1-10 for critical rules, 11-20 for important rules, etc.)
-
Leave gaps between priorities to allow for future insertions (e.g., 10, 20, 30, etc.)
-
Leverage Effective Dates: Use effective dates to manage rule lifecycle, particularly for time-limited promotions or policy changes.
-
Use Tags for Organization: Apply consistent tags to help categorize and filter rules, especially in large rule sets.
-
Version Control: Keep rule set files in version control along with your application code.
-
Validate Before Deployment: Use the built-in validation features to validate rule sets before deploying them to production.
Common Gotchas and Troubleshooting
-
Parameter Names: Ensure parameter names in the YAML file match those expected by your Java implementation. Mismatches will cause validation errors.
-
Date Formats: Effective dates must be in ISO-8601 format (
YYYY-MM-DDThh:mm:ssZ
). Incorrect formats will cause validation errors. -
Case Sensitivity: All names (checks, actions, parameters) are case-sensitive and must match exactly between YAML and Java implementations.
-
Expression Syntax: Rule expressions must follow the exact syntax, including spaces between operators. The validation will catch syntax errors, but they can be tricky to spot manually.
Code Generation
This page describes how to use the code generation maven plugin to generate Java stub classes for business checks and actions from the YAML rule-set files.
Axiom Codegen Maven Plugin
The axiom-codegen
Maven plugin provides a simple way to generate Java stub classes for your business checks and actions defined in Axiom rule set YAML files. This plugin will automatically:
- Generate Java classes for all business checks and actions in your rule set YAML files
- Place them in appropriate packages based on your configuration
- Add the generated sources to your project's compilation path
Adding the Plugin to Your Project
To use the axiom-codegen plugin in your project, add the following configuration to your pom.xml
file:
<plugin>
<groupId>com.lyxtera</groupId>
<artifactId>axiom-codegen</artifactId>
<version>${axiom.version}</version>
<executions>
<execution>
<id>generate-stubs</id>
<goals>
<goal>generate-stubs</goal>
</goals>
<configuration>
<packageName>com.example.rules</packageName>
<contextKeyEnum>com.example.rules.MyContextKey</contextKeyEnum>
<ruleSets>${project.basedir}/src/main/resources/my_ruleset.yaml</ruleSets>
<outputDirectory>src/main/java/</outputDirectory>
<overwriteExisting>true</overwriteExisting>
<skip>false</skip>
</configuration>
</execution>
</executions>
</plugin>
Plugin Configuration Parameters
The plugin accepts the following configuration parameters:
Parameter | Property | Description | Default | Required |
---|---|---|---|---|
packageName |
axiom.stubs.package |
Base package for generated classes | - | Yes |
contextKeyEnum |
axiom.stubs.contextKeyEnum |
Fully qualified name of the context enum class | - | Yes |
ruleSets |
axiom.stubs.ruleSets |
Comma-separated list of rule set YAML files to process | - | Yes |
outputDirectory |
axiom.stubs.outputDirectory |
Directory to output generated sources to | ${project.build.directory}/generated-sources/axiom |
No |
overwriteExisting |
axiom.stubs.overwriteExisting |
Whether to overwrite existing files | false |
No |
skip |
axiom.stubs.skip |
Skip the rule stub generation | false |
No |
Example Configuration
Here's a complete example from the axiom-examples
project:
<plugin>
<groupId>com.lyxtera</groupId>
<artifactId>axiom-codegen</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<id>generate-axiom-stubs</id>
<phase>generate-sources</phase>
<goals>
<goal>generate-stubs</goal>
</goals>
<configuration>
<packageName>com.lyxtera.axiom.examples.rules</packageName>
<contextKeyEnum>com.lyxtera.axiom.examples.rules.CustomerContextKey</contextKeyEnum>
<ruleSets>${project.basedir}/src/main/resources/customer_discount_ruleset.yaml</ruleSets>
<outputDirectory>src/main/java/</outputDirectory>
<overwriteExisting>true</overwriteExisting>
<skip>false</skip>
</configuration>
</execution>
</executions>
</plugin>
This configuration will process the customer_discount_ruleset.yaml
file and generate Java stub classes in the com.lyxtera.axiom.examples.rules
package.
Using Properties in Configuration
You can also use Maven properties to configure the plugin, making it easier to manage configurations across different environments:
<properties>
<axiom.stubs.skip>false</axiom.stubs.skip>
<axiom.stubs.package>com.example.rules</axiom.stubs.package>
<axiom.stubs.contextKeyEnum>com.example.rules.MyContextKey</axiom.stubs.contextKeyEnum>
</properties>
<plugin>
<groupId>com.lyxtera</groupId>
<artifactId>axiom-codegen</artifactId>
<version>${axiom.version}</version>
<executions>
<execution>
<id>generate-axiom-stubs</id>
<phase>generate-sources</phase>
<goals>
<goal>generate-stubs</goal>
</goals>
</execution>
</executions>
</plugin>
Generated Code Structure
The axiom-codegen
plugin creates two types of stub classes:
- Business Check Classes - Placed in the
checks
package under your base package - Business Action Classes - Placed in the
actions
package under your base package
For example, given the following YAML ruleset:
rulesetName: "Customer Discount Ruleset"
rulesetDescription: "Rules for applying discounts to customers"
businessChecks:
- name: isHighValueCustomer
description: Determines if the customer has high spending for the past N days
params:
- spendingThreshold
- days
- name: hasLoyaltyStatus
description: Checks if the customer has loyalty status
params:
- loyaltyLevel
businessActions:
- name: applyDiscount
description: Applies a discount to the customer's order
params:
- percentage
The generator will create the following Java classes:
com.example.rules.checks.IsHighValueCustomerCheck.java
com.example.rules.checks.HasLoyaltyStatusCheck.java
com.example.rules.actions.ApplyDiscountAction.java
Each generated class includes:
- Proper package declaration
- Necessary imports
- Rule metadata annotations
- Empty implementation stubs for the business logic
- Javadoc comments based on the descriptions in the YAML file
Rule Set Validations
Axiom provides a robust validation framework to ensure rule sets are correctly configured before they're loaded into your application. This document covers the validation process, error types, and how to handle validation failures.
Validation Process
Validation happens automatically when a rule set is loaded. The validation process includes:
- Syntax validation: Checks the YAML structure and syntax
- Reference validation: Verifies references to business checks and actions
- Expression validation: Validates rule expressions syntax and parameters
- Semantic validation: Checks for logical consistency in the rule set
Types of Validations
Structural Validations
These validations ensure the rule set YAML has the correct structure:
- Required fields (rulesetName, business check names, rule names, etc.)
- Unique identifiers within their scope (rule names, check names, etc.)
- Proper data types (priority as a number, dates in ISO format, etc.)
Reference Validations
These validations ensure all references are valid:
- Business checks referenced in rules exist in the registered checks
- Business actions referenced in rules exist in the registered actions
- Parameters match those defined in the business checks and actions
Expression Validations
These validations focus on rule expressions:
- Syntax correctness (proper use of operators, parentheses, etc.)
- Correct parameter count for checks and actions
- Parameter type compatibility
Semantic Validations
These validations check for logical issues:
- Non-circular dependencies
- Effective date ranges are valid (from date is before to date)
- Priority values are valid
Validation Error Types
Axiom produces specific error types for different validation failures:
Error Type | Description | Example |
---|---|---|
MissingRequiredFieldError |
A required field is missing | Missing rule name or priority |
InvalidReferenceError |
Reference to a non-existent component | Check reference not found in registered checks |
ParameterMismatchError |
Parameter count or names don't match | Too many or too few parameters for a check |
ExpressionSyntaxError |
Syntax error in a rule expression | Missing 'then' keyword or unbalanced parentheses |
DateFormatError |
Invalid date format in effective dates | Date not in ISO-8601 format |
DuplicateIdentifierError |
Duplicate name within scope | Two rules with the same name |
Error Handling
When validation errors occur, Axiom throws a RuleSetValidationException
that contains detailed information about all validation failures. Here's how to handle it:
try {
RuleSet<MyContextKey> ruleSet = ruleSetLoader.load();
// Use the rule set...
} catch (RuleSetValidationException e) {
// Get all validation errors
List<ValidationError> errors = e.getValidationErrors();
// Log detailed information about each error
errors.forEach(error -> {
logger.error("Validation error: {} - {}",
error.getErrorType(),
error.getMessage());
// If the error has a location in the YAML, log that too
if (error.hasLocation()) {
logger.error(" Location: line {}, column {}",
error.getLocation().getLine(),
error.getLocation().getColumn());
}
});
// Take appropriate action (e.g., fail application startup)
throw new ApplicationStartupException("Rule set validation failed", e);
}
Validation Examples
Example 1: Missing Check Reference
If a rule references a check that doesn't exist:
rules:
- name: "Invalid Rule"
description: "References a non-existent check"
expression: nonExistentCheck() then validAction()
priority: 10
Validation error:
InvalidReferenceError: Check 'nonExistentCheck' referenced in rule 'Invalid Rule' not found in registered checks
Example 2: Parameter Count Mismatch
If a check is called with the wrong number of parameters:
businessChecks:
- name: checkCustomerAge
description: "Checks if customer is above age threshold"
params:
- ageThreshold
rules:
- name: "Invalid Parameters"
description: "Calls check with wrong parameter count"
expression: checkCustomerAge(18, "extra") then validAction()
priority: 10
Validation error:
ParameterMismatchError: Check 'checkCustomerAge' in rule 'Invalid Parameters' expects 1 parameters but was called with 2
Example 3: Invalid Expression Syntax
If the rule expression has syntax errors:
rules:
- name: "Syntax Error"
description: "Has syntax error in expression"
expression: validCheck() validAction() # Missing 'then' keyword
priority: 10
Validation error:
ExpressionSyntaxError: Missing 'then' keyword in rule 'Syntax Error'
Custom Validators
You can extend Axiom's validation framework with custom validators:
public class CustomRuleSetValidator<T extends Enum<T>> implements RuleSetValidator<T> {
@Override
public List<ValidationError> validate(RuleSet<T> ruleSet,
Map<String, BusinessCheck<T>> checks,
Map<String, BusinessAction<T>> actions) {
List<ValidationError> errors = new ArrayList<>();
// Custom validation logic
if (ruleSet.getRules().size() > 100) {
errors.add(new ValidationError(
"TooManyRulesError",
"Rule set contains more than 100 rules, which may impact performance",
null // No specific location
));
}
return errors;
}
}
Register your custom validator:
RuleSetLoader<MyContextKey> loader = new YamlRuleSetLoader<>("ruleset.yaml")
.addValidator(new CustomRuleSetValidator<>());
Best Practices
- Validate Early: Validate rule sets during application startup to fail fast
- Detailed Logging: Log detailed validation errors to help identify issues
- Testing: Create tests that verify your rule sets pass validation
- CI/CD Integration: Add validation to your CI/CD pipeline to catch issues before deployment
- Custom Validators: Create custom validators for domain-specific requirements
Rules
Rules Overview
Rules are the core building blocks of the Axiom framework. They define the conditions under which business actions should be performed, forming the foundation of your business logic.
Anatomy of a Rule
A business rule in Axiom consists of:
- Name: A unique identifier for the rule
- Description: A human-readable explanation of the rule's purpose
- Expression: A condition and action pair that defines when and what to execute
- Priority: A numeric value determining evaluation order (lower values = higher priority)
- Effective Dates: Optional time boundaries for when the rule is active
In code, a rule is represented by the BusinessRule<K>
class:
public class BusinessRule<K extends Enum<K>> {
private final String name;
private final Condition<K> condition;
private final List<RuleFunction<K>> actions;
// Constructor and methods...
public boolean evaluate(RuleContext<K> context) {
if (condition == null || TRUE.equals(condition.evaluate(context))) {
for (RuleFunction<K> action : actions) {
action.execute(context);
}
return true;
}
return false;
}
}
Rule Expression Syntax
Rule expressions in YAML follow this pattern:
<condition> then <action>
Where:
- <condition>
is a boolean expression that determines if the rule matches
- then
is the keyword separating condition from action
- <action>
is the business action to execute when the condition is true
Condition Syntax
Conditions can be:
- Simple checks:
hasRiskScore(90)
- Negated checks:
not hasFraudSignals()
- Compound expressions:
- AND:
isHighValueOrder(1000) and isNewCustomer()
- OR:
hasFraudSignals() or hasRiskScore(95)
- Complex expressions with parentheses:
isEnterpriseCustomer() and (hasHighRiskScore(90) or hasRecentFraudActivity())
Examples of Rule Expressions
# Simple rule
isHighValueOrder(1000) then applyDiscount(10)
# Negated condition
not hasDiscountAlready() then applyDiscount(5)
# Compound condition with AND
isRepeatCustomer() and isPremiumMember() then applyDiscount(15)
# Compound condition with OR
hasFraudSignals() or hasRiskScore(95) then blockTransaction()
# Complex condition with parentheses
isHighValueOrder(5000) and (not hasApproval() or isNewCustomer()) then requireManualReview()
Rule Definition in YAML
Here's how rules are defined in a YAML rule set file:
rules:
- name: "High Value Order Discount"
description: "Apply 10% discount to orders over $1000"
expression: isHighValueOrder(1000) then applyDiscount(10)
priority: 10
effectiveFrom: "2023-01-01T00:00:00Z"
- name: "Repeat Customer Discount"
description: "Apply 5% discount to repeat customers"
expression: isRepeatCustomer() then applyDiscount(5)
priority: 20
effectiveFrom: "2023-01-01T00:00:00Z"
effectiveTo: "2023-12-31T23:59:59Z" # This rule expires at the end of 2023
- name: "Premium Membership Discount"
description: "Apply 15% discount to premium members"
expression: isPremiumMember() then applyDiscount(15)
priority: 15
effectiveFrom: "2023-03-01T00:00:00Z"
Rule Evaluation Process
When a context is evaluated against a rule set, the following process occurs:
- Rules are filtered by their effective dates, keeping only currently active rules
- Remaining rules are sorted by priority (lower number = higher priority)
- For each rule (in priority order):
a. The condition is evaluated against the context
b. If the condition is true, the rule's actions are executed
c. For
executeFirstMatchingRule()
, processing stops after the first match d. ForexecuteAllMatchingRules()
, all matching rules are processed
Here's a sequence diagram illustrating the rule evaluation process:
βββββββββββ ββββββββββββββββββ ββββββββββ βββββββββββββββββ
β Service β β RuleOrchestratorβ β RuleSetβ β BusinessRule β
ββββββ¬βββββ βββββββββ¬ββββββββββ βββββ¬βββββ βββββββββ¬ββββββββ
β β β β
β executeFirstMatchingRule(ctx)β β β
βββββββββββββββββββββββ>β β β
β β β β
β β getRulesInPriorityOrderβ β
β ββββββββββββββββββββββββ>β β
β β β β
β β [filtered active rules]β β
β β<ββββββββββββββββββββββββ β
β β β β
β β βββββββ΄βββββ β
β β β Sort by β β
β β β priority β β
β β βββββββ¬βββββ β
β β β β
β β βββββββ΄βββββ β
β β βFor each β β
β β β rule β β
β β βββββββ¬βββββ β
β β β β
β β β evaluate(context) β
β β βββββββββββββββββββββββββ>
β β β β
β β β [true/false] β
β β β<βββββββββββββββββββββββ
β β β β
β β βββββββ΄βββββ β
β β βIf matchedβ β
β β βbreak loopβ β
β β βββββββ¬βββββ β
β β β β
β RuleExecutionResult β β β
β<βββββββββββββββββββββββ β β
β β β β
Advanced Rule Patterns
Parameter-Based Rules
Rules can accept parameters to make them more flexible:
# A rule with a configurable threshold
- name: "Dynamic Risk Threshold Rule"
description: "Block transactions above a configurable risk threshold"
expression: hasRiskScore($RISK_THRESHOLD) then blockTransaction()
priority: 5
effectiveFrom: "2023-01-01T00:00:00Z"
The parameter $RISK_THRESHOLD
can be provided at runtime.
Multi-Action Rules
Rules can perform multiple actions:
# A rule that performs multiple actions
- name: "Suspicious Transaction Rule"
description: "Flag and log suspicious transactions for review"
expression: isSuspiciousTransaction() then flagForReview() and logTransaction()
priority: 30
effectiveFrom: "2023-01-01T00:00:00Z"
Rules with Complex Conditions
You can create complex conditions using AND, OR, NOT, and parentheses:
# A rule with a complex condition
- name: "Complex Approval Rule"
description: "Require approval for high-value orders from new customers in high-risk regions"
expression: >
isHighValueOrder(5000) and
(isNewCustomer() or
(isFromHighRiskRegion() and not hasApprovedKYC()))
then requireManualApproval()
priority: 5
effectiveFrom: "2023-01-01T00:00:00Z"
Working with Rule Results
When rules are executed, they produce a RuleExecutionResult
that contains information about which rules matched and the final state of the context:
// Execute all matching rules
RuleExecutionResult<MyContextKey> result = orchestrator.executeAllMatchingRules(context);
// Check if any rules matched
if (result.hasMatches()) {
// Get the matched rules
List<BusinessRule<MyContextKey>> matchedRules = result.getMatchedRules();
// Get the names of matched rules
List<String> matchedRuleNames = matchedRules.stream()
.map(BusinessRule::getName)
.collect(Collectors.toList());
// Log the matched rule names
System.out.println("Matched rules: " + String.join(", ", matchedRuleNames));
// Get the context after rule execution (which may have been modified by rule actions)
RuleContext<MyContextKey> resultContext = result.getContext();
// Check if a specific action was performed
if (resultContext.get(MyContextKey.REQUEST_BLOCKED, Boolean.class).orElse(false)) {
System.out.println("Request was blocked by a rule");
}
}
Rule Performance Considerations
-
Rule Priority: Carefully assign priorities to ensure the most frequently matching rules have higher priority (lower numbers) to reduce the average number of rule evaluations.
-
Condition Complexity: Complex conditions with many subconditions can impact performance. Consider breaking complex rules into multiple simpler rules when possible.
-
Rule Count: The number of rules in a rule set directly impacts evaluation time. Keep rule sets focused and consider using multiple specialized rule sets instead of one large generic rule set.
-
Context Size: Large contexts with many values can impact serialization/deserialization performance. Include only necessary data in the context.
Related Sections
- Rule Priority - Learn more about rule priority and evaluation order
- Rule Effective Dates - Understanding rule activation and expiration
- Business Actions & Checks Overview - Details on the components used in rule expressions
- Rule Testing - How to test rules effectively
Rule Priority
Rule priority is a critical aspect of the Axiom framework that determines the order in which rules are evaluated. This page explains how rule priorities work, how they affect rule execution, and best practices for setting priorities effectively.
How Priority Works in Axiom
In Axiom, rule priority is expressed as a numeric value, where lower values indicate higher priority. For example, a rule with priority 10 is considered more important than a rule with priority 100 and will be evaluated first.
When a rule set is evaluated against a context, the rules are sorted by priority. This means:
- Rules with the lowest priority numbers are evaluated first
- When two rules have the same priority, their relative order is undefined
Here's how priorities are defined in a rule set YAML file:
rules:
- name: "Critical Security Rule"
description: "Block requests with critical security violations"
expression: hasCriticalSecurityViolation() then blockRequest()
priority: 1 # Highest priority
effectiveFrom: "2023-01-01T00:00:00Z"
- name: "High Risk Score Rule"
description: "Block requests with very high risk scores"
expression: hasRiskScore(90) then blockRequest()
priority: 10 # Medium-high priority
effectiveFrom: "2023-01-01T00:00:00Z"
- name: "New Customer Additional Verification"
description: "Add verification for new customers"
expression: isNewCustomer() then requireAdditionalVerification()
priority: 100 # Lower priority
effectiveFrom: "2023-01-01T00:00:00Z"
Priority in the Rule Execution Flow
When rules are executed using a RuleOrchestrator
, the priority order affects the execution flow:
// Get the rule set from the loader
RuleSetLoader<MyContextKey> loader = new YamlRuleSetLoader<>("risk_rules.yaml");
RuleSet<MyContextKey> ruleSet = loader.loadRuleSet();
// Create an orchestrator for the rule set
RuleOrchestrator<MyContextKey> orchestrator = new RuleOrchestrator<>(ruleSet);
// Create a context for rule evaluation
RuleContext<MyContextKey> context = new RuleContext<>(MyContextKey.class);
context.add(MyContextKey.RISK_SCORE, 95);
context.add(MyContextKey.IS_NEW_CUSTOMER, true);
// Execute the first matching rule
RuleExecutionResult<MyContextKey> result = orchestrator.executeFirstMatchingRule(context);
In this example:
1. The orchestrator retrieves all rules from the rule set
2. Rules are filtered by their effective dates (only currently active rules are considered)
3. Remaining rules are sorted by priority (lowest to highest number)
4. Rules are evaluated in priority order
5. With executeFirstMatchingRule()
, once a rule matches (its condition evaluates to true), its actions are executed and no further rules are evaluated
6. With executeAllMatchingRules()
, all matching rules are executed in priority order
Code Example: Priority Order Handling
The following code demonstrates how rule priorities are handled in the RuleSet
class:
/**
* Gets all rules in priority order (lowest numbers first)
*
* @return Unmodifiable list of rules in priority order
*/
public List<BusinessRule<K>> getRulesInPriorityOrder() {
return rules.stream()
.filter(PrioritizedRule::isEffectiveNow)
.map(PrioritizedRule::getRule)
.collect(Collectors.toUnmodifiableList());
}
/**
* Adds a rule to the set with the specified priority and effective date range
*
* @param rule The rule to add
* @param priority Lower values indicate higher priority (priority 1 is higher than priority 2)
* @param effectiveFrom The date and time from which the rule is effective
* @param effectiveTo The date and time until which the rule is effective (null for indefinite)
* @throws RuleException if priority is less than 1
*/
void addRule(BusinessRule<K> rule, int priority, ZonedDateTime effectiveFrom, ZonedDateTime effectiveTo) {
if (priority < 1) {
throw RuleException.invalidPriority();
}
rules.add(new PrioritizedRule<>(rule, priority, effectiveFrom, effectiveTo));
// Sort in ascending order (lowest priority number first = highest priority)
Collections.sort(rules);
}
The PrioritizedRule
inner class implements Comparable
to enable sorting:
private static class PrioritizedRule<K extends Enum<K>> implements Comparable<PrioritizedRule<K>> {
private final BusinessRule<K> rule;
private final int priority;
private final ZonedDateTime effectiveFrom;
private final ZonedDateTime effectiveTo;
// Constructor...
@Override
public int compareTo(PrioritizedRule<K> other) {
// Lower priority number means higher priority, so use natural ordering
return Integer.compare(this.priority, other.priority);
}
}
When Rule Priority Matters Most
Rule priority is particularly important in several scenarios:
1. Exception Handling Rules
When you have general rules and specific exception cases, you want the exceptions to have higher priority:
# Exception rule (higher priority)
- name: "VIP Customer Exception"
description: "Skip fraud checks for VIP customers"
expression: isVipCustomer() then skipFraudChecks()
priority: 5
effectiveFrom: "2023-01-01T00:00:00Z"
# General rule (lower priority)
- name: "Standard Fraud Check"
description: "Apply standard fraud checks to all transactions"
expression: isTransaction() then applyStandardFraudChecks()
priority: 50
effectiveFrom: "2023-01-01T00:00:00Z"
2. Rules with Overlapping Conditions
When multiple rules could match the same input, priority determines which one takes precedence:
# More specific rule (higher priority)
- name: "High-Value International Transfer Rule"
description: "Apply enhanced scrutiny to high-value international transfers"
expression: isInternationalTransfer() and isHighValue(10000) then applyEnhancedScrutiny()
priority: 10
effectiveFrom: "2023-01-01T00:00:00Z"
# More general rule (lower priority)
- name: "International Transfer Rule"
description: "Apply basic scrutiny to all international transfers"
expression: isInternationalTransfer() then applyBasicScrutiny()
priority: 20
effectiveFrom: "2023-01-01T00:00:00Z"
3. Security vs. Business Rules
Security and compliance rules typically take precedence over business/operational rules:
# Security rule (highest priority)
- name: "Security Block Rule"
description: "Block requests with security threats"
expression: hasSecurityThreat() then blockRequest()
priority: 1
effectiveFrom: "2023-01-01T00:00:00Z"
# Compliance rule (high priority)
- name: "Compliance Approval Rule"
description: "Require compliance approval for high-risk transactions"
expression: isHighRiskTransaction() then requireComplianceApproval()
priority: 10
effectiveFrom: "2023-01-01T00:00:00Z"
# Business rule (lower priority)
- name: "Discount Rule"
description: "Apply discount for eligible transactions"
expression: isEligibleForDiscount() then applyDiscount(10)
priority: 100
effectiveFrom: "2023-01-01T00:00:00Z"
Advanced Priority Strategies
1. Priority Ranges by Rule Type
A common approach is to assign priority ranges based on rule categories:
Priority Range | Rule Category | Examples |
---|---|---|
1-9 | Security & Fraud | Blocking known attacks, critical security |
10-99 | Compliance & Risk | KYC checks, regulatory requirements |
100-499 | Business Logic | Approvals, workflows, business decisions |
500-999 | Customer Experience | Discounts, promotions, personalization |
1000+ | Operational/Analytics | Data collection, metrics, optimization |
This approach makes it clear which types of rules take precedence and allows for easy insertion of new rules within a category.
2. Dynamic Priority Assignment
In some advanced scenarios, you may want to assign rule priorities dynamically:
// Create a rule set with dynamically assigned priorities
RuleSet<MyContextKey> ruleSet = new RuleSet<>();
// Security rules have highest priority (1-9)
ruleSet.addRule(createSecurityRule("Block Known Attackers"),
1, ZonedDateTime.now());
// Compliance rules have next highest priority (10-99)
ruleSet.addRule(createComplianceRule("High Risk Country Check"),
10, ZonedDateTime.now());
// Business rules have lower priority (100+)
ruleSet.addRule(createBusinessRule("New Customer Experience"),
100, ZonedDateTime.now());
// The priority could come from configuration or database
Integer discountRulePriority = configService.getRulePriority("SeasonalDiscount");
ruleSet.addRule(createDiscountRule("Seasonal Discount"),
discountRulePriority, ZonedDateTime.now());
3. Staggered Priorities for Future Rules
Leave gaps in your priority numbering to accommodate future rules:
rules:
- name: "Critical Security Rule"
priority: 1 # Highest priority
- name: "Important Security Rule"
priority: 10 # Gap allows for rules 2-9 to be added later
- name: "Standard Security Rule"
priority: 20 # Gap allows for rules 11-19 to be added later
This approach allows you to insert new rules between existing ones without having to reassign priorities.
Best Practices
-
Lower Numbers for Higher Priority: Always remember that lower numbers indicate higher priority in Axiom.
-
Establish a Priority System: Define a clear system for assigning priorities (like the ranges shown above) and document it.
-
Leave Gaps Between Priorities: Don't use consecutive numbers (1, 2, 3) for priorities. Instead, use increments (10, 20, 30) to allow for future rules to be inserted.
-
Prioritize Security & Compliance: Always give security and compliance rules higher priority than business or operational rules.
-
Be Cautious with Priority 1: Reserve the absolute highest priorities (1, 2, etc.) for truly critical rules that must trump all others.
-
Document Priority Decisions: When assigning priorities, document the reasoning to help future maintainers understand why certain priorities were chosen.
-
Review Priority Order Regularly: As rule sets grow, review the priority order regularly to ensure it still makes logical sense.
Related Sections
- Rule Overview - General information about rules in Axiom
- Rule Effective Dates - How effective dates interact with priorities
- Rule Orchestrator Operations - How orchestrators use rule priorities
Rule Effective Dates
Effective dates are a powerful feature in Axiom that allow you to control when rules are active without changing code or deploying new configurations. By using effective dates, you can set rules to automatically activate or deactivate at specific points in time.
How Effective Dates Work
Each rule in Axiom can have:
- Effect From Date: The timestamp when the rule becomes active
- Effect To Date: (Optional) The timestamp when the rule becomes inactive
Rules are only considered during evaluation if the current time falls within this range. If no "effective to" date is specified, the rule remains active indefinitely once it reaches its "effective from" date.
Specifying Effective Dates in YAML
In a rule set YAML file, effective dates are specified using ISO-8601 format:
rules:
- name: "Permanent Rule"
description: "This rule is permanent once activated"
expression: isPermanentCondition() then performPermanentAction()
priority: 10
effectiveFrom: "2023-01-01T00:00:00Z" # Active from January 1, 2023
- name: "Temporary Promotion Rule"
description: "This rule is only active for a specific period"
expression: isEligibleForPromotion() then applyPromotion()
priority: 20
effectiveFrom: "2023-06-01T00:00:00Z" # Active from June 1, 2023
effectiveTo: "2023-06-30T23:59:59Z" # Until June 30, 2023
- name: "Future Rule"
description: "This rule will become active in the future"
expression: isFutureCondition() then performFutureAction()
priority: 30
effectiveFrom: "2024-01-01T00:00:00Z" # Won't be active until January 1, 2024
Implementation in the RuleSet Class
The effective date filtering happens in the RuleSet
class. Here's the implementation of the isEffectiveNow()
method from the PrioritizedRule
inner class:
/**
* Checks if the rule is effective at the current time.
*
* @return true if the rule is effective, false otherwise
*/
boolean isEffectiveNow() {
ZonedDateTime now = ZonedDateTime.now();
boolean afterStart = effectiveFrom == null || effectiveFrom.isBefore(now) || effectiveFrom.isEqual(now);
boolean beforeEnd = effectiveTo == null || effectiveTo.isAfter(now) || effectiveTo.isEqual(now);
return afterStart && beforeEnd;
}
This method is called when retrieving rules from a rule set:
/**
* Gets all rules in priority order (lowest numbers first)
*
* @return Unmodifiable list of rules in priority order
*/
public List<BusinessRule<K>> getRulesInPriorityOrder() {
return rules.stream()
.filter(PrioritizedRule::isEffectiveNow) // Filter based on effective dates
.map(PrioritizedRule::getRule)
.collect(Collectors.toUnmodifiableList());
}
Programmatically Setting Effective Dates
You can also set effective dates programmatically when adding rules to a rule set:
// Create a rule set
RuleSet<MyContextKey> ruleSet = new RuleSet<>();
// Create business rules
BusinessRule<MyContextKey> permanentRule = createPermanentRule();
BusinessRule<MyContextKey> temporaryRule = createTemporaryRule();
BusinessRule<MyContextKey> futureRule = createFutureRule();
// Add a permanent rule (effective from a specific date, with no end date)
ruleSet.addRule(
permanentRule,
10,
ZonedDateTime.parse("2023-01-01T00:00:00Z")
);
// Add a temporary rule (effective for a specific date range)
ruleSet.addRule(
temporaryRule,
20,
ZonedDateTime.parse("2023-06-01T00:00:00Z"),
ZonedDateTime.parse("2023-06-30T23:59:59Z")
);
// Add a rule that will become active in the future
ruleSet.addRule(
futureRule,
30,
ZonedDateTime.parse("2024-01-01T00:00:00Z")
);
Advanced Usage Patterns
1. Seasonal Rules
Use effective dates to implement seasonal business rules:
# Summer promotion
- name: "Summer Discount Rule"
description: "Apply summer discount to all orders"
expression: isOrder() then applyDiscount(15, "SUMMER2023")
priority: 100
effectiveFrom: "2023-06-21T00:00:00Z" # Summer solstice
effectiveTo: "2023-09-22T23:59:59Z" # Autumn equinox
# Holiday promotion
- name: "Holiday Season Discount Rule"
description: "Apply holiday discount to all orders"
expression: isOrder() then applyDiscount(20, "HOLIDAY2023")
priority: 100
effectiveFrom: "2023-11-24T00:00:00Z" # Black Friday
effectiveTo: "2023-12-31T23:59:59Z" # New Year's Eve
2. Rule Versions and Transitions
Effective dates can be used to manage transitions between rule versions:
# Version 1 of the rule (being phased out)
- name: "Risk Evaluation Rule v1"
description: "Original risk evaluation algorithm"
expression: evaluateRiskV1() then assignRiskScore()
priority: 50
effectiveFrom: "2022-01-01T00:00:00Z"
effectiveTo: "2023-02-28T23:59:59Z" # Expires end of February 2023
# Version 2 of the rule (replacing v1)
- name: "Risk Evaluation Rule v2"
description: "Improved risk evaluation algorithm"
expression: evaluateRiskV2() then assignRiskScore()
priority: 50
effectiveFrom: "2023-02-01T00:00:00Z" # Overlap period in February for testing
This creates a one-month overlap period where both rules are active, allowing for A/B testing or gradual transition.
3. Time-based Rule Activation
For complex time-based activation, you can combine effective dates with time checks in your rule conditions:
# Regular business hours rule
- name: "Business Hours Service Level"
description: "Apply standard SLA during business hours"
expression: isBusinessHours() then applyStandardSLA()
priority: 100
effectiveFrom: "2023-01-01T00:00:00Z"
# After-hours rule
- name: "After Hours Service Level"
description: "Apply extended SLA outside business hours"
expression: not isBusinessHours() then applyExtendedSLA()
priority: 100
effectiveFrom: "2023-01-01T00:00:00Z"
Where isBusinessHours()
is a business check that examines the current time:
@RuleMetadata(name = "isBusinessHours", description = "Checks if the current time is within business hours")
public class BusinessHoursCheck implements BusinessCheck<MyContextKey> {
@Override
public Value execute(RuleContext<MyContextKey> context) {
ZonedDateTime now = ZonedDateTime.now();
int hour = now.getHour();
DayOfWeek day = now.getDayOfWeek();
boolean isWeekday = day != DayOfWeek.SATURDAY && day != DayOfWeek.SUNDAY;
boolean isDuringBusinessHours = hour >= 9 && hour < 17; // 9 AM to 5 PM
return Value.of(isWeekday && isDuringBusinessHours);
}
}
4. Regulatory Compliance Rules
Use effective dates to implement regulatory compliance rules that must activate on specific dates:
# GDPR Compliance Rule (effective from GDPR enforcement date)
- name: "GDPR Data Processing Rule"
description: "Apply GDPR requirements to European user data processing"
expression: isEuropeanUserData() then applyGDPRRequirements()
priority: 5 # High priority for compliance rules
effectiveFrom: "2018-05-25T00:00:00Z" # GDPR enforcement date
# California Consumer Privacy Act (CCPA) Rule
- name: "CCPA Data Processing Rule"
description: "Apply CCPA requirements to California user data processing"
expression: isCaliforniaUserData() then applyCCPARequirements()
priority: 5 # High priority for compliance rules
effectiveFrom: "2020-01-01T00:00:00Z" # CCPA enforcement date
Testing Rules with Effective Dates
When testing rules with effective dates, you may need to simulate different points in time. Here's an example of how to test a rule with future effective dates:
@Test
void testRuleWithFutureEffectiveDate() {
// Create a rule set
RuleSet<MyContextKey> ruleSet = new RuleSet<>();
// Create a rule with a future effective date
BusinessRule<MyContextKey> futureRule = createTestRule();
ZonedDateTime futureDate = ZonedDateTime.now().plusDays(30); // 30 days in the future
ruleSet.addRule(futureRule, 10, futureDate);
// Create an orchestrator
RuleOrchestrator<MyContextKey> orchestrator = new RuleOrchestrator<>(ruleSet);
// Create a context
RuleContext<MyContextKey> context = new RuleContext<>(MyContextKey.class);
context.add(MyContextKey.TEST_VALUE, "test");
// Execute rules - should not match because the rule is not effective yet
RuleExecutionResult<MyContextKey> result = orchestrator.executeFirstMatchingRule(context);
assertThat(result.hasMatch()).isFalse();
// Now, we'll test with a FixedClock to simulate that it's 31 days in the future
// Note: This requires modifying the PrioritizedRule class to accept a Clock parameter
// which is beyond the scope of this explanation
}
Best Practices
-
Use UTC Times: Always use UTC (Zulu) time zone for effective dates to avoid daylight saving time issues.
-
Specify End Times Precisely: When setting an
effectiveTo
date, use the end of the day (23:59:59) to ensure the rule is active for the entire last day. -
Plan for Transitions: When replacing an existing rule with a new version, consider an overlap period to ensure smooth transitions.
-
Use Time-Based Activations Carefully: For time-of-day specific rules, consider using business checks that examine the current time rather than using multiple rules with different effective dates.
-
Test with Different Time Points: When testing rules with effective dates, test with times before, during, and after the effective period.
-
Document Effective Dates: Include clear documentation about why particular effective dates were chosen, especially for regulatory or business-critical rules.
-
Audit Effective Date Changes: Keep an audit trail of changes to effective dates, especially for compliance-related rules.
Related Sections
- Rule Overview - General information about rules in Axiom
- Rule Priority - How rule priority interacts with effective dates
- Rule Testing - How to test rules with effective dates
Rule Context Overview
The RuleContext
is a central component in the Axiom framework, serving as a thread-safe container for data that is processed by rules. It provides a type-safe way to store and retrieve data, ensuring that rules can access the information they need during evaluation.
Key Features
- Type Safety: Uses Java generics to ensure type safety when storing and retrieving values
- Thread Safety: Designed to be thread-safe, allowing concurrent access from multiple threads
- Enum-Based Keys: Uses enum values as keys, providing compile-time validation for key names
- Optional Values: Uses Java's Optional to handle the absence of values gracefully
- JSON Serialization: Supports serialization to and from JSON for persistence or transmission
Core Concepts
Context Keys
The RuleContext
uses enum values as keys to store and retrieve data. This approach provides several benefits:
- Compile-time validation of key names
- Clear documentation of available keys through the enum definition
- Type safety when retrieving values
Example enum definition:
public enum OrderContextKey {
CUSTOMER_ID,
ORDER_AMOUNT,
PRODUCT_IDS,
IS_REPEAT_CUSTOMER,
HAS_DISCOUNT_APPLIED,
SHIPPING_COUNTRY
}
Value Storage and Retrieval
The RuleContext
provides methods to store, retrieve, and remove values:
// Create a new context
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
// Add values
context.add(OrderContextKey.CUSTOMER_ID, "CUST-12345");
context.add(OrderContextKey.ORDER_AMOUNT, 199.99);
context.add(OrderContextKey.PRODUCT_IDS, Arrays.asList("PROD-001", "PROD-002"));
context.add(OrderContextKey.IS_REPEAT_CUSTOMER, true);
// Retrieve optional values (returns Optional objects)
Optional<String> customerId = context.get(OrderContextKey.CUSTOMER_ID, String.class);
Optional<Double> orderAmount = context.get(OrderContextKey.ORDER_AMOUNT, Double.class);
Optional<List<String>> productIds = context.get(OrderContextKey.PRODUCT_IDS, List.class);
// Retrieve required values (throws exception if not present)
String requiredCustomerId = context.getRequired(OrderContextKey.CUSTOMER_ID, String.class);
Double requiredOrderAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
// Remove values
context.remove(OrderContextKey.IS_REPEAT_CUSTOMER, Boolean.class);
// Check if context is empty
boolean isEmpty = context.isEmpty();
JSON Serialization and Deserialization
The RuleContext
supports serialization to and from JSON, which is useful for:
- Persisting context data to a database
- Transmitting context data over a network
- Debugging rule execution by inspecting the context state
// Serialize to JSON
String json = context.toJson();
// Deserialize from JSON
RuleContext<OrderContextKey> deserializedContext =
RuleContext.fromJson(OrderContextKey.class, json);
Using RuleContext in Business Checks and Actions
Business checks and actions use the RuleContext
to access the data they need for evaluation or execution.
In Business Checks
Business checks retrieve values from the context to evaluate conditions:
@RuleMetadata(name = "isHighValueOrder", description = "Checks if the order value exceeds a threshold")
public class HighValueOrderCheck implements BusinessCheck<OrderContextKey> {
@Override
public Value execute(RuleContext<OrderContextKey> context, @Arg("threshold") Value threshold) {
Double orderAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
Double thresholdValue = threshold.asNumber().doubleValue();
return Value.of(orderAmount > thresholdValue);
}
}
In Business Actions
Business actions can both retrieve and modify the context:
@RuleMetadata(name = "applyDiscount", description = "Applies a percentage discount to the order")
public class ApplyDiscountAction implements BusinessAction<OrderContextKey> {
@Override
public Value execute(RuleContext<OrderContextKey> context, @Arg("percent") Value percent) {
Double orderAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
Double discountPercent = percent.asNumber().doubleValue();
Double discountedAmount = orderAmount * (1 - (discountPercent / 100));
context.add(OrderContextKey.ORDER_AMOUNT, discountedAmount);
context.add(OrderContextKey.HAS_DISCOUNT_APPLIED, true);
return Value.of(true);
}
}
Best Practices
-
Define Clear Context Keys: Use descriptive names for your enum values and include comments to document their purpose and expected types.
-
Keep Context Focused: Include only the data relevant to your rules to avoid cluttering the context.
-
Handle Missing Values Gracefully: Use the
get
method when a value might not exist, andgetRequired
only when you're certain a value should be present. -
Type Safety: Always specify the expected type when retrieving values to ensure type safety.
-
Thread Safety: Remember that
RuleContext
is thread-safe, but any objects you store in it might not be. Ensure that objects stored in the context are either immutable or properly synchronized if they'll be accessed concurrently.
Related Sections
- Rule Context Definition & Operations - Details on specific operations available in RuleContext
- Business Actions & Checks Overview - How business actions and checks interact with RuleContext
- Rule Orchestrators Overview - How rule orchestrators use RuleContext during rule execution
Rule Context Operations
The RuleContext
class provides a rich set of operations for storing, retrieving, and manipulating data during rule evaluation. This guide explores the advanced operations available in RuleContext
and provides practical examples of their usage.
Core Operations
Adding Values
The primary way to add values to a context is using the add
method:
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
// Add simple values
context.add(OrderContextKey.CUSTOMER_ID, "CUST-12345");
context.add(OrderContextKey.ORDER_AMOUNT, 199.99);
context.add(OrderContextKey.IS_PRIME_MEMBER, true);
// Add complex objects
List<String> productIds = Arrays.asList("PROD-001", "PROD-002", "PROD-003");
context.add(OrderContextKey.PRODUCT_IDS, productIds);
Map<String, Integer> productQuantities = new HashMap<>();
productQuantities.put("PROD-001", 2);
productQuantities.put("PROD-002", 1);
productQuantities.put("PROD-003", 3);
context.add(OrderContextKey.PRODUCT_QUANTITIES, productQuantities);
// Add dates and times
context.add(OrderContextKey.ORDER_DATE, ZonedDateTime.now());
Retrieving Values
There are two primary methods for retrieving values:
get
: Returns anOptional
containing the value if presentgetRequired
: Returns the value directly, throwing an exception if not present
// Using get (returns Optional)
Optional<String> customerId = context.get(OrderContextKey.CUSTOMER_ID, String.class);
if (customerId.isPresent()) {
System.out.println("Customer ID: " + customerId.get());
}
// Using getRequired (throws exception if not present)
try {
Double orderAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
System.out.println("Order amount: $" + orderAmount);
} catch (ContextException e) {
System.err.println("Required value not found: " + e.getMessage());
}
// Getting complex objects
Optional<List<String>> optionalProductIds = context.get(OrderContextKey.PRODUCT_IDS, List.class);
if (optionalProductIds.isPresent()) {
List<String> products = optionalProductIds.get();
System.out.println("Number of products: " + products.size());
}
// Type casting for complex types
Optional<Map<String, Integer>> optionalQuantities =
context.get(OrderContextKey.PRODUCT_QUANTITIES, Map.class);
if (optionalQuantities.isPresent()) {
Map<String, Integer> quantities = optionalQuantities.get();
// Note: You may need to handle type casting for generic types carefully
}
Removing Values
Values can be removed from the context using the remove
method:
// Remove a value
context.remove(OrderContextKey.TEMPORARY_DATA, Object.class);
// Check if removal was successful
boolean stillExists = context.get(OrderContextKey.TEMPORARY_DATA, Object.class).isPresent();
Checking for Empty Context
You can check if a context has any values using the isEmpty
method:
if (context.isEmpty()) {
System.out.println("Context is empty");
} else {
System.out.println("Context contains values");
}
Advanced Operations
JSON Serialization and Deserialization
The RuleContext
supports conversion to and from JSON:
// Convert context to JSON
String json = context.toJson();
System.out.println("Context as JSON: " + json);
// Create a new context from JSON
RuleContext<OrderContextKey> deserializedContext =
RuleContext.fromJson(OrderContextKey.class, json);
// Verify values were preserved
Optional<String> deserializedCustomerId =
deserializedContext.get(OrderContextKey.CUSTOMER_ID, String.class);
This is particularly useful for: - Persisting contexts to databases - Transmitting contexts between services - Debugging rule execution by logging contexts - Creating snapshots of context state for testing
Type-safe Access with CtxGet
For more type-safe access to context values, you can use the CtxGet
utility:
// Create a type-safe accessor for a specific context key and type
CtxGet<OrderContextKey, String> customerId =
CtxGet.of(OrderContextKey.CUSTOMER_ID, String.class);
// Use the accessor to get values from a context
Optional<String> id = customerId.from(context);
// Use with getRequired
String requiredId = customerId.getRequired(context);
This approach provides better type safety when accessing context values in multiple places.
Context Merging
You can merge multiple contexts together:
// Create two contexts
RuleContext<OrderContextKey> orderContext = new RuleContext<>(OrderContextKey.class);
orderContext.add(OrderContextKey.ORDER_AMOUNT, 199.99);
RuleContext<OrderContextKey> customerContext = new RuleContext<>(OrderContextKey.class);
customerContext.add(OrderContextKey.CUSTOMER_ID, "CUST-12345");
// Merge contexts (creates a new context with values from both)
RuleContext<OrderContextKey> mergedContext = RuleContext.merge(orderContext, customerContext);
// Values from both contexts are accessible
Optional<String> customerId = mergedContext.get(OrderContextKey.CUSTOMER_ID, String.class);
Optional<Double> orderAmount = mergedContext.get(OrderContextKey.ORDER_AMOUNT, Double.class);
If both contexts contain the same key, the value from the later context in the merge parameters will be used.
Bulk Operations
For performing operations on multiple values:
// Add multiple values at once
Map<OrderContextKey, Object> values = new HashMap<>();
values.put(OrderContextKey.CUSTOMER_ID, "CUST-12345");
values.put(OrderContextKey.ORDER_AMOUNT, 199.99);
values.put(OrderContextKey.IS_PRIME_MEMBER, true);
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
context.addAll(values);
// Get all keys
Set<OrderContextKey> keys = context.getKeys();
System.out.println("Context contains keys: " + keys);
// Clear all values
context.clear();
Common Patterns
Context Builder Pattern
For creating contexts with many values, a builder pattern can be helpful:
public class OrderContextBuilder {
private RuleContext<OrderContextKey> context;
public OrderContextBuilder() {
context = new RuleContext<>(OrderContextKey.class);
}
public OrderContextBuilder withCustomerId(String customerId) {
context.add(OrderContextKey.CUSTOMER_ID, customerId);
return this;
}
public OrderContextBuilder withOrderAmount(Double amount) {
context.add(OrderContextKey.ORDER_AMOUNT, amount);
return this;
}
public OrderContextBuilder withProducts(List<String> productIds) {
context.add(OrderContextKey.PRODUCT_IDS, productIds);
return this;
}
public OrderContextBuilder withPrimeMembership(boolean isPrimeMember) {
context.add(OrderContextKey.IS_PRIME_MEMBER, isPrimeMember);
return this;
}
public RuleContext<OrderContextKey> build() {
return context;
}
}
// Usage
RuleContext<OrderContextKey> context = new OrderContextBuilder()
.withCustomerId("CUST-12345")
.withOrderAmount(199.99)
.withProducts(Arrays.asList("PROD-001", "PROD-002"))
.withPrimeMembership(true)
.build();
Context Factory Pattern
For creating pre-configured contexts for different scenarios:
public class OrderContextFactory {
public static RuleContext<OrderContextKey> createPrimeCustomerContext(String customerId) {
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
context.add(OrderContextKey.CUSTOMER_ID, customerId);
context.add(OrderContextKey.IS_PRIME_MEMBER, true);
context.add(OrderContextKey.CUSTOMER_TIER, "PRIME");
return context;
}
public static RuleContext<OrderContextKey> createNewCustomerContext(String customerId) {
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
context.add(OrderContextKey.CUSTOMER_ID, customerId);
context.add(OrderContextKey.IS_NEW_CUSTOMER, true);
context.add(OrderContextKey.CUSTOMER_CREATION_DATE, ZonedDateTime.now());
return context;
}
public static RuleContext<OrderContextKey> createHighValueOrderContext(
String customerId, double amount) {
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
context.add(OrderContextKey.CUSTOMER_ID, customerId);
context.add(OrderContextKey.ORDER_AMOUNT, amount);
context.add(OrderContextKey.IS_HIGH_VALUE, true);
return context;
}
}
// Usage
RuleContext<OrderContextKey> primeContext =
OrderContextFactory.createPrimeCustomerContext("CUST-12345");
RuleContext<OrderContextKey> highValueContext =
OrderContextFactory.createHighValueOrderContext("CUST-67890", 999.99);
Context Decoration Pattern
For incrementally enhancing a context with additional information:
public class OrderContextDecorator {
public static RuleContext<OrderContextKey> addPriceInformation(
RuleContext<OrderContextKey> context) {
// Get product IDs
List<String> productIds = context.getRequired(OrderContextKey.PRODUCT_IDS, List.class);
// Simulate looking up prices from a product service
Map<String, Double> productPrices = new HashMap<>();
for (String productId : productIds) {
double price = lookupProductPrice(productId);
productPrices.put(productId, price);
}
// Calculate total
double totalPrice = productPrices.values().stream().mapToDouble(Double::valueOf).sum();
// Add to context
context.add(OrderContextKey.PRODUCT_PRICES, productPrices);
context.add(OrderContextKey.ORDER_AMOUNT, totalPrice);
return context;
}
public static RuleContext<OrderContextKey> addCustomerInformation(
RuleContext<OrderContextKey> context) {
// Get customer ID
String customerId = context.getRequired(OrderContextKey.CUSTOMER_ID, String.class);
// Simulate looking up customer information
CustomerInfo info = lookupCustomerInfo(customerId);
// Add to context
context.add(OrderContextKey.CUSTOMER_TIER, info.getTier());
context.add(OrderContextKey.IS_PRIME_MEMBER, info.isPrimeMember());
context.add(OrderContextKey.CUSTOMER_REGION, info.getRegion());
return context;
}
// Simulation methods
private static double lookupProductPrice(String productId) {
// In a real implementation, this would call a product service
return 19.99;
}
private static CustomerInfo lookupCustomerInfo(String customerId) {
// In a real implementation, this would call a customer service
return new CustomerInfo("SILVER", true, "US-EAST");
}
private static class CustomerInfo {
private final String tier;
private final boolean primeMember;
private final String region;
public CustomerInfo(String tier, boolean primeMember, String region) {
this.tier = tier;
this.primeMember = primeMember;
this.region = region;
}
public String getTier() { return tier; }
public boolean isPrimeMember() { return primeMember; }
public String getRegion() { return region; }
}
}
// Usage
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
context.add(OrderContextKey.CUSTOMER_ID, "CUST-12345");
context.add(OrderContextKey.PRODUCT_IDS, Arrays.asList("PROD-001", "PROD-002"));
// Decorate with additional information
context = OrderContextDecorator.addPriceInformation(context);
context = OrderContextDecorator.addCustomerInformation(context);
Best Practices
1. Use Strong Typing
Always specify the expected type when retrieving values:
// Good - with explicit type
Optional<String> customerId = context.get(OrderContextKey.CUSTOMER_ID, String.class);
// Bad - returns Optional<Object>
Optional<?> genericValue = context.get(OrderContextKey.CUSTOMER_ID);
2. Handle Missing Values Gracefully
Use get
when a value might not be present and handle the Optional properly:
// Good - safe handling
Optional<Double> discountPercentage = context.get(OrderContextKey.DISCOUNT_PERCENTAGE, Double.class);
double finalDiscount = discountPercentage.orElse(0.0);
// Good - with more complex fallback
double finalDiscount = context.get(OrderContextKey.DISCOUNT_PERCENTAGE, Double.class)
.orElseGet(() -> calculateDefaultDiscount(context));
// Bad - potential NullPointerException
Double discount = context.get(OrderContextKey.DISCOUNT_PERCENTAGE, Double.class).get();
3. Use getRequired Judiciously
Use getRequired
only when you're certain a value should be present:
// Good - when the value must be present
String customerId = context.getRequired(OrderContextKey.CUSTOMER_ID, String.class);
// Better - with meaningful exception
try {
String customerId = context.getRequired(OrderContextKey.CUSTOMER_ID, String.class);
} catch (ContextException e) {
throw new BusinessException("Customer ID is required for this operation", e);
}
4. Keep Contexts Focused
Include only the data relevant to your rules to avoid cluttering the context:
// Good - relevant data only
context.add(OrderContextKey.ORDER_AMOUNT, 199.99);
context.add(OrderContextKey.CUSTOMER_TIER, "GOLD");
// Bad - irrelevant data
context.add(OrderContextKey.WEBPAGE_COLOR_THEME, "DARK");
context.add(OrderContextKey.LAST_DB_QUERY_TIME_MS, 23);
5. Document Context Keys
Document your enum keys to make it clear what each key represents:
/**
* Keys for the order processing rule context.
*/
public enum OrderContextKey {
/**
* The unique identifier of the customer. Type: String
*/
CUSTOMER_ID,
/**
* The total amount of the order in USD. Type: Double
*/
ORDER_AMOUNT,
/**
* Whether the customer is a prime member. Type: Boolean
*/
IS_PRIME_MEMBER,
/**
* List of product IDs in the order. Type: List<String>
*/
PRODUCT_IDS
}
6. Clean Up Temporary Values
Remove temporary or intermediate values that are no longer needed:
// Add temporary calculation values
context.add(OrderContextKey.TEMP_SUBTOTAL, 190.0);
context.add(OrderContextKey.TEMP_TAX_RATE, 0.05);
// Perform calculations
double subtotal = context.getRequired(OrderContextKey.TEMP_SUBTOTAL, Double.class);
double taxRate = context.getRequired(OrderContextKey.TEMP_TAX_RATE, Double.class);
double total = subtotal * (1 + taxRate);
// Add the final result
context.add(OrderContextKey.ORDER_TOTAL, total);
// Clean up temporary values
context.remove(OrderContextKey.TEMP_SUBTOTAL, Double.class);
context.remove(OrderContextKey.TEMP_TAX_RATE, Double.class);
7. Use Immutable Objects When Possible
Store immutable objects in the context when possible to prevent accidental modifications:
// Good - immutable list
List<String> productIds = List.of("PROD-001", "PROD-002");
context.add(OrderContextKey.PRODUCT_IDS, productIds);
// Good - defensive copy of mutable object
List<String> mutableList = new ArrayList<>();
mutableList.add("PROD-001");
mutableList.add("PROD-002");
context.add(OrderContextKey.PRODUCT_IDS, Collections.unmodifiableList(new ArrayList<>(mutableList)));
// Bad - mutable object that could be changed elsewhere
context.add(OrderContextKey.PRODUCT_IDS, mutableList);
Related Sections
- Rule Context Overview - General information about rule contexts
- Business Actions & Checks Overview - How context is used in actions and checks
- Rule Testing - How to test code that uses rule contexts
Business Actions & Checks
Business Actions & Checks Overview
Business Actions and Business Checks are the core components that define the behavior of rules in the Axiom framework. They provide the building blocks for creating expressive and powerful business rules.
What are Business Checks and Actions?
- Business Checks are functions that evaluate conditions against a rule context. They return boolean values indicating whether a condition is met.
- Business Actions are functions that perform operations when a rule's condition is met. They can modify the rule context or perform external operations.
Both Business Checks and Business Actions are implemented as Java classes that implement the respective interfaces and are registered with the Axiom framework.
Business Checks
Business Checks implement the BusinessCheck<K>
interface, which defines a single method:
public interface BusinessCheck<K extends Enum<K>> extends RuleFunction<K> {
// Inherited from RuleFunction
Value execute(RuleContext<K> context, Value... args);
}
Implementing a Business Check
Here's an example of a Business Check implementation:
@RuleMetadata(
name = "hasRiskScore",
description = "Checks if the risk score is above a specified threshold"
)
public class HasRiskScoreCheck implements BusinessCheck<MyContextKey> {
@Override
public Value execute(RuleContext<MyContextKey> context, @Arg("threshold") Value threshold) {
// Get the risk score from the context
Integer riskScore = context.getRequired(MyContextKey.RISK_SCORE, Integer.class);
// Get the threshold value from the argument
Integer thresholdValue = threshold.asNumber().intValue();
// Return true if the risk score is greater than or equal to the threshold
return Value.of(riskScore >= thresholdValue);
}
}
Key points about this implementation:
- The
@RuleMetadata
annotation provides metadata about the check, including its name and description. - The
execute
method takes aRuleContext
and optional arguments, and returns aValue
object. - The
@Arg
annotation is used to name the arguments, which helps with validation and documentation. - The implementation retrieves values from the context, performs a comparison, and returns a boolean result.
Using Business Checks in Rules
Business Checks are used in rule expressions to define conditions:
rules:
- name: "High Risk Score Rule"
description: "Block requests with very high risk scores"
expression: hasRiskScore(90) then blockRequest()
priority: 80
effectiveFrom: "2023-05-15T00:00:00Z"
In this example, hasRiskScore(90)
is a call to the HasRiskScoreCheck
with an argument of 90
.
Business Actions
Business Actions implement the BusinessAction<K>
interface, which also defines a single method:
public interface BusinessAction<K extends Enum<K>> extends RuleFunction<K> {
// Inherited from RuleFunction
Value execute(RuleContext<K> context, Value... args);
}
Implementing a Business Action
Here's an example of a Business Action implementation:
@RuleMetadata(
name = "blockRequest",
description = "Blocks the request entirely"
)
public class BlockRequestAction implements BusinessAction<MyContextKey> {
@Override
public Value execute(RuleContext<MyContextKey> context) {
// Mark the request as blocked in the context
context.add(MyContextKey.REQUEST_BLOCKED, true);
// Log the block action
String requestId = context.getRequired(MyContextKey.REQUEST_ID, String.class);
System.out.println("Blocking request: " + requestId);
// Return true to indicate the action was performed
return Value.of(true);
}
}
Key points about this implementation:
- The
@RuleMetadata
annotation provides metadata about the action, including its name and description. - The
execute
method takes aRuleContext
and optional arguments, and returns aValue
object. - The implementation modifies the context by adding a value indicating the request is blocked.
- The implementation may perform additional operations, such as logging.
- The action returns a boolean value indicating whether it was successful.
Using Business Actions in Rules
Business Actions are used in rule expressions to define what happens when a condition is met:
rules:
- name: "Fraud Detection Rule"
description: "Block requests with fraud signals"
expression: hasFraudSignals() then blockRequest()
priority: 100
effectiveFrom: "2023-01-01T00:00:00Z"
In this example, blockRequest()
is a call to the BlockRequestAction
.
Registering Business Checks and Actions
Business Checks and Actions must be registered with the Axiom framework to be used in rules. This is typically done in an AxiomModule
implementation:
public class MyAxiomModule extends AxiomModule<MyContextKey> {
@Override
protected void configureBusinessRules(
MapBinder<String, BusinessCheck<MyContextKey>> checks,
MapBinder<String, BusinessAction<MyContextKey>> actions) {
// Register business checks
checks.addBinding("hasFraudSignals").to(HasFraudSignalsCheck.class);
checks.addBinding("hasRiskScore").to(HasRiskScoreCheck.class);
// Register business actions
actions.addBinding("blockRequest").to(BlockRequestAction.class);
actions.addBinding("flagForReview").to(FlagForReviewAction.class);
}
// Other methods...
}
The names used in the addBinding
calls must match the names used in rule expressions.
Working with Arguments
Both Business Checks and Business Actions can accept arguments in rule expressions. These arguments are passed as Value
objects to the execute
method.
Defining Arguments
Arguments can be defined using the @Arg
annotation:
@Override
public Value execute(RuleContext<MyContextKey> context,
@Arg("threshold") Value threshold,
@Arg("tolerance") Value tolerance) {
// Use the arguments...
}
Accessing Argument Values
The Value
class provides methods to convert the argument to various types:
// Convert to primitive types
Integer intValue = value.asNumber().intValue();
Double doubleValue = value.asNumber().doubleValue();
Boolean boolValue = value.asBoolean();
String stringValue = value.asString();
// Check the type
boolean isInteger = value.isInteger();
boolean isDouble = value.isDouble();
boolean isBoolean = value.isBoolean();
boolean isString = value.isString();
Validating Arguments
It's important to validate arguments to ensure they are of the expected type:
if (!threshold.isInteger()) {
throw new IllegalArgumentException("Threshold must be an integer");
}
Best Practices
-
Keep Checks and Actions Focused: Each check or action should have a single, well-defined responsibility.
-
Use Clear Names: Choose descriptive names for your checks and actions that clearly indicate their purpose.
-
Provide Detailed Descriptions: Use the
description
field in the@RuleMetadata
annotation to provide clear documentation. -
Validate Inputs: Always validate context values and arguments to ensure they are of the expected type.
-
Handle Errors Gracefully: Catch and handle exceptions appropriately to prevent rule execution from failing unexpectedly.
-
Document Expected Context Keys: Clearly document which context keys your checks and actions expect to be present.
-
Return Meaningful Values: Ensure that your checks and actions return values that accurately reflect their execution status.
Related Sections
- Business Components Definition - How to define business components in rule sets
- Business Components Usage - How to use business components in rules
- Business Components Implementation - Detailed implementation guidelines
- Business Components Validation - How to validate business components
Business Components Definition
Business components are the building blocks of Axiom rules. This document explains how to define business checks and actions, including their metadata, parameters, and implementation patterns.
Business Checks vs. Business Actions
Before diving into definitions, it's important to understand the distinction between checks and actions:
- Business Checks: Evaluate conditions and return boolean values (
true
/false
). They represent the "if" part of a rule. - Business Actions: Perform operations and can return any value. They represent the "then" part of a rule.
Defining Business Checks
A business check is implemented as a Java class that implements the BusinessCheck
interface:
@RuleMetadata(
name = "isHighValueCustomer",
description = "Checks if a customer is considered high value based on criteria"
)
public class HighValueCustomerCheck implements BusinessCheck<CustomerContextKey> {
@Override
public Value execute(RuleContext<CustomerContextKey> context, @Arg("spendThreshold") Value spendThreshold) {
// Get required data from context
Double totalSpend = context.getRequired(CustomerContextKey.LIFETIME_SPEND, Double.class);
Boolean isPremium = context.getOptional(CustomerContextKey.IS_PREMIUM_MEMBER, Boolean.class)
.orElse(false);
// Apply business logic
Double threshold = spendThreshold.asNumber().doubleValue();
boolean isHighValue = totalSpend > threshold || isPremium;
// Return result as a Value
return Value.of(isHighValue);
}
}
Key Components:
- @RuleMetadata Annotation: Provides metadata about the check:
name
: The identifier used in rule expressions (must match the name in YAML)-
description
: A description of what the check does -
Interface Implementation: The class must implement
BusinessCheck<T>
whereT
is your context key enum. -
execute Method: The core method that implements the check logic:
context
: Contains all data needed for the check@Arg
parameters: Values provided when the check is called in a rule expression- Return value: A
Value
object, typically containing a boolean
Defining Business Actions
A business action is implemented as a Java class that implements the BusinessAction
interface:
@RuleMetadata(
name = "applyDiscount",
description = "Applies a percentage discount to the order"
)
public class ApplyDiscountAction implements BusinessAction<OrderContextKey> {
@Override
public Value execute(RuleContext<OrderContextKey> context,
@Arg("percent") Value percent,
@Arg("reason") Value reason) {
// Get required data from context
Double orderAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
// Apply business logic
Double discountPercent = percent.asNumber().doubleValue();
String discountReason = reason.asString();
Double discountedAmount = orderAmount * (1 - (discountPercent / 100));
// Update context with new values
context.add(OrderContextKey.ORDER_AMOUNT, discountedAmount);
context.add(OrderContextKey.DISCOUNT_APPLIED, true);
context.add(OrderContextKey.DISCOUNT_REASON, discountReason);
// Return success indicator
return Value.of(true);
}
}
Key Components:
- @RuleMetadata Annotation: Similar to checks, provides metadata:
name
: The identifier used in rule expressions-
description
: A description of what the action does -
Interface Implementation: The class must implement
BusinessAction<T>
. -
execute Method: Implements the action logic:
- Can have multiple
@Arg
parameters - Typically modifies the context
- Returns a
Value
that can be of any type (often a success indicator)
Working with Parameters
Parameters allow rules to customize the behavior of checks and actions. Here's how to work with them:
Parameter Annotations
Use the @Arg
annotation to define parameters:
public Value execute(RuleContext<T> context,
@Arg("name") Value nameParam,
@Arg("threshold") Value thresholdParam) {
// ...
}
Parameter Types
All parameters are passed as Value
objects, which can be converted to specific types:
String name = nameParam.asString();
Double threshold = thresholdParam.asNumber().doubleValue();
Boolean flag = thresholdParam.asBoolean();
List<String> items = thresholdParam.asList(String.class);
Map<String, Object> data = thresholdParam.asMap();
Optional Parameters
Sometimes you might want to make parameters optional with defaults:
public Value execute(RuleContext<T> context, @Arg("threshold") Value thresholdParam) {
// Default to 100 if not provided or not a valid number
Double threshold = 100.0;
try {
threshold = thresholdParam.asNumber().doubleValue();
} catch (ValueConversionException e) {
// Log warning and use default
logger.warn("Invalid threshold value, using default");
}
// ...
}
Parameter Validation
It's important to validate parameters to ensure they meet your requirements:
public Value execute(RuleContext<T> context, @Arg("age") Value ageParam) {
// Validate age is a positive number
if (!ageParam.isNumber()) {
throw new IllegalArgumentException("Age must be a number");
}
Double age = ageParam.asNumber().doubleValue();
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
// ...
}
Context Interaction
Business components interact with the rule context to get data and store results:
Reading from Context
// Required value (throws exception if missing or wrong type)
Customer customer = context.getRequired(CustomerContextKey.CUSTOMER, Customer.class);
// Optional value (returns Optional)
Optional<String> promoCode = context.getOptional(CustomerContextKey.PROMO_CODE, String.class);
// With default value
String region = context.getOptional(CustomerContextKey.REGION, String.class)
.orElse("DEFAULT");
Writing to Context
// Adding new values
context.add(OrderContextKey.DISCOUNT_APPLIED, true);
context.add(OrderContextKey.DISCOUNT_AMOUNT, 25.50);
// Updating existing values
context.add(OrderContextKey.ORDER_AMOUNT, newAmount); // Overwrites existing value
Documentation Best Practices
Good documentation is crucial for business components:
-
Descriptive Names: Use clear, business-oriented names for your components.
-
Thorough Descriptions: The
description
field should explain what the component does in business terms. -
Javadoc Documentation: Add detailed Javadoc to your classes explaining:
- Purpose and behavior
- Parameter details and valid values
- Return value meaning
- Context keys used and modified
- Examples of usage
Example:
/**
* Checks if a customer qualifies for VIP status based on spend and loyalty.
* <p>
* A customer qualifies for VIP status if:
* <ul>
* <li>Their annual spend exceeds the specified threshold, OR</li>
* <li>They have been a member for at least 5 years and have made a purchase in the last 6 months</li>
* </ul>
* <p>
* Required context keys:
* <ul>
* <li>{@link CustomerContextKey#ANNUAL_SPEND} - Double, the customer's annual spend</li>
* <li>{@link CustomerContextKey#MEMBERSHIP_YEARS} - Integer, years as a member</li>
* <li>{@link CustomerContextKey#LAST_PURCHASE_DATE} - LocalDate, date of last purchase</li>
* </ul>
* <p>
* Parameters:
* <ul>
* <li>spendThreshold (Double) - The annual spend threshold for VIP status</li>
* </ul>
*
* @see VipBenefitsAction
*/
@RuleMetadata(
name = "isVipCustomer",
description = "Checks if a customer qualifies for VIP status"
)
public class VipCustomerCheck implements BusinessCheck<CustomerContextKey> {
// Implementation...
}
Best Practices for Component Definition
-
Single Responsibility: Each component should do one thing well.
-
Immutability: Make your components immutable for thread safety.
-
Error Handling: Validate inputs and handle errors gracefully.
-
Testability: Design components to be easily testable in isolation.
-
Performance: Be mindful of performance, especially for frequently used checks.
-
Reusability: Design components to be reusable across different rule sets.
-
Consistency: Follow a consistent naming convention for components.
-
Dependency Injection: Use DI for external dependencies rather than static references.
Advanced Component Patterns
Composite Checks
Sometimes you might want to create a check that combines multiple other checks:
@RuleMetadata(
name = "isEligibleForPromotion",
description = "Checks if a customer is eligible for a promotion"
)
public class PromotionEligibilityCheck implements BusinessCheck<CustomerContextKey> {
private final BusinessCheck<CustomerContextKey> ageCheck;
private final BusinessCheck<CustomerContextKey> regionCheck;
@Inject
public PromotionEligibilityCheck(
@Named("isAdult") BusinessCheck<CustomerContextKey> ageCheck,
@Named("isInTargetRegion") BusinessCheck<CustomerContextKey> regionCheck) {
this.ageCheck = ageCheck;
this.regionCheck = regionCheck;
}
@Override
public Value execute(RuleContext<CustomerContextKey> context, @Arg("promoId") Value promoId) {
// First check age
Value ageResult = ageCheck.execute(context, Value.of(18));
if (!ageResult.asBoolean()) {
return Value.of(false);
}
// Then check region
Value regionResult = regionCheck.execute(context, Value.of("NA,EU"));
if (!regionResult.asBoolean()) {
return Value.of(false);
}
// If both pass, check promotion-specific logic
String promotionId = promoId.asString();
// ... additional promotion-specific logic
return Value.of(true);
}
}
Parameterized Actions
For more complex actions that need configuration:
@RuleMetadata(
name = "sendNotification",
description = "Sends a notification through the configured channel"
)
public class NotificationAction implements BusinessAction<UserContextKey> {
private final NotificationService notificationService;
private final TemplateEngine templateEngine;
@Inject
public NotificationAction(
NotificationService notificationService,
TemplateEngine templateEngine) {
this.notificationService = notificationService;
this.templateEngine = templateEngine;
}
@Override
public Value execute(RuleContext<UserContextKey> context,
@Arg("channel") Value channel,
@Arg("templateId") Value templateId,
@Arg("priority") Value priority) {
// Get user from context
User user = context.getRequired(UserContextKey.USER, User.class);
// Process parameters
String channelType = channel.asString();
String template = templateId.asString();
String priorityLevel = priority.asString();
// Prepare notification
Map<String, Object> templateData = new HashMap<>();
templateData.put("userName", user.getName());
templateData.put("userId", user.getId());
// ... add more template data
String content = templateEngine.render(template, templateData);
// Send notification
NotificationResult result = notificationService.send(
user.getContactInfo(),
channelType,
content,
NotificationPriority.valueOf(priorityLevel)
);
// Update context with notification result
context.add(UserContextKey.NOTIFICATION_SENT, true);
context.add(UserContextKey.NOTIFICATION_ID, result.getNotificationId());
return Value.of(result.isSuccess());
}
}
Business Components Usage
This document covers how to use business checks and actions effectively in your rule expressions. You'll learn how to define rule expressions, pass parameters, and follow best practices for component usage.
Rule Expression Basics
Rule expressions in Axiom follow a simple pattern:
check_condition then action
Where:
- check_condition
is an expression that evaluates to true or false
- then
is a keyword that separates the condition from the action
- action
is the action to perform when the condition is true
For example:
isHighValueCustomer(5000) then applyPremiumDiscount(15)
This rule checks if a customer is a high-value customer with a spending threshold of $5,000, and if true, applies a 15% premium discount.
Working with Business Checks in Expressions
Basic Check Usage
The simplest form of a check in a rule expression is:
checkName(param1, param2, ...)
For example:
isAdult(18)
Logical Operators
You can combine multiple checks using logical operators:
checkA(param) AND checkB(param) // Both must be true
checkA(param) OR checkB(param) // Either can be true
NOT checkA(param) // Inverts the result
Examples:
isAdult(18) AND hasValidId()
isPreferredCustomer() OR hasActivePromotion()
NOT isRestrictedRegion("US,CA")
Complex Expressions
You can create more complex expressions using parentheses:
(checkA() OR checkB()) AND NOT checkC()
For example:
(isPremiumMember() OR hasLoyaltyStatus("GOLD")) AND NOT hasOutstandingInvoices()
Working with Business Actions in Expressions
Actions are simpler than checks because they're always placed after the then
keyword and can't be combined:
then actionName(param1, param2, ...)
For example:
then applyDiscount(10)
then addLoyaltyPoints(100, "PROMOTION")
Parameter Types
Axiom supports different parameter types in rule expressions:
String Parameters
String parameters must be enclosed in quotes:
isInRegion("North America")
Numeric Parameters
Numbers don't need quotes:
isAboveThreshold(1000)
isWithinRange(0.5, 1.5)
Boolean Parameters
Boolean values don't need quotes:
setFlag(true)
enableFeature(false)
List Parameters
You can pass comma-separated values that will be converted to lists:
isInCountries("US,CA,MX")
In your implementation, you would parse this:
String countries = countryParam.asString();
List<String> countryList = Arrays.asList(countries.split(","));
Date Parameters
Dates should be passed as ISO-8601 formatted strings:
isAfterDate("2023-01-01T00:00:00Z")
In your implementation, you would parse this:
String dateStr = dateParam.asString();
Instant date = Instant.parse(dateStr);
Real-World Usage Examples
E-commerce Discount Rules
rules:
- name: "Premium Member Discount"
description: "Apply 15% discount for premium members"
expression: isPremiumMember() then applyDiscount(15, "PREMIUM_MEMBER")
priority: 10
- name: "First-Time Customer Discount"
description: "Apply 10% discount for first-time customers"
expression: isFirstTimeCustomer() then applyDiscount(10, "FIRST_TIME")
priority: 20
- name: "Large Order Discount"
description: "Apply 5% discount for orders over $1000"
expression: isOrderValueAbove(1000) then applyDiscount(5, "LARGE_ORDER")
priority: 30
- name: "Weekend Flash Sale"
description: "Apply 20% discount during weekend flash sale hours"
expression: isWeekend() AND isTimeBetween("10:00", "14:00") then applyDiscount(20, "FLASH_SALE")
priority: 5
Fraud Detection Rules
rules:
- name: "High-Risk Country Order"
description: "Flag orders from high-risk countries for review"
expression: isFromHighRiskCountry() then flagForReview("COUNTRY_RISK", "HIGH")
priority: 10
- name: "Multiple Failed Payments"
description: "Block accounts with multiple failed payment attempts"
expression: hasFailedPaymentAttempts(3) then blockAccount("PAYMENT_FAILURES")
priority: 5
- name: "Unusual Purchase Pattern"
description: "Flag unusual purchase patterns for review"
expression: isUnusualPurchasePattern(0.85) then flagForReview("UNUSUAL_PATTERN", "MEDIUM")
priority: 15
Content Moderation Rules
rules:
- name: "Explicit Content Detection"
description: "Automatically reject content flagged as explicit"
expression: containsExplicitContent(0.9) then rejectContent("EXPLICIT")
priority: 5
- name: "Potential Copyright Infringement"
description: "Flag content with potential copyright issues for review"
expression: copyrightMatchScore(0.7) then flagForReview("COPYRIGHT", "HIGH")
priority: 10
Best Practices for Component Usage
1. Consistent Naming Conventions
Use consistent naming patterns for your checks and actions:
- Checks: Use "is", "has", or "can" prefixes for boolean checks
- Actions: Use verb-based names that describe what they do
Good examples:
- isEligibleForDiscount()
- hasCompletedProfile()
- applyDiscount()
- sendNotification()
2. Parameter Organization
- Order parameters from most important to least important
- Use consistent parameter ordering across similar components
- Consider using default values for optional parameters
3. Error Handling
- Validate parameters in your component implementations
- Handle missing context data gracefully
- Provide clear error messages
4. Documentation
- Document the purpose of each check and action clearly
- Describe parameter requirements and expected values
- Include examples of usage in rule expressions
5. Granularity
- Keep checks and actions focused on a single responsibility
- Prefer multiple specific checks over complex checks with many parameters
- Balance specificity with reusability
For example, instead of:
isEligibleForDiscount(type, amount, region, ...)
Consider:
isPreferredCustomerType(type) AND isInTargetRegion(region) then applyDiscount(amount)
6. Common Patterns
Feature Flagging
isFeatureEnabled("feature_name") then enableFeature()
Progressive Discounting
isOrderValueAbove(1000) then applyDiscount(5)
isOrderValueAbove(2000) then applyDiscount(10)
isOrderValueAbove(5000) then applyDiscount(15)
Combined Conditions
isPremiumMember() AND hasItemInCart("PREMIUM_ONLY") then allowPurchase()
Exclusion Rules
isRestrictedUser() then blockOperation("USER_RESTRICTED")
isRestrictedRegion(region) then blockOperation("REGION_RESTRICTED")
Testing Component Usage
When testing rules that use your components:
- Create unit tests for individual checks and actions
- Create integration tests for complete rule executions
- Test edge cases and boundary conditions
- Verify that rules combine components as expected
Example test:
@Test
public void testPremiumMemberDiscount() {
// Setup test context
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
context.add(OrderContextKey.USER_TYPE, "PREMIUM");
context.add(OrderContextKey.ORDER_AMOUNT, 100.0);
// Execute rule set
RuleExecutionResult<OrderContextKey> result = discountOrchestrator.executeAllMatchingRules(context);
// Verify results
assertTrue(result.hasMatches());
assertEquals(1, result.getMatchedRules().size());
assertEquals("Premium Member Discount", result.getMatchedRules().get(0).getName());
assertEquals(85.0, context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class), 0.01);
}
Performance Considerations
-
Keep Checks Lightweight: Checks are evaluated first and potentially more often than actions.
-
Cache Expensive Operations: For checks that need expensive operations:
```java
public Value execute(RuleContext
if (cachedResult.isPresent()) {
return Value.of(cachedResult.get());
}
// Perform expensive calculation
boolean result = performExpensiveOperation();
// Cache the result in the context
context.add(MyContextKey.CACHED_EXPENSIVE_CHECK_RESULT, result);
return Value.of(result);
} ```
- Optimize Order of Checks: In complex expressions, put faster/more likely to fail checks first:
isFastCheck() AND isExpensiveCheck() then doAction()
- Reuse Context Objects: When executing multiple rule sets, reuse the same context object.
Business Components Implementation
This guide provides detailed instructions for implementing Business Checks and Business Actions in Axiom, covering both basic and advanced implementation patterns.
Core Implementation Requirements
Both Business Checks and Business Actions implement the RuleFunction<K>
interface, which defines a single method:
public interface RuleFunction<K extends Enum<K>> {
Value execute(RuleContext<K> context, Value... args);
}
They are distinguished by implementing their respective marker interfaces:
public interface BusinessCheck<K extends Enum<K>> extends RuleFunction<K> {
// No additional methods - marker interface
}
public interface BusinessAction<K extends Enum<K>> extends RuleFunction<K> {
// No additional methods - marker interface
}
Basic Implementation Pattern
Business Check Implementation
Here's a basic implementation of a Business Check:
@RuleMetadata(
name = "isHighValueOrder",
description = "Checks if the order value exceeds a specified threshold"
)
public class HighValueOrderCheck implements BusinessCheck<OrderContextKey> {
@Override
public Value execute(RuleContext<OrderContextKey> context, @Arg("threshold") Value threshold) {
// Get the order amount from the context
Double orderAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
// Get the threshold value from the argument
Double thresholdValue = threshold.asNumber().doubleValue();
// Return true if the order amount exceeds the threshold
return Value.of(orderAmount >= thresholdValue);
}
}
Business Action Implementation
Here's a basic implementation of a Business Action:
@RuleMetadata(
name = "applyDiscount",
description = "Applies a percentage discount to the order amount"
)
public class ApplyDiscountAction implements BusinessAction<OrderContextKey> {
@Override
public Value execute(RuleContext<OrderContextKey> context, @Arg("percentage") Value percentage) {
// Get the order amount from the context
Double orderAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
// Get the discount percentage from the argument
Double discountPercentage = percentage.asNumber().doubleValue();
// Calculate the discounted amount
Double discountedAmount = orderAmount * (1 - (discountPercentage / 100));
// Update the context with the discounted amount
context.add(OrderContextKey.ORDER_AMOUNT, discountedAmount);
context.add(OrderContextKey.DISCOUNT_APPLIED, true);
context.add(OrderContextKey.DISCOUNT_PERCENTAGE, discountPercentage);
// Return true to indicate success
return Value.of(true);
}
}
The @RuleMetadata Annotation
The @RuleMetadata
annotation is crucial for registering your business components with the Axiom framework. It provides metadata that is used for:
- Identifying the component in rule expressions
- Validating rule expressions
- Documenting the component's purpose
Required fields:
- name
: The name used to reference the component in rule expressions
- description
: A human-readable description of the component's purpose
Example:
@RuleMetadata(
name = "hasRiskScore",
description = "Checks if the risk score exceeds a specified threshold"
)
The @Arg Annotation
The @Arg
annotation is used to name the parameters in the execute
method. This is important for:
- Documentation and code readability
- Validation of rule expressions
- Better error messages when arguments are missing or invalid
Example:
public Value execute(RuleContext<OrderContextKey> context,
@Arg("threshold") Value threshold,
@Arg("tolerance") Value tolerance) {
// Implementation
}
Advanced Implementation Patterns
Service-Dependent Components
For business components that need to interact with external services:
@RuleMetadata(
name = "hasGoodCreditScore",
description = "Checks if the customer has a good credit score"
)
public class CreditScoreCheck implements BusinessCheck<CustomerContextKey> {
private final CreditScoreService creditScoreService;
@Inject
public CreditScoreCheck(CreditScoreService creditScoreService) {
this.creditScoreService = creditScoreService;
}
@Override
public Value execute(RuleContext<CustomerContextKey> context, @Arg("minScore") Value minScore) {
// Get the customer ID from the context
String customerId = context.getRequired(CustomerContextKey.CUSTOMER_ID, String.class);
// Get the minimum score from the argument
int minimumScore = minScore.asNumber().intValue();
// Call the credit score service
int creditScore = creditScoreService.getCreditScore(customerId);
// Add the credit score to the context for potential use by other components
context.add(CustomerContextKey.CREDIT_SCORE, creditScore);
// Return true if the credit score is at least the minimum
return Value.of(creditScore >= minimumScore);
}
}
Composite Checks
For complex conditions that combine multiple simpler checks:
@RuleMetadata(
name = "isEligibleForPremiumOffer",
description = "Checks if a customer is eligible for premium offers"
)
public class PremiumOfferEligibilityCheck implements BusinessCheck<CustomerContextKey> {
private final BusinessCheck<CustomerContextKey> loyaltyCheck;
private final BusinessCheck<CustomerContextKey> spendingCheck;
private final BusinessCheck<CustomerContextKey> regionCheck;
@Inject
public PremiumOfferEligibilityCheck(
@Named("isLoyalCustomer") BusinessCheck<CustomerContextKey> loyaltyCheck,
@Named("hasHighSpending") BusinessCheck<CustomerContextKey> spendingCheck,
@Named("isInTargetRegion") BusinessCheck<CustomerContextKey> regionCheck) {
this.loyaltyCheck = loyaltyCheck;
this.spendingCheck = spendingCheck;
this.regionCheck = regionCheck;
}
@Override
public Value execute(RuleContext<CustomerContextKey> context) {
// Create values for the sub-checks
Value loyaltyResult = loyaltyCheck.execute(context);
Value spendingResult = spendingCheck.execute(context, Value.of(1000)); // Min spending $1000
Value regionResult = regionCheck.execute(context);
// Customer is eligible if they are loyal AND either have high spending OR are in target region
boolean isEligible = loyaltyResult.asBoolean() &&
(spendingResult.asBoolean() || regionResult.asBoolean());
return Value.of(isEligible);
}
}
Stateful Actions
For actions that need to maintain state between executions:
@RuleMetadata(
name = "limitDiscountUsage",
description = "Limits the number of times a discount can be used and applies it if allowed"
)
@Singleton // Important to ensure state is maintained
public class LimitedDiscountAction implements BusinessAction<OrderContextKey> {
private final Map<String, Integer> discountUsageByCustomer = new ConcurrentHashMap<>();
private final int maxUsagePerCustomer;
@Inject
public LimitedDiscountAction(@Named("maxDiscountUsage") int maxUsagePerCustomer) {
this.maxUsagePerCustomer = maxUsagePerCustomer;
}
@Override
public Value execute(RuleContext<OrderContextKey> context, @Arg("percentage") Value percentage) {
// Get the customer ID
String customerId = context.getRequired(OrderContextKey.CUSTOMER_ID, String.class);
// Get current usage
int currentUsage = discountUsageByCustomer.getOrDefault(customerId, 0);
// Check if the customer has reached the limit
if (currentUsage >= maxUsagePerCustomer) {
context.add(OrderContextKey.DISCOUNT_DENIED_REASON, "Usage limit reached");
return Value.of(false); // Discount not applied
}
// Increment usage
discountUsageByCustomer.put(customerId, currentUsage + 1);
// Get the order amount
Double orderAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
// Apply the discount
Double discountPercentage = percentage.asNumber().doubleValue();
Double discountedAmount = orderAmount * (1 - (discountPercentage / 100));
// Update the context
context.add(OrderContextKey.ORDER_AMOUNT, discountedAmount);
context.add(OrderContextKey.DISCOUNT_APPLIED, true);
context.add(OrderContextKey.DISCOUNT_PERCENTAGE, discountPercentage);
context.add(OrderContextKey.DISCOUNT_USAGE_COUNT, currentUsage + 1);
return Value.of(true); // Discount applied
}
}
Configurable Components
For components that need configuration beyond constructor injection:
@RuleMetadata(
name = "isInPromotionPeriod",
description = "Checks if the current date is within a configurable promotion period"
)
public class PromotionPeriodCheck implements BusinessCheck<OrderContextKey> {
private final ZonedDateTime startDate;
private final ZonedDateTime endDate;
private final String promotionCode;
@Inject
public PromotionPeriodCheck(
@Named("promotionConfig") PromotionConfiguration config) {
this.startDate = config.getStartDate();
this.endDate = config.getEndDate();
this.promotionCode = config.getPromotionCode();
}
@Override
public Value execute(RuleContext<OrderContextKey> context) {
// Get the current date
ZonedDateTime now = ZonedDateTime.now();
// Check if the current date is within the promotion period
boolean isInPeriod = !now.isBefore(startDate) && !now.isAfter(endDate);
// If in period, add the promotion code to the context
if (isInPeriod) {
context.add(OrderContextKey.ACTIVE_PROMOTION_CODE, promotionCode);
}
return Value.of(isInPeriod);
}
public static class PromotionConfiguration {
private final ZonedDateTime startDate;
private final ZonedDateTime endDate;
private final String promotionCode;
public PromotionConfiguration(
ZonedDateTime startDate,
ZonedDateTime endDate,
String promotionCode) {
this.startDate = startDate;
this.endDate = endDate;
this.promotionCode = promotionCode;
}
public ZonedDateTime getStartDate() { return startDate; }
public ZonedDateTime getEndDate() { return endDate; }
public String getPromotionCode() { return promotionCode; }
}
}
Handling Multiple Arguments
For business components that need multiple parameters:
@RuleMetadata(
name = "isWithinRange",
description = "Checks if a value is within a specified range"
)
public class RangeCheck implements BusinessCheck<OrderContextKey> {
@Override
public Value execute(RuleContext<OrderContextKey> context,
@Arg("target") Value target,
@Arg("min") Value min,
@Arg("max") Value max) {
// Get the values from the arguments
String targetKey = target.asString();
Double minValue = min.asNumber().doubleValue();
Double maxValue = max.asNumber().doubleValue();
// Get the target value from the context
Double targetValue = context.getRequired(OrderContextKey.valueOf(targetKey), Double.class);
// Check if the value is within range
boolean isWithinRange = targetValue >= minValue && targetValue <= maxValue;
return Value.of(isWithinRange);
}
}
// Usage in YAML:
// expression: isWithinRange("ORDER_AMOUNT", 100, 1000) then applyStandardProcessing()
Error Handling
Proper error handling is crucial for robust business components:
@RuleMetadata(
name = "validateCustomerData",
description = "Validates customer data and flags issues"
)
public class CustomerDataValidationCheck implements BusinessCheck<CustomerContextKey> {
@Override
public Value execute(RuleContext<CustomerContextKey> context) {
try {
// Get required data
String customerId = context.getRequired(CustomerContextKey.CUSTOMER_ID, String.class);
String email = context.getRequired(CustomerContextKey.EMAIL, String.class);
// Validate email format
if (!isValidEmail(email)) {
context.add(CustomerContextKey.VALIDATION_ERRORS, List.of("Invalid email format"));
return Value.of(false);
}
// More validations...
return Value.of(true); // All validations passed
} catch (ContextException e) {
// Handle missing required data
String missingField = extractMissingFieldName(e.getMessage());
context.add(CustomerContextKey.VALIDATION_ERRORS,
List.of("Missing required field: " + missingField));
return Value.of(false);
} catch (Exception e) {
// Handle unexpected errors
context.add(CustomerContextKey.VALIDATION_ERRORS,
List.of("Unexpected error: " + e.getMessage()));
return Value.of(false);
}
}
private boolean isValidEmail(String email) {
// Email validation logic
return email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
private String extractMissingFieldName(String message) {
// Extract field name from error message
// Simplified example
return message.contains("No value for key")
? message.substring(message.lastIndexOf('.') + 1)
: "unknown";
}
}
Logging and Monitoring
Adding logging and monitoring to business components:
@RuleMetadata(
name = "applyRiskBasedFee",
description = "Applies a fee based on the risk level"
)
public class RiskBasedFeeAction implements BusinessAction<OrderContextKey> {
private final Logger logger = LoggerFactory.getLogger(RiskBasedFeeAction.class);
private final MetricsRegistry metrics;
@Inject
public RiskBasedFeeAction(MetricsRegistry metrics) {
this.metrics = metrics;
}
@Override
public Value execute(RuleContext<OrderContextKey> context) {
String orderId = context.getRequired(OrderContextKey.ORDER_ID, String.class);
Integer riskScore = context.getRequired(OrderContextKey.RISK_SCORE, Integer.class);
Double orderAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
logger.debug("Calculating risk-based fee for order {} with risk score {}",
orderId, riskScore);
// Measure execution time
long startTime = System.nanoTime();
try {
// Calculate fee percentage based on risk score
double feePercentage = calculateFeePercentage(riskScore);
// Track the fee percentage applied
metrics.recordValue("risk.fee.percentage", feePercentage);
// Calculate fee amount
double feeAmount = orderAmount * (feePercentage / 100);
// Update context
context.add(OrderContextKey.FEE_PERCENTAGE, feePercentage);
context.add(OrderContextKey.FEE_AMOUNT, feeAmount);
context.add(OrderContextKey.TOTAL_WITH_FEES, orderAmount + feeAmount);
logger.info("Applied {}% risk-based fee (${}) to order {}",
feePercentage, feeAmount, orderId);
return Value.of(true);
} catch (Exception e) {
logger.error("Error applying risk-based fee to order {}: {}",
orderId, e.getMessage(), e);
metrics.incrementCounter("risk.fee.errors");
return Value.of(false);
} finally {
long duration = System.nanoTime() - startTime;
metrics.recordTiming("risk.fee.calculation.time", duration / 1_000_000); // Convert to ms
}
}
private double calculateFeePercentage(int riskScore) {
// Example logic:
if (riskScore < 20) return 0.0; // Low risk - no fee
if (riskScore < 50) return 1.0; // Medium risk - 1% fee
if (riskScore < 80) return 2.5; // High risk - 2.5% fee
return 5.0; // Very high risk - 5% fee
}
}
Testing Considerations
When implementing business components, consider their testability:
-
Dependency Injection: Use constructor injection for dependencies to enable easy mocking in tests.
-
Pure Functions: When possible, make your components pure functions that don't have side effects beyond the context.
-
Clear Responsibilities: Keep components focused on a single responsibility for easier testing.
-
Testable Arguments: Design your argument structure to be easily testable with various inputs.
For more detailed testing approaches, see the Rule Testing guide.
Registration in AxiomModule
After implementing your business components, register them in your AxiomModule
:
public class OrderProcessingModule extends AxiomModule<OrderContextKey> {
public OrderProcessingModule() {
super(OrderContextKey.class);
}
@Override
protected void configureBusinessRules(
MapBinder<String, BusinessCheck<OrderContextKey>> checks,
MapBinder<String, BusinessAction<OrderContextKey>> actions) {
// Register business checks
checks.addBinding("isHighValueOrder").to(HighValueOrderCheck.class);
checks.addBinding("isRepeatCustomer").to(RepeatCustomerCheck.class);
checks.addBinding("isWithinRange").to(RangeCheck.class);
checks.addBinding("isInPromotionPeriod").to(PromotionPeriodCheck.class);
// Register business actions
actions.addBinding("applyDiscount").to(ApplyDiscountAction.class);
actions.addBinding("limitDiscountUsage").to(LimitedDiscountAction.class);
actions.addBinding("applyRiskBasedFee").to(RiskBasedFeeAction.class);
}
// Configure necessary bindings for components
@Provides
@Singleton
@Named("promotionConfig")
PromotionPeriodCheck.PromotionConfiguration providePromotionConfig() {
return new PromotionPeriodCheck.PromotionConfiguration(
ZonedDateTime.parse("2023-11-24T00:00:00Z"), // Black Friday
ZonedDateTime.parse("2023-12-31T23:59:59Z"), // New Year's Eve
"HOLIDAY2023"
);
}
@Provides
@Named("maxDiscountUsage")
int provideMaxDiscountUsage() {
return 3; // Each customer can use the discount up to 3 times
}
}
Best Practices
-
Single Responsibility: Each business component should focus on a single responsibility.
-
Clear Naming: Choose clear, descriptive names for your components that indicate their purpose.
-
Comprehensive Documentation: Provide detailed descriptions in
@RuleMetadata
for self-documenting code. -
Argument Validation: Validate all arguments to ensure they are of the expected type and value range.
-
Context Safety: Be careful when modifying the context; avoid removing or overwriting values unexpectedly.
-
Error Handling: Implement robust error handling to prevent rule execution failures.
-
Thread Safety: Ensure your components are thread-safe, especially if they maintain state.
-
Performance Consciousness: Be mindful of performance, especially for components that interact with external services.
-
Consistent Return Values: Always return
Value
objects consistently; useValue.of(true)
to indicate success. -
Test Coverage: Write comprehensive tests for your business components to ensure they behave as expected.
Related Sections
- Business Components Overview - Core concepts of business components
- Business Components Definition - How to define components in rule sets
- Business Components Usage - How to use components in rules
- Business Components Validation - How to validate components
Business Components Validation
Validation is a critical aspect of business component development in Axiom. This document covers validation patterns, error handling, and best practices to ensure your business checks and actions operate reliably.
Importance of Validation
Proper validation in business components ensures:
- Robustness: Components can handle unexpected inputs without crashing
- Correctness: Business logic operates on valid data only
- Clear Feedback: Users receive meaningful error messages when something goes wrong
- Security: Input validation helps prevent security vulnerabilities
- Maintainability: Validation acts as documentation of expected inputs
Input Validation Patterns
Parameter Validation
Always validate parameters passed to your business checks and actions:
@Override
public Value execute(RuleContext<OrderContextKey> context, @Arg("discount") Value discountParam) {
// Validate parameter is present and is a number
if (discountParam == null) {
throw new IllegalArgumentException("Discount parameter is required");
}
if (!discountParam.isNumber()) {
throw new IllegalArgumentException("Discount must be a number");
}
Double discount = discountParam.asNumber().doubleValue();
// Validate discount is within acceptable range
if (discount < 0 || discount > 100) {
throw new IllegalArgumentException("Discount must be between 0 and 100");
}
// Continue with execution...
}
Context Data Validation
Always validate data retrieved from the context:
@Override
public Value execute(RuleContext<CustomerContextKey> context) {
// Check if required key exists
if (!context.hasValue(CustomerContextKey.CUSTOMER_ID)) {
throw new MissingContextDataException("Customer ID is required");
}
// Get and validate customer ID
String customerId = context.getRequired(CustomerContextKey.CUSTOMER_ID, String.class);
if (customerId.isEmpty()) {
throw new InvalidContextDataException("Customer ID cannot be empty");
}
// Get optional data with validation
Optional<Integer> age = context.getOptional(CustomerContextKey.AGE, Integer.class);
if (age.isPresent() && (age.get() < 0 || age.get() > 120)) {
throw new InvalidContextDataException("Age must be between 0 and 120");
}
// Continue with execution...
}
Type Validation
When working with complex types, validate their structure:
@Override
public Value execute(RuleContext<OrderContextKey> context) {
// Get the order from context
Order order = context.getRequired(OrderContextKey.ORDER, Order.class);
// Validate order structure
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new InvalidContextDataException("Order must contain at least one item");
}
if (order.getCustomerId() == null || order.getCustomerId().isEmpty()) {
throw new InvalidContextDataException("Order must have a customer ID");
}
// Validate individual items
for (OrderItem item : order.getItems()) {
if (item.getQuantity() <= 0) {
throw new InvalidContextDataException("Order item quantity must be greater than zero");
}
if (item.getPrice() < 0) {
throw new InvalidContextDataException("Order item price cannot be negative");
}
}
// Continue with execution...
}
Custom Validation Exceptions
Define custom exceptions for different validation scenarios:
/**
* Exception thrown when required data is missing from the context.
*/
public class MissingContextDataException extends RuntimeException {
public MissingContextDataException(String message) {
super(message);
}
}
/**
* Exception thrown when data in the context is invalid.
*/
public class InvalidContextDataException extends RuntimeException {
public InvalidContextDataException(String message) {
super(message);
}
}
/**
* Exception thrown when a parameter is invalid.
*/
public class InvalidParameterException extends RuntimeException {
private final String parameterName;
public InvalidParameterException(String parameterName, String message) {
super(message);
this.parameterName = parameterName;
}
public String getParameterName() {
return parameterName;
}
}
Validation Utilities
Create reusable validation utilities to simplify common validation tasks:
/**
* Utility class for validating rule context data.
*/
public class ContextValidator {
/**
* Validates that required keys are present in the context.
*
* @param context The rule context to validate
* @param keys The keys that must be present
* @throws MissingContextDataException if any key is missing
*/
public static <T extends Enum<T>> void validateRequiredKeys(RuleContext<T> context, T... keys) {
for (T key : keys) {
if (!context.hasValue(key)) {
throw new MissingContextDataException("Required key missing: " + key);
}
}
}
/**
* Validates a numeric value is within a given range.
*
* @param value The value to validate
* @param min The minimum allowed value (inclusive)
* @param max The maximum allowed value (inclusive)
* @param errorMessage The error message if validation fails
* @throws InvalidContextDataException if validation fails
*/
public static void validateRange(Double value, Double min, Double max, String errorMessage) {
if (value < min || value > max) {
throw new InvalidContextDataException(errorMessage);
}
}
/**
* Validates a string is not null or empty.
*
* @param value The string to validate
* @param errorMessage The error message if validation fails
* @throws InvalidContextDataException if validation fails
*/
public static void validateNotEmpty(String value, String errorMessage) {
if (value == null || value.trim().isEmpty()) {
throw new InvalidContextDataException(errorMessage);
}
}
// More validation methods...
}
Using these utilities in your components:
@Override
public Value execute(RuleContext<OrderContextKey> context, @Arg("threshold") Value threshold) {
// Validate required context keys
ContextValidator.validateRequiredKeys(context,
OrderContextKey.ORDER_ID,
OrderContextKey.CUSTOMER_ID,
OrderContextKey.ORDER_AMOUNT);
// Validate order amount
Double amount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
ContextValidator.validateRange(amount, 0.0, Double.MAX_VALUE,
"Order amount must be positive");
// Validate customer ID
String customerId = context.getRequired(OrderContextKey.CUSTOMER_ID, String.class);
ContextValidator.validateNotEmpty(customerId, "Customer ID cannot be empty");
// Continue with execution...
}
Defensive Validation Approaches
Graceful Fallbacks
For non-critical validations, consider using fallbacks instead of throwing exceptions:
@Override
public Value execute(RuleContext<OrderContextKey> context) {
// Get discount with fallback to zero
Optional<Double> discountOpt = context.getOptional(OrderContextKey.DISCOUNT_PERCENTAGE, Double.class);
Double discount = discountOpt.orElse(0.0);
// Clamp discount to valid range
discount = Math.max(0.0, Math.min(discount, 100.0));
// Get order amount with validation
Double amount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
if (amount < 0) {
logger.warn("Negative order amount detected: {}. Using absolute value.", amount);
amount = Math.abs(amount);
}
// Apply discount
Double discountedAmount = amount * (1 - (discount / 100.0));
context.add(OrderContextKey.DISCOUNTED_AMOUNT, discountedAmount);
return Value.of(true);
}
Validation Levels
Consider implementing different validation levels based on your application needs:
public enum ValidationLevel {
STRICT, // Throw exceptions for any validation issue
NORMAL, // Throw exceptions for critical issues, log warnings for others
LENIENT // Log warnings but try to proceed for most issues
}
@Override
public Value execute(RuleContext<OrderContextKey> context, @Arg("amount") Value amountParam) {
// Get validation level from context or use default
ValidationLevel level = context
.getOptional(OrderContextKey.VALIDATION_LEVEL, ValidationLevel.class)
.orElse(ValidationLevel.NORMAL);
// Validate amount based on level
Double amount = amountParam.asNumber().doubleValue();
if (amount < 0) {
switch (level) {
case STRICT:
throw new InvalidParameterException("amount", "Amount cannot be negative");
case NORMAL:
logger.warn("Negative amount detected: {}. Using absolute value.", amount);
amount = Math.abs(amount);
break;
case LENIENT:
logger.info("Using negative amount as provided: {}", amount);
break;
}
}
// Continue with execution...
}
Validation Testing
Unit Testing Validations
Always include tests specifically for validation scenarios:
@Test
public void testExecute_withNegativeDiscountParam_throwsException() {
// Setup
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
Value negativeDiscount = Value.of(-10);
// Execute and verify exception
assertThrows(IllegalArgumentException.class, () -> {
discountAction.execute(context, negativeDiscount);
});
}
@Test
public void testExecute_withMissingCustomerId_throwsMissingContextDataException() {
// Setup
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
context.add(OrderContextKey.ORDER_AMOUNT, 100.0);
// Note: Not adding CUSTOMER_ID
// Execute and verify exception
assertThrows(MissingContextDataException.class, () -> {
customerDiscountCheck.execute(context, Value.of(10));
});
}
Testing Error Messages
Test that error messages are clear and informative:
@Test
public void testExecute_withInvalidParam_providesHelpfulErrorMessage() {
// Setup
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
Value invalidDiscount = Value.of(150); // Over 100%
// Execute and verify exception message
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
discountAction.execute(context, invalidDiscount);
});
assertTrue(exception.getMessage().contains("between 0 and 100"),
"Error message should explain the valid range");
}
Best Practices
1. Validate Early
Perform validation as early as possible in your business component:
@Override
public Value execute(RuleContext<T> context, @Arg("param") Value param) {
// Validate inputs first
validateInputs(context, param);
// Then proceed with business logic
// ...
}
private void validateInputs(RuleContext<T> context, Value param) {
// Validation logic here
}
2. Be Specific About Requirements
In your error messages, be specific about what is expected:
// Poor: "Invalid input"
// Better: "Age must be between 18 and 120"
if (age < 18 || age > 120) {
throw new InvalidContextDataException("Age must be between 18 and 120, got: " + age);
}
3. Log Validation Failures
Always log validation failures with appropriate levels:
try {
// Validate user input
if (userId.isEmpty()) {
logger.warn("Empty user ID provided");
throw new InvalidContextDataException("User ID cannot be empty");
}
// Continue with execution...
} catch (Exception e) {
logger.error("Validation failed: {}", e.getMessage(), e);
throw e;
}
4. Document Validation Requirements
Document all validation rules in your component's Javadoc:
/**
* Applies a percentage discount to the order amount.
*
* <p>Validation requirements:</p>
* <ul>
* <li>Context must contain ORDER_AMOUNT (Double, positive)</li>
* <li>Context must contain CUSTOMER_ID (String, non-empty)</li>
* <li>Discount parameter must be a number between 0 and 100</li>
* </ul>
*
* @throws MissingContextDataException if required context keys are missing
* @throws InvalidContextDataException if context data is invalid
* @throws IllegalArgumentException if discount parameter is invalid
*/
@Override
public Value execute(RuleContext<OrderContextKey> context, @Arg("discount") Value discountParam) {
// Implementation...
}
5. Use Default Values Carefully
When using default values, make sure they are safe and appropriate:
// Get order date with fallback to current date
LocalDate orderDate = context
.getOptional(OrderContextKey.ORDER_DATE, LocalDate.class)
.orElse(LocalDate.now());
6. Fail Fast for Critical Validations
For critical validation issues, fail fast:
// Security validation - fail fast
if (!isAuthorized(userId, requestedAction)) {
logger.error("Unauthorized action attempt: User {} tried to perform {}", userId, requestedAction);
throw new SecurityValidationException("User is not authorized to perform this action");
}
// Continue with other validations...
7. Separate Validation Logic
For complex components, separate validation logic for better maintainability:
public class CustomerDiscountCheck implements BusinessCheck<OrderContextKey> {
@Override
public Value execute(RuleContext<OrderContextKey> context, @Arg("threshold") Value threshold) {
// Validate inputs
ValidationResult result = validateInputs(context, threshold);
if (!result.isValid()) {
throw result.getException();
}
// Business logic
// ...
}
private ValidationResult validateInputs(RuleContext<OrderContextKey> context, Value threshold) {
ValidationResult result = new ValidationResult();
// Check required context keys
if (!context.hasValue(OrderContextKey.CUSTOMER_ID)) {
return result.invalid(new MissingContextDataException("Customer ID is required"));
}
if (!context.hasValue(OrderContextKey.ORDER_AMOUNT)) {
return result.invalid(new MissingContextDataException("Order amount is required"));
}
// Validate threshold parameter
if (!threshold.isNumber()) {
return result.invalid(
new IllegalArgumentException("Threshold must be a number"));
}
Double thresholdValue = threshold.asNumber().doubleValue();
if (thresholdValue < 0) {
return result.invalid(
new IllegalArgumentException("Threshold cannot be negative"));
}
return result.valid();
}
private static class ValidationResult {
private boolean valid = true;
private RuntimeException exception;
public ValidationResult valid() {
this.valid = true;
return this;
}
public ValidationResult invalid(RuntimeException exception) {
this.valid = false;
this.exception = exception;
return this;
}
public boolean isValid() {
return valid;
}
public RuntimeException getException() {
return exception;
}
}
}
Integration with Axiom Validation Framework
Axiom provides a validation framework that works at the rule set level. Business component validations are complementary to this framework:
- Rule Set Validation: Ensures rule sets are structurally valid before loading
- Business Component Validation: Ensures runtime behavior is correct during execution
You can leverage the rule set validation framework to register custom validators for your business components:
public class BusinessComponentValidator<T extends Enum<T>> implements RuleSetValidator<T> {
@Override
public List<ValidationError> validate(RuleSet<T> ruleSet,
Map<String, BusinessCheck<T>> checks,
Map<String, BusinessAction<T>> actions) {
List<ValidationError> errors = new ArrayList<>();
// Validate that referenced checks exist
for (Rule<T> rule : ruleSet.getRules()) {
String expression = rule.getExpression();
// Extract check names from expression and validate they exist
List<String> checkNames = extractCheckNames(expression);
for (String checkName : checkNames) {
if (!checks.containsKey(checkName)) {
errors.add(new ValidationError(
"MissingCheckError",
"Check '" + checkName + "' referenced in rule '" + rule.getName() +
"' is not defined",
null
));
}
}
// Similar validation for actions
// ...
}
return errors;
}
private List<String> extractCheckNames(String expression) {
// Logic to extract check names from expression
// ...
}
}
Register your custom validator with the rule set loader:
RuleSetLoader<MyContextKey> loader = new YamlRuleSetLoader<>("ruleset.yaml")
.addValidator(new BusinessComponentValidator<>());
Rule Orchestrator
Rule Orchestrators Overview
The RuleOrchestrator
is a central component in the Axiom framework that coordinates the evaluation and execution of business rules. It serves as the main entry point for rule execution and provides methods to apply rules from a rule set against a given context.
Key Responsibilities
The RuleOrchestrator
has several key responsibilities:
- Rule Execution: Evaluates rule conditions and executes rule actions
- Rule Selection: Determines which rules match a given context
- Result Handling: Provides detailed results of rule execution
- Execution Strategies: Supports different execution strategies (first match, all matches)
Creating a Rule Orchestrator
A RuleOrchestrator
is created with a reference to a RuleSet
:
// Create a rule orchestrator for a rule set
RuleOrchestrator<MyContextKey> orchestrator = new RuleOrchestrator<>(ruleSet);
In a dependency injection environment like Guice, you would typically inject the orchestrator:
@Inject
@Named("fraud_detection_ruleset")
private RuleOrchestrator<MyContextKey> fraudDetectionOrchestrator;
Execution Methods
The RuleOrchestrator
provides several methods for executing rules:
Execute First Match
Executes the first matching rule in priority order:
// Create a context for rule evaluation
RuleContext<MyContextKey> context = new RuleContext<>(MyContextKey.class);
context.add(MyContextKey.TRANSACTION_AMOUNT, 5000.0);
context.add(MyContextKey.RISK_SCORE, 95);
// Execute the first matching rule
RuleExecutionResult<MyContextKey> result = orchestrator.executeFirstMatchingRule(context);
if (result.hasMatch()) {
BusinessRule<MyContextKey> matchedRule = result.getMatchedRule();
System.out.println("Rule applied: " + matchedRule.getName());
}
Execute All Matches
Executes all matching rules in priority order:
// Execute all matching rules
RuleExecutionResult<MyContextKey> result = orchestrator.executeAllMatchingRules(context);
if (result.hasMatches()) {
List<BusinessRule<MyContextKey>> matchedRules = result.getMatchedRules();
System.out.println("Number of rules applied: " + matchedRules.size());
// Print the names of all matched rules
matchedRules.forEach(rule -> System.out.println("Rule applied: " + rule.getName()));
}
Get Matching Rules
Retrieves all matching rules without executing their actions:
// Get all matching rules without executing them
List<BusinessRule<MyContextKey>> matchingRules = orchestrator.getMatchingRules(context);
System.out.println("Number of matching rules: " + matchingRules.size());
Get First Matching Rule
Retrieves the first matching rule without executing its actions:
// Get the first matching rule without executing it
Optional<BusinessRule<MyContextKey>> firstMatchingRule = orchestrator.getFirstMatchingRule(context);
if (firstMatchingRule.isPresent()) {
System.out.println("First matching rule: " + firstMatchingRule.get().getName());
}
Working with Rule Execution Results
The RuleExecutionResult
class provides detailed information about the execution of rules:
RuleExecutionResult<MyContextKey> result = orchestrator.executeFirstMatchingRule(context);
// Check if any rule matched
boolean hasMatch = result.hasMatch();
// Get the matched rule (for executeFirstMatchingRule)
BusinessRule<MyContextKey> matchedRule = result.getMatchedRule();
// Get all matched rules (for executeAllMatchingRules)
List<BusinessRule<MyContextKey>> matchedRules = result.getMatchedRules();
// Get the execution context (which may have been modified by rule actions)
RuleContext<MyContextKey> resultContext = result.getContext();
Integration with Dependency Injection
The RuleOrchestrator
is typically integrated with a dependency injection framework like Guice using the AxiomModule
:
public class MyAxiomModule extends AxiomModule<MyContextKey> {
@Override
protected Map<String, RuleSetLoader<MyContextKey>> getRegisteredLoaders() {
Map<String, RuleSetLoader<MyContextKey>> loaders = new HashMap<>();
loaders.put("fraud_detection", new YamlRuleSetLoader<>("fraud_detection_ruleset.yaml"));
loaders.put("high_value_approval", new YamlRuleSetLoader<>("high_value_approval_ruleset.yaml"));
return loaders;
}
@Override
protected void configureBusinessRules(
MapBinder<String, BusinessCheck<MyContextKey>> checks,
MapBinder<String, BusinessAction<MyContextKey>> actions) {
// Register business checks
checks.addBinding("hasFraudSignals").to(HasFraudSignalsCheck.class);
checks.addBinding("hasRiskScore").to(HasRiskScoreCheck.class);
// Register business actions
actions.addBinding("blockTransaction").to(BlockTransactionAction.class);
actions.addBinding("flagForReview").to(FlagForReviewAction.class);
}
}
Then, in your application code, you can inject the orchestrators:
@Inject
@Named("fraud_detection")
private RuleOrchestrator<MyContextKey> fraudDetectionOrchestrator;
@Inject
@Named("high_value_approval")
private RuleOrchestrator<MyContextKey> highValueApprovalOrchestrator;
Best Practices
-
Use Named Orchestrators: When working with multiple rule sets, use named orchestrators to clearly identify which rule set is being used.
-
Handle No Matches: Always check if a rule matched before accessing the matched rule to avoid
NoSuchElementException
. -
Consider Context Modifications: Remember that rule actions can modify the context, so the context after rule execution may be different from the input context.
-
Choose the Right Execution Strategy: Use
executeFirstMatchingRule
when you want only one rule to apply, andexecuteAllMatchingRules
when multiple rules should apply. -
Separate Orchestrators by Domain: Create separate orchestrators for different domains or aspects of your application to keep rule execution focused.
Related Sections
- Rule Orchestrator Injection - How to inject rule orchestrators into your application
- Rule Orchestrator Operations - Detailed information about rule orchestrator operations
- Rule Sets Overview - How rule sets are used with rule orchestrators
- Rule Context Overview - How rule contexts are used with rule orchestrators
Rule Orchestrator Injection
Integrating rule orchestrators into your application requires proper dependency injection. This document covers how to inject and use rule orchestrators in different application components.
Understanding Rule Orchestrator Injection
Rule orchestrators are the entry points for executing rules in your application. They're created automatically by the Axiom module based on your rule set configurations. To use them, you need to:
- Configure the Axiom module with your rule sets
- Inject the appropriate rule orchestrators into your application components
- Invoke the orchestrators at the right points in your business logic
Basic Injection Pattern
The most common pattern is to inject a rule orchestrator directly into a service class:
public class OrderProcessingService {
private final RuleOrchestrator<OrderContextKey> discountRuleOrchestrator;
@Inject
public OrderProcessingService(
@Named("order_discounts") RuleOrchestrator<OrderContextKey> discountRuleOrchestrator) {
this.discountRuleOrchestrator = discountRuleOrchestrator;
}
public Order processOrder(Order order) {
// Create context
RuleContext<OrderContextKey> context = createOrderContext(order);
// Execute rules
RuleExecutionResult<OrderContextKey> result =
discountRuleOrchestrator.executeAllMatchingRules(context);
// Process results
if (result.hasMatches()) {
// Extract data from the context and update the order
updateOrderWithDiscounts(order, result.getContext());
}
return order;
}
private RuleContext<OrderContextKey> createOrderContext(Order order) {
// Create and populate context
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
context.add(OrderContextKey.ORDER_ID, order.getId());
context.add(OrderContextKey.CUSTOMER_ID, order.getCustomerId());
context.add(OrderContextKey.ORDER_AMOUNT, order.getTotalAmount());
// ... add more data as needed
return context;
}
private void updateOrderWithDiscounts(Order order, RuleContext<OrderContextKey> context) {
// Get updated data from context
Double discountedAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
// Update order
order.setTotalAmount(discountedAmount);
// Check for additional data that might have been added by rules
context.getOptional(OrderContextKey.DISCOUNT_REASON, String.class)
.ifPresent(order::setDiscountReason);
}
}
Named Orchestrators
Each rule set is exposed as a named rule orchestrator. The name comes from the key used when registering the rule set loader:
// In your Axiom module configuration
AxiomModule.buildForKey(OrderContextKey.class)
.withRuleLoaders(loaders -> loaders
.loader("order_discounts", new YamlRuleSetLoader<>("discount_rules.yaml"))
.loader("fraud_detection", new YamlRuleSetLoader<>("fraud_rules.yaml"))
)
// ... other configuration
.build();
This creates two named rule orchestrators that can be injected using the @Named
annotation:
@Inject
@Named("order_discounts")
private RuleOrchestrator<OrderContextKey> discountOrchestrator;
@Inject
@Named("fraud_detection")
private RuleOrchestrator<OrderContextKey> fraudDetectionOrchestrator;
Injecting Multiple Orchestrators
Sometimes you need to inject multiple orchestrators into a service:
public class OrderService {
private final RuleOrchestrator<OrderContextKey> discountOrchestrator;
private final RuleOrchestrator<OrderContextKey> fraudDetectionOrchestrator;
private final RuleOrchestrator<OrderContextKey> shippingRulesOrchestrator;
@Inject
public OrderService(
@Named("order_discounts") RuleOrchestrator<OrderContextKey> discountOrchestrator,
@Named("fraud_detection") RuleOrchestrator<OrderContextKey> fraudDetectionOrchestrator,
@Named("shipping_rules") RuleOrchestrator<OrderContextKey> shippingRulesOrchestrator) {
this.discountOrchestrator = discountOrchestrator;
this.fraudDetectionOrchestrator = fraudDetectionOrchestrator;
this.shippingRulesOrchestrator = shippingRulesOrchestrator;
}
public OrderProcessingResult processOrder(Order order) {
// Create a shared context
RuleContext<OrderContextKey> context = createOrderContext(order);
// First check for fraud
RuleExecutionResult<OrderContextKey> fraudResult =
fraudDetectionOrchestrator.executeFirstMatchingRuleingRule(context);
// If fraud detected, reject the order
if (fraudResult.hasMatches()) {
return OrderProcessingResult.rejected(
context.getOptional(OrderContextKey.REJECTION_REASON, String.class)
.orElse("Potential fraud detected"));
}
// Apply discounts
RuleExecutionResult<OrderContextKey> discountResult =
discountOrchestrator.executeAllMatchingRules(context);
// Apply shipping rules
RuleExecutionResult<OrderContextKey> shippingResult =
shippingRulesOrchestrator.executeAllMatchingRules(context);
// Update order with all rule results
updateOrderFromRuleResults(order, context);
return OrderProcessingResult.approved(order);
}
// Helper methods...
}
Injecting All Orchestrators
Sometimes you might want to inject all orchestrators as a map:
public class RuleExecutionService {
private final Map<String, RuleOrchestrator<OrderContextKey>> orchestrators;
@Inject
public RuleExecutionService(Map<String, RuleOrchestrator<OrderContextKey>> orchestrators) {
this.orchestrators = orchestrators;
}
public RuleExecutionResult<OrderContextKey> executeRuleSet(
String ruleSetName, RuleContext<OrderContextKey> context) {
RuleOrchestrator<OrderContextKey> orchestrator = orchestrators.get(ruleSetName);
if (orchestrator == null) {
throw new IllegalArgumentException("No rule set found with name: " + ruleSetName);
}
return orchestrator.executeAllMatchingRules(context);
}
}
To make this work, you need to configure your module to allow multibinding:
@Override
protected void configure() {
// Configure multibinding for rule orchestrators
MapBinder<String, RuleOrchestrator<OrderContextKey>> orchestratorBinder =
MapBinder.newMapBinder(
binder(),
new TypeLiteral<String>() {},
new TypeLiteral<RuleOrchestrator<OrderContextKey>>() {}
);
// Your other configuration...
}
Lazy Injection
For better performance, especially with many rule sets, you might want to use lazy injection:
public class OrderService {
private final Provider<RuleOrchestrator<OrderContextKey>> discountOrchestratorProvider;
@Inject
public OrderService(
@Named("order_discounts") Provider<RuleOrchestrator<OrderContextKey>> discountOrchestratorProvider) {
this.discountOrchestratorProvider = discountOrchestratorProvider;
}
public Order processOrder(Order order) {
// Create context
RuleContext<OrderContextKey> context = createOrderContext(order);
// Get the orchestrator only when needed
RuleOrchestrator<OrderContextKey> orchestrator = discountOrchestratorProvider.get();
// Execute rules
RuleExecutionResult<OrderContextKey> result = orchestrator.executeAllMatchingRules(context);
// Process results...
return order;
}
}
Rule Orchestrators in Spring Applications
If you're using Spring instead of Guice, you can still use Axiom. Here's how to configure rule orchestrators in a Spring context:
@Configuration
public class AxiomConfig {
@Bean
public Module axiomModule() {
return AxiomModule.buildForKey(OrderContextKey.class)
.withRuleLoaders(loaders -> loaders
.loader("order_discounts", new YamlRuleSetLoader<>("discount_rules.yaml"))
.loader("fraud_detection", new YamlRuleSetLoader<>("fraud_rules.yaml"))
)
.withChecks(checks -> checks
.check("isHighValueOrder", HighValueOrderCheck.class)
// ... other checks
)
.withActions(actions -> actions
.action("applyDiscount", ApplyDiscountAction.class)
// ... other actions
)
.build();
}
@Bean
public AbstractModule springIntegrationModule() {
return new AbstractModule() {
@Override
protected void configure() {
// Empty, just for binding the injector
}
};
}
@Bean
public Injector guiceInjector(Module axiomModule, AbstractModule springIntegrationModule) {
return Guice.createInjector(axiomModule, springIntegrationModule);
}
@Bean
@Qualifier("order_discounts")
public RuleOrchestrator<OrderContextKey> discountOrchestrator(Injector injector) {
return injector.getInstance(
Key.get(
new TypeLiteral<RuleOrchestrator<OrderContextKey>>() {},
Names.named("order_discounts")
)
);
}
@Bean
@Qualifier("fraud_detection")
public RuleOrchestrator<OrderContextKey> fraudDetectionOrchestrator(Injector injector) {
return injector.getInstance(
Key.get(
new TypeLiteral<RuleOrchestrator<OrderContextKey>>() {},
Names.named("fraud_detection")
)
);
}
}
Then in your Spring services:
@Service
public class OrderService {
private final RuleOrchestrator<OrderContextKey> discountOrchestrator;
@Autowired
public OrderService(
@Qualifier("order_discounts") RuleOrchestrator<OrderContextKey> discountOrchestrator) {
this.discountOrchestrator = discountOrchestrator;
}
// Service methods...
}
Best Practices
-
Name Consistently: Use consistent naming for your rule sets and orchestrators.
-
Single Responsibility: Each orchestrator should handle a specific domain or function.
-
Reuse Contexts: When using multiple orchestrators in sequence, reuse the same context to accumulate results.
-
Error Handling: Add robust error handling around rule execution.
-
Testing: Mock orchestrators in unit tests to isolate service logic.
-
Documentation: Document the purpose and expected behavior of each orchestrator.
-
Performance: Be mindful of initialization costs, especially with many rule sets.
-
Context Lifecycle: Carefully manage the lifecycle of rule contexts to prevent memory leaks.
Rule Orchestrator Operations
Rule orchestrators are the core execution components in Axiom. This document explains the various operations available on rule orchestrators and how to use them effectively.
Rule Orchestrator Overview
A rule orchestrator is responsible for:
- Managing a specific rule set
- Executing rules against a context
- Applying rule priority and effective date filtering
- Returning execution results
Each rule set is associated with a dedicated rule orchestrator, which is automatically created and registered in your dependency injection container.
Key Methods
Rule orchestrators provide several execution methods to meet different needs:
executeAllMatchingRules
RuleExecutionResult<T> executeAllMatchingRules(RuleContext<T> context);
This method: - Evaluates all rules in the rule set - Executes actions for all rules whose conditions match - Returns a result containing all matched rules
Use this when: - You want to apply all matching rules (e.g., applying multiple discounts) - You need a cumulative effect of multiple rules
Example:
// Create and populate context
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
context.add(OrderContextKey.ORDER_ID, "ORD-12345");
context.add(OrderContextKey.ORDER_AMOUNT, 150.0);
context.add(OrderContextKey.CUSTOMER_TYPE, "PREMIUM");
// Execute all matching rules
RuleExecutionResult<OrderContextKey> result = discountOrchestrator.executeAllMatchingRules(context);
// Get the discounted amount after all rules applied
if (result.hasMatches()) {
Double finalAmount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
System.out.println("Final amount after all discounts: " + finalAmount);
}
executeFirstMatchingRuleingRule
RuleExecutionResult<T> executeFirstMatchingRuleingRule(RuleContext<T> context);
This method: - Evaluates rules in priority order - Stops after finding and executing the first matching rule - Returns a result containing only the first matched rule
Use this when: - You want a single rule to be applied (e.g., choose one discount only) - You need an exclusive behavior (only one rule should win)
Example:
// Create and populate context
RuleContext<UserContextKey> context = new RuleContext<>(UserContextKey.class);
context.add(UserContextKey.USER_ID, "USR-123");
context.add(UserContextKey.IP_ADDRESS, "203.0.113.42");
context.add(UserContextKey.LOGIN_ATTEMPTS, 5);
// Execute first matching rule
RuleExecutionResult<UserContextKey> result = fraudDetectionOrchestrator.executeFirstMatchingRuleingRule(context);
// Handle the result
if (result.hasMatches()) {
Rule<UserContextKey> matchedRule = result.getFirstMatchedRule().get();
System.out.println("Fraud rule matched: " + matchedRule.getName());
// Take action based on the matched rule
Boolean blockUser = context.getOptional(UserContextKey.BLOCK_USER, Boolean.class).orElse(false);
if (blockUser) {
userService.blockUser(context.getRequired(UserContextKey.USER_ID, String.class));
}
}
executeAllByPriority
RuleExecutionResult<T> executeAllByPriority(RuleContext<T> context);
This method: - Evaluates rules in priority order - Executes all matching rules, but maintains priority order - Returns a result with all matched rules in priority order
Use this when: - You want to apply multiple rules but need to ensure they're applied in a specific order - The order of operations matters (e.g., apply base discounts before promotional ones)
Example:
// Create and populate context
RuleContext<DocumentContextKey> context = new RuleContext<>(DocumentContextKey.class);
context.add(DocumentContextKey.DOCUMENT_ID, "DOC-456");
context.add(DocumentContextKey.CONTENT, documentContent);
context.add(DocumentContextKey.METADATA, metadata);
// Execute all rules in priority order
RuleExecutionResult<DocumentContextKey> result = documentProcessingOrchestrator.executeAllByPriority(context);
// Process the results in the order they were applied
if (result.hasMatches()) {
List<Rule<DocumentContextKey>> matchedRules = result.getMatchedRules();
System.out.println("Applied processing rules in this order:");
for (Rule<DocumentContextKey> rule : matchedRules) {
System.out.println("- " + rule.getName() + " (priority: " + rule.getPriority() + ")");
}
// Get the processed document
String processedContent = context.getRequired(DocumentContextKey.CONTENT, String.class);
documentRepository.save(processedContent);
}
evaluateConditions
List<Rule<T>> evaluateConditions(RuleContext<T> context);
This method: - Only evaluates the conditions of rules, without executing any actions - Returns a list of rules whose conditions match - Does not modify the context
Use this when: - You want to preview which rules would match but don't want any actions to be executed - You need to make decisions based on which rules would match - You're implementing a rule authoring UI that shows which rules would fire
Example:
// Create and populate context
RuleContext<PolicyContextKey> context = new RuleContext<>(PolicyContextKey.class);
context.add(PolicyContextKey.POLICY_ID, "POL-789");
context.add(PolicyContextKey.COVERAGE_AMOUNT, 500000);
context.add(PolicyContextKey.CUSTOMER_AGE, 35);
context.add(PolicyContextKey.CUSTOMER_REGION, "WEST");
// Evaluate which rules would match
List<Rule<PolicyContextKey>> matchingRules = policyRulesOrchestrator.evaluateConditions(context);
// Show preview to user
System.out.println("The following rules would apply:");
for (Rule<PolicyContextKey> rule : matchingRules) {
System.out.println("- " + rule.getName() + ": " + rule.getDescription());
}
Working with RuleExecutionResult
The RuleExecutionResult
class provides access to:
- Matched Rules: The rules that matched and were executed
- Context: The final rule context after execution
- Execution Statistics: Data about the execution process
Checking for Matches
boolean hasMatches();
Optional<Rule<T>> getFirstMatchedRule();
List<Rule<T>> getMatchedRules();
These methods help you determine if any rules matched and which ones.
Accessing the Context
RuleContext<T> getContext();
This gives you access to the context after rule execution, which may have been modified by actions.
Execution Statistics
RuleExecutionStats getStats();
This provides execution statistics:
// Get execution statistics
RuleExecutionStats stats = result.getStats();
System.out.println("Execution time: " + stats.getExecutionTimeMs() + "ms");
System.out.println("Rules evaluated: " + stats.getRulesEvaluated());
System.out.println("Actions executed: " + stats.getActionsExecuted());
Advanced Operations
Rule Filtering
You can filter rules before execution:
// Create a custom rule filter
RuleFilter<OrderContextKey> highPriorityFilter = rule -> rule.getPriority() < 100;
// Apply the filter when executing
RuleExecutionResult<OrderContextKey> result = orderRulesOrchestrator
.withFilter(highPriorityFilter)
.executeAllMatchingRules(context);
Common filter scenarios:
// Filter by tag
RuleFilter<T> tagFilter = rule -> rule.getTags().contains("promotion");
// Filter by name pattern
RuleFilter<T> nameFilter = rule -> rule.getName().startsWith("Discount-");
// Composite filter
RuleFilter<T> compositeFilter = rule ->
rule.getPriority() < 50 && rule.getTags().contains("critical");
Context Preprocessing
You can preprocess the context before rule execution:
// Create a context preprocessor
ContextPreprocessor<OrderContextKey> currencyConverter = context -> {
if (context.hasValue(OrderContextKey.CURRENCY) &&
!context.getRequired(OrderContextKey.CURRENCY, String.class).equals("USD")) {
// Convert currency to USD
String currency = context.getRequired(OrderContextKey.CURRENCY, String.class);
Double amount = context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class);
Double convertedAmount = currencyService.convertToUSD(amount, currency);
// Update context with converted amount
context.add(OrderContextKey.ORDER_AMOUNT, convertedAmount);
context.add(OrderContextKey.CURRENCY, "USD");
context.add(OrderContextKey.ORIGINAL_CURRENCY, currency);
context.add(OrderContextKey.ORIGINAL_AMOUNT, amount);
}
return context;
};
// Apply the preprocessor when executing
RuleExecutionResult<OrderContextKey> result = orderRulesOrchestrator
.withContextPreprocessor(currencyConverter)
.executeAllMatchingRules(context);
Custom Execution Listeners
You can add listeners to track rule execution:
// Create an execution listener
RuleExecutionListener<OrderContextKey> loggingListener = new RuleExecutionListener<>() {
@Override
public void onRuleEvaluationStart(Rule<OrderContextKey> rule, RuleContext<OrderContextKey> context) {
logger.debug("Evaluating rule: {}", rule.getName());
}
@Override
public void onRuleMatched(Rule<OrderContextKey> rule, RuleContext<OrderContextKey> context) {
logger.info("Rule matched: {}", rule.getName());
}
@Override
public void onRuleActionExecuted(Rule<OrderContextKey> rule, RuleContext<OrderContextKey> context) {
logger.info("Rule action executed: {}", rule.getName());
}
@Override
public void onRuleEvaluationEnd(Rule<OrderContextKey> rule, boolean matched, RuleContext<OrderContextKey> context) {
logger.debug("Rule evaluation completed: {} (matched: {})", rule.getName(), matched);
}
};
// Apply the listener when executing
RuleExecutionResult<OrderContextKey> result = orderRulesOrchestrator
.withExecutionListener(loggingListener)
.executeAllMatchingRules(context);
Performance Considerations
Context Reuse
When executing multiple rule sets, reuse the same context to avoid creating new objects:
// Create a single context
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
context.add(OrderContextKey.ORDER_ID, orderId);
// ... add more data
// Execute multiple rule sets with the same context
RuleExecutionResult<OrderContextKey> fraudResult =
fraudDetectionOrchestrator.executeFirstMatchingRuleingRule(context);
if (!fraudResult.hasMatches()) {
RuleExecutionResult<OrderContextKey> discountResult =
discountOrchestrator.executeAllMatchingRules(context);
RuleExecutionResult<OrderContextKey> shippingResult =
shippingOrchestrator.executeAllMatchingRules(context);
}
Parallel Execution
For independent rule sets, consider parallel execution:
// Create executor service
ExecutorService executor = Executors.newFixedThreadPool(3);
// Execute rule sets in parallel
Future<RuleExecutionResult<OrderContextKey>> discountFuture =
executor.submit(() -> discountOrchestrator.executeAllMatchingRules(context.copy()));
Future<RuleExecutionResult<OrderContextKey>> taxFuture =
executor.submit(() -> taxOrchestrator.executeAllMatchingRules(context.copy()));
Future<RuleExecutionResult<OrderContextKey>> shippingFuture =
executor.submit(() -> shippingOrchestrator.executeAllMatchingRules(context.copy()));
// Get results
RuleExecutionResult<OrderContextKey> discountResult = discountFuture.get();
RuleExecutionResult<OrderContextKey> taxResult = taxFuture.get();
RuleExecutionResult<OrderContextKey> shippingResult = shippingFuture.get();
// Merge contexts if needed
context.add(OrderContextKey.ORDER_AMOUNT,
discountResult.getContext().getRequired(OrderContextKey.ORDER_AMOUNT, Double.class));
context.add(OrderContextKey.TAX_AMOUNT,
taxResult.getContext().getRequired(OrderContextKey.TAX_AMOUNT, Double.class));
context.add(OrderContextKey.SHIPPING_AMOUNT,
shippingResult.getContext().getRequired(OrderContextKey.SHIPPING_AMOUNT, Double.class));
// Clean up
executor.shutdown();
Best Practices
-
Choose the Right Execution Method: Select the execution method that matches your business requirements.
-
Error Handling: Always add proper error handling around rule execution:
java
try {
RuleExecutionResult<T> result = orchestrator.executeAllMatchingRules(context);
// Process result
} catch (RuleExecutionException e) {
logger.error("Error executing rules: {}", e.getMessage(), e);
// Implement fallback behavior
}
- Context Management: Be careful with context data, especially when sharing across different rule sets:
java
// Create a safe copy when needed
RuleContext<T> safeCopy = context.copy();
- Performance Monitoring: Track rule execution performance:
java
RuleExecutionStats stats = result.getStats();
if (stats.getExecutionTimeMs() > 100) {
logger.warn("Slow rule execution: {}ms for ruleset {}",
stats.getExecutionTimeMs(), orchestrator.getRuleSetName());
}
- Logging and Auditing: Implement proper logging for rule executions:
```java logger.info("Executed ruleset: {}, matches: {}, execution time: {}ms", orchestrator.getRuleSetName(), result.getMatchedRules().size(), result.getStats().getExecutionTimeMs());
// Log individual matches for audit purposes result.getMatchedRules().forEach(rule -> logger.info("Rule matched: {}, priority: {}", rule.getName(), rule.getPriority())); ```
- Testing: Write comprehensive tests for your orchestrator operations:
```java
@Test
public void testDiscountRules() {
// Setup test context
RuleContext
// Execute rules
RuleExecutionResult<OrderContextKey> result = discountOrchestrator.executeAllMatchingRules(context);
// Verify results
assertTrue(result.hasMatches());
assertEquals(2, result.getMatchedRules().size());
assertEquals(180.0, context.getRequired(OrderContextKey.ORDER_AMOUNT, Double.class), 0.01);
} ```
Integration & Frameworks
Spring Boot Integration (covered in rule-orchestrator-injection.md)
Testing & Best Practices
Testing Rules
Effective testing is essential for ensuring that your Axiom business rules work as expected. This guide covers strategies and techniques for testing rules at different levels, from unit testing individual components to integration testing the entire rule system.
Testing Approach
A comprehensive testing strategy for Axiom rules should include:
- Unit Tests: Testing individual business checks and actions
- Rule Tests: Testing individual rules with mock contexts
- Rule Set Tests: Testing sets of rules together
- Integration Tests: Testing the entire rule system in a realistic environment
Unit Testing Business Checks and Actions
Before testing rules, it's important to test the building blocks: business checks and actions.
Testing Business Checks
Business checks should be tested to ensure they correctly evaluate conditions based on the context:
@Test
void testHighRiskScoreCheck() {
// Create the business check
HasRiskScoreCheck check = new HasRiskScoreCheck();
// Create a context with a high risk score
RuleContext<TestCtxKey> context = new RuleContext<>(TestCtxKey.class);
context.add(TestCtxKey.RISK_SCORE, 95);
// Execute the check with a threshold of 90
Value result = check.execute(context, Value.of(90));
// Verify the result is true (risk score exceeds threshold)
assertThat(result.asBoolean()).isTrue();
// Create another context with a lower risk score
RuleContext<TestCtxKey> lowRiskContext = new RuleContext<>(TestCtxKey.class);
lowRiskContext.add(TestCtxKey.RISK_SCORE, 85);
// Execute the check again
Value lowRiskResult = check.execute(lowRiskContext, Value.of(90));
// Verify the result is false (risk score does not exceed threshold)
assertThat(lowRiskResult.asBoolean()).isFalse();
}
Testing Business Actions
Business actions should be tested to ensure they modify the context or perform external operations correctly:
@Test
void testBlockRequestAction() {
// Create the business action
BlockRequestAction action = new BlockRequestAction();
// Create a context
RuleContext<TestCtxKey> context = new RuleContext<>(TestCtxKey.class);
context.add(TestCtxKey.REQUEST_ID, "REQ-12345");
// Execute the action
Value result = action.execute(context);
// Verify the action succeeded
assertThat(result.asBoolean()).isTrue();
// Verify the context was modified correctly
assertThat(context.get(TestCtxKey.REQUEST_BLOCKED, Boolean.class))
.isPresent()
.hasValue(true);
}
For actions with external dependencies, you might need to use mocks:
@Test
void testNotificationAction() {
// Create mock notification service
NotificationService mockService = mock(NotificationService.class);
// Create the action with the mock service
SendNotificationAction action = new SendNotificationAction(mockService);
// Create a context
RuleContext<TestCtxKey> context = new RuleContext<>(TestCtxKey.class);
context.add(TestCtxKey.CUSTOMER_ID, "CUST-12345");
context.add(TestCtxKey.NOTIFICATION_MESSAGE, "Test message");
// Execute the action
action.execute(context);
// Verify the notification service was called correctly
verify(mockService).sendNotification(
eq("CUST-12345"),
eq("Test message")
);
}
Testing Individual Rules
Once you've tested the individual checks and actions, you can test complete rules:
@Test
void testFraudDetectionRule() {
// Create a business rule
Condition<TestCtxKey> condition = new HasFraudSignalsCondition();
List<RuleFunction<TestCtxKey>> actions = List.of(new BlockRequestAction());
BusinessRule<TestCtxKey> rule = new BusinessRule<>("Fraud Detection Rule", condition, actions);
// Create a context with fraud signals
RuleContext<TestCtxKey> context = new RuleContext<>(TestCtxKey.class);
context.add(TestCtxKey.HAS_FRAUD_SIGNALS, true);
context.add(TestCtxKey.REQUEST_ID, "REQ-12345");
// Evaluate the rule
boolean matched = rule.evaluate(context);
// Verify the rule matched and the action was performed
assertThat(matched).isTrue();
assertThat(context.get(TestCtxKey.REQUEST_BLOCKED, Boolean.class))
.isPresent()
.hasValue(true);
// Test with a context that doesn't match the condition
RuleContext<TestCtxKey> noFraudContext = new RuleContext<>(TestCtxKey.class);
noFraudContext.add(TestCtxKey.HAS_FRAUD_SIGNALS, false);
noFraudContext.add(TestCtxKey.REQUEST_ID, "REQ-67890");
// Evaluate again
boolean noMatch = rule.evaluate(noFraudContext);
// Verify the rule didn't match and no action was performed
assertThat(noMatch).isFalse();
assertThat(noFraudContext.get(TestCtxKey.REQUEST_BLOCKED, Boolean.class))
.isEmpty();
}
Testing Rule Sets
Testing entire rule sets allows you to verify the interaction between multiple rules, especially the priority ordering:
@Test
void testRuleSetPriorityOrder() {
// Create a rule set
RuleSet<TestCtxKey> ruleSet = new RuleSet<>();
// Add rules with different priorities
BusinessRule<TestCtxKey> highPriorityRule = createHighPriorityRule();
BusinessRule<TestCtxKey> lowPriorityRule = createLowPriorityRule();
ruleSet.addRule(highPriorityRule, 10, ZonedDateTime.now());
ruleSet.addRule(lowPriorityRule, 20, ZonedDateTime.now());
// Create an orchestrator
RuleOrchestrator<TestCtxKey> orchestrator = new RuleOrchestrator<>(ruleSet);
// Create a context that matches both rules
RuleContext<TestCtxKey> context = new RuleContext<>(TestCtxKey.class);
context.add(TestCtxKey.MATCHES_HIGH_PRIORITY, true);
context.add(TestCtxKey.MATCHES_LOW_PRIORITY, true);
// Execute first match
RuleExecutionResult<TestCtxKey> result = orchestrator.executeFirstMatchingRule(context);
// Verify the high priority rule matched
assertThat(result.hasMatch()).isTrue();
assertThat(result.getMatchedRule().getName()).isEqualTo("High Priority Rule");
}
You can also test rule sets with effective dates:
@Test
void testRuleSetEffectiveDates() {
// Create a rule set
RuleSet<TestCtxKey> ruleSet = new RuleSet<>();
// Create rules
BusinessRule<TestCtxKey> activeRule = createTestRule("Active Rule");
BusinessRule<TestCtxKey> futureRule = createTestRule("Future Rule");
BusinessRule<TestCtxKey> expiredRule = createTestRule("Expired Rule");
// Add rules with different effective dates
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime yesterday = now.minusDays(1);
ZonedDateTime tomorrow = now.plusDays(1);
ZonedDateTime lastWeek = now.minusWeeks(1);
ZonedDateTime lastMonth = now.minusMonths(1);
ruleSet.addRule(activeRule, 10, lastWeek); // Active from last week
ruleSet.addRule(futureRule, 20, tomorrow); // Active from tomorrow
ruleSet.addRule(expiredRule, 30, lastMonth, yesterday); // Expired yesterday
// Get rules in priority order (should filter out inactive rules)
List<BusinessRule<TestCtxKey>> activeRules = ruleSet.getRulesInPriorityOrder();
// Verify only the active rule is returned
assertThat(activeRules).hasSize(1);
assertThat(activeRules.get(0).getName()).isEqualTo("Active Rule");
}
Integration Testing with Guice
For integration testing with Guice, you can create a test module that configures the Axiom framework:
public class TestAxiomModule extends AxiomModule<TestCtxKey> {
public TestAxiomModule() {
super(TestCtxKey.class);
}
@Override
protected Map<String, RuleSetLoader<TestCtxKey>> getRegisteredLoaders() {
Map<String, RuleSetLoader<TestCtxKey>> loaders = new HashMap<>();
// Add test rule set loaders
loaders.put("test_rules", new YamlRuleSetLoader<>("test_rules.yaml"));
return loaders;
}
@Override
protected void configureBusinessRules(
MapBinder<String, BusinessCheck<TestCtxKey>> checks,
MapBinder<String, BusinessAction<TestCtxKey>> actions) {
// Register test business checks
checks.addBinding("isTestCondition").to(TestConditionCheck.class);
// Register test business actions
actions.addBinding("performTestAction").to(TestAction.class);
}
}
Then you can write integration tests that use the module:
@Test
void testIntegrationWithGuice() {
// Create Guice injector with the test module
Injector injector = Guice.createInjector(new TestAxiomModule());
// Get the rule orchestrator
RuleOrchestrator<TestCtxKey> orchestrator =
injector.getInstance(Key.get(
new TypeLiteral<RuleOrchestrator<TestCtxKey>>() {},
Names.named("test_rules")
));
// Create a test context
RuleContext<TestCtxKey> context = new RuleContext<>(TestCtxKey.class);
context.add(TestCtxKey.TEST_CONDITION, true);
// Execute rules
RuleExecutionResult<TestCtxKey> result = orchestrator.executeFirstMatchingRule(context);
// Verify the rule matched and action was performed
assertThat(result.hasMatch()).isTrue();
assertThat(result.getMatchedRule().getName()).isEqualTo("Test Rule");
assertThat(context.get(TestCtxKey.TEST_ACTION_PERFORMED, Boolean.class))
.isPresent()
.hasValue(true);
}
Testing Real YAML Rule Sets
Testing with actual YAML rule sets allows you to verify the complete rule definition process:
@Test
void testYamlRuleSet() throws IOException {
// Create a temporary YAML file
Path tempFile = Files.createTempFile("test_rules", ".yaml");
Files.write(tempFile, Arrays.asList(
"rulesetName: \"Test Rule Set\"",
"rulesetDescription: \"Rules for testing\"",
"",
"businessChecks:",
" - name: isTestCondition",
" description: A test condition",
"",
"businessActions:",
" - name: performTestAction",
" description: A test action",
"",
"rules:",
" - name: \"Test Rule\"",
" description: \"A rule for testing\"",
" expression: isTestCondition() then performTestAction()",
" priority: 10",
" effectiveFrom: \"2023-01-01T00:00:00Z\""
));
try {
// Create a loader for the YAML file
RuleSetLoader<TestCtxKey> loader =
new YamlRuleSetLoader<>(tempFile.toString());
// Create an orchestrator test helper (see below)
OrchestratorTestHelper<TestCtxKey> helper =
new OrchestratorTestHelper<>(loader);
// Register the required business checks and actions
helper.registerCheck("isTestCondition", new TestConditionCheck());
helper.registerAction("performTestAction", new TestAction());
// Create a test context
RuleContext<TestCtxKey> context = new RuleContext<>(TestCtxKey.class);
context.add(TestCtxKey.TEST_CONDITION, true);
// Execute the rule
RuleExecutionResult<TestCtxKey> result =
helper.getOrchestrator().executeFirstMatchingRule(context);
// Verify the rule matched
assertThat(result.hasMatch()).isTrue();
assertThat(result.getMatchedRule().getName()).isEqualTo("Test Rule");
} finally {
// Clean up
Files.deleteIfExists(tempFile);
}
}
Where OrchestratorTestHelper
is a test utility class:
public class OrchestratorTestHelper<K extends Enum<K>> {
private final RuleSetLoader<K> loader;
private final Map<String, BusinessCheck<K>> checks = new HashMap<>();
private final Map<String, BusinessAction<K>> actions = new HashMap<>();
private RuleOrchestrator<K> orchestrator;
public OrchestratorTestHelper(RuleSetLoader<K> loader) {
this.loader = loader;
}
public void registerCheck(String name, BusinessCheck<K> check) {
checks.put(name, check);
}
public void registerAction(String name, BusinessAction<K> action) {
actions.put(name, action);
}
public RuleOrchestrator<K> getOrchestrator() {
if (orchestrator == null) {
RuleParser<K> parser = new RuleParser<>(checks, actions);
RuleSet<K> ruleSet = loader.loadRuleSet(parser);
orchestrator = new RuleOrchestrator<>(ruleSet);
}
return orchestrator;
}
}
Advanced Testing Techniques
Testing with Mocked Time
For testing rules with effective dates, you can mock the time:
@Test
void testEffectiveDateHandling() {
// Create a rule set
RuleSet<TestCtxKey> ruleSet = new RuleSet<>();
// Add a rule with a future effective date
BusinessRule<TestCtxKey> rule = createTestRule();
ZonedDateTime futureDate = ZonedDateTime.now().plusDays(7);
ruleSet.addRule(rule, 10, futureDate);
// Create an orchestrator
RuleOrchestrator<TestCtxKey> orchestrator = new RuleOrchestrator<>(ruleSet);
// Create a context
RuleContext<TestCtxKey> context = new RuleContext<>(TestCtxKey.class);
// Execute rules - should not match because the rule is not effective yet
RuleExecutionResult<TestCtxKey> result = orchestrator.executeFirstMatchingRule(context);
assertThat(result.hasMatch()).isFalse();
// Now, we need to mock time to be after the effective date
// This would require modifying Axiom's code to use a Clock that can be mocked
// For demonstration purposes, we'll just show the concept
// Assume we've modified RuleSet to use a Clock
Clock mockClock = Clock.fixed(
futureDate.plusDays(1).toInstant(),
ZoneId.systemDefault()
);
// Assuming we can set the clock on the rule set
// ruleSet.setClock(mockClock);
// Execute rules again - now should match
// RuleExecutionResult<TestCtxKey> resultAfterTimeChange = orchestrator.executeFirstMatchingRule(context);
// assertThat(resultAfterTimeChange.hasMatch()).isTrue();
}
Testing Rule Execution Performance
For performance-critical applications, you can measure rule execution time:
@Test
void testRuleExecutionPerformance() {
// Create a rule set with multiple rules
RuleSet<TestCtxKey> ruleSet = createLargeRuleSet(100); // 100 rules
// Create an orchestrator
RuleOrchestrator<TestCtxKey> orchestrator = new RuleOrchestrator<>(ruleSet);
// Create a context
RuleContext<TestCtxKey> context = new RuleContext<>(TestCtxKey.class);
context.add(TestCtxKey.PERFORMANCE_TEST, true);
// Measure execution time
long startTime = System.nanoTime();
orchestrator.executeFirstMatchingRule(context);
long endTime = System.nanoTime();
long durationMs = (endTime - startTime) / 1_000_000; // Convert to milliseconds
// Assert that execution time is within acceptable limits
assertThat(durationMs).isLessThan(100); // Less than 100ms
}
Testing Rule Execution Results
For more complex rule evaluations, you can test the execution results thoroughly:
@Test
void testComplexRuleExecutionResults() {
// Create or load a rule set with multiple rules
RuleSet<TestCtxKey> ruleSet = loadTestRuleSet();
// Create an orchestrator
RuleOrchestrator<TestCtxKey> orchestrator = new RuleOrchestrator<>(ruleSet);
// Create a context that should trigger specific rules
RuleContext<TestCtxKey> context = new RuleContext<>(TestCtxKey.class);
context.add(TestCtxKey.CUSTOMER_TYPE, "PREMIUM");
context.add(TestCtxKey.ORDER_AMOUNT, 5000.0);
// Execute all matching rules
RuleExecutionResult<TestCtxKey> result = orchestrator.executeAllMatchingRules(context);
// Verify multiple aspects of the result
assertThat(result.hasMatches()).isTrue();
assertThat(result.getMatchedRules()).hasSize(3); // Expecting 3 matching rules
// Verify specific rules matched
List<String> matchedRuleNames = result.getMatchedRules().stream()
.map(BusinessRule::getName)
.collect(Collectors.toList());
assertThat(matchedRuleNames).containsExactly(
"Premium Customer Rule",
"High Value Order Rule",
"Premium High Value Rule"
);
// Verify the context was modified as expected
RuleContext<TestCtxKey> resultContext = result.getContext();
// Verify discount was applied
assertThat(resultContext.get(TestCtxKey.DISCOUNT_APPLIED, Boolean.class))
.isPresent()
.hasValue(true);
// Verify the discount amount
assertThat(resultContext.get(TestCtxKey.DISCOUNT_PERCENTAGE, Double.class))
.isPresent()
.hasValue(15.0); // Expecting 15% discount
}
Best Practices for Testing Rules
-
Test Each Component: Test business checks, actions, and rules individually before testing them together.
-
Use Descriptive Test Names: Name your tests clearly to describe what's being tested and the expected outcome.
-
Create Test Utilities: Develop helper classes to simplify rule testing, especially for complex rule sets.
-
Test Edge Cases: Test with extreme values, empty contexts, and other edge cases.
-
Test Rule Interactions: Ensure that rules with overlapping conditions and different priorities work correctly together.
-
Mock External Dependencies: Use mocking frameworks to isolate rule testing from external systems.
-
Test Performance: For large rule sets, test performance to ensure rules evaluate efficiently.
-
Test Effective Dates: Verify that rules are only active during their specified date ranges.
-
Automate Tests: Include rule tests in your CI/CD pipeline to catch regressions early.
-
Document Test Scenarios: Maintain clear documentation of what each test is verifying.
Related Sections
- Rule Overview - Understanding the basics of rules
- Rule Effective Dates - Testing rules with effective dates
- Rule Priority - Testing rule priority ordering
Best Practices for Axiom Rules
This guide covers best practices for developing, testing, and maintaining Axiom business rules in production environments.
Development Best Practices
1. Rule Design Principles
Keep Rules Simple and Focused
- Each rule should have a single, clear purpose
- Avoid complex nested conditions that are hard to understand
- Use descriptive rule names that explain what the rule does
# Good: Clear, focused rule
- name: "Apply Premium Customer Discount"
description: "Apply 10% discount for premium customers with orders over $100"
expression: isPremiumCustomer() and orderAmount() > 100 then applyDiscount(10)
# Avoid: Complex, multi-purpose rule
- name: "Complex Customer Processing"
expression: (isPremiumCustomer() or (isRegularCustomer() and orderAmount() > 200 and hasLoyaltyPoints())) and not hasActivePromotion() then (applyDiscount(5) and updateLoyaltyPoints() and sendNotification())
Use Meaningful Names
- Business checks and actions should have descriptive names
- Context keys should clearly indicate what data they contain
- Rule names should explain business intent, not technical implementation
2. Code Organization
Separate Business Logic by Domain
// Good: Organize by business domain
com.example.rules.customer.checks.IsHighValueCustomerCheck
com.example.rules.customer.actions.ApplyCustomerDiscountAction
com.example.rules.order.checks.HasMinimumOrderAmountCheck
com.example.rules.order.actions.ApplyShippingDiscountAction
Use Consistent Parameter Naming
// Good: Consistent parameter names
@Arg("threshold") Value threshold
@Arg("percentage") Value percentage
@Arg("minAmount") Value minAmount
// Avoid: Inconsistent naming
@Arg("thresh") Value threshold
@Arg("pct") Value percentage
@Arg("minimum_amount") Value minAmount
3. Error Handling
Validate Input Parameters
public Value execute(RuleContext<CustomerContextKey> ctx, @Arg("threshold") Value threshold) {
// Validate parameters
if (threshold == null) {
throw new IllegalArgumentException("Threshold parameter cannot be null");
}
// Validate context data
if (!ctx.has(CustomerContextKey.SPENDING_AMOUNT)) {
throw new IllegalStateException("Required context key SPENDING_AMOUNT is missing");
}
// Business logic here...
}
Handle Edge Cases Gracefully
public Value execute(RuleContext<CustomerContextKey> ctx) {
// Handle potential null or missing data
Optional<LocalDateTime> regDate = ctx.get(CustomerContextKey.REGISTRATION_DATE, LocalDateTime.class);
if (regDate.isEmpty()) {
// Return false for customers without registration date rather than throwing
return Value.of(false);
}
// Continue with business logic...
}
Testing Best Practices
1. Comprehensive Test Coverage
Test All Rule Scenarios
@Test
@DisplayName("Should apply discount for high value customers")
void testHighValueCustomerDiscount() {
// Given: High value customer
RuleContext<CustomerContextKey> context = createContext(
new BigDecimal("2500.00"), // High spending
5, // High loyalty
LocalDateTime.now().minusDays(35)
);
// When: Rules are executed
RuleExecutionResult<CustomerContextKey> result = ruleOrchestrator.executeAllMatchingRules(context);
// Then: Verify expected behavior
assertThat(result.hasMatches()).isTrue();
assertThat(result.hasFailed()).isFalse();
BigDecimal discount = context.getRequired(CustomerContextKey.DISCOUNT_PERCENTAGE, BigDecimal.class);
assertThat(discount).isGreaterThan(BigDecimal.ZERO);
}
Test Edge Cases
@Test
@DisplayName("Should handle new customer scenario gracefully")
void testNewCustomer() {
// Given: Brand new customer with minimal data
RuleContext<CustomerContextKey> context = createContext(
new BigDecimal("50.00"), // Low spending
1, // Low loyalty
LocalDateTime.now() // Just registered
);
// When: Rules are executed
RuleExecutionResult<CustomerContextKey> result = ruleOrchestrator.executeAllMatchingRules(context);
// Then: Should handle gracefully (no rules may match)
if (result.hasFailed()) {
assertThat(result.getFailureReason()).hasValue("No rules matched the context");
} else {
assertThat(result.hasMatches()).isTrue();
}
}
2. Integration Testing
Test End-to-End Scenarios
@DisplayName("Axiom Integration Tests")
class AxiomIntegrationTest {
@Inject
@Named("customer_discount")
private RuleOrchestrator<CustomerContextKey> ruleOrchestrator;
@BeforeEach
void setUp() {
// Initialize Guice injector with production configuration
Injector injector = Guice.createInjector(new ApplicationMainModule());
injector.injectMembers(this);
}
@Test
@DisplayName("Should execute multiple rules in priority order")
void testRulePriorityExecution() {
// Test that rules execute in correct priority order
// and produce expected cumulative results
}
}
3. Performance Testing
Benchmark Rule Execution
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class RulePerformanceBenchmark {
@Benchmark
public RuleExecutionResult<CustomerContextKey> benchmarkHighValueCustomerRules() {
return ruleOrchestrator.executeAllMatchingRules(highValueCustomerContext);
}
@Benchmark
public RuleExecutionResult<CustomerContextKey> benchmarkSimpleCustomerRules() {
return ruleOrchestrator.executeAllMatchingRules(simpleCustomerContext);
}
}
Production Deployment Best Practices
1. Rule Versioning and Deployment
Use Effective Dates for Rule Changes
rules:
- name: "Holiday Discount 2024"
description: "Special holiday discount for December 2024"
expression: orderAmount() > 100 then applyDiscount(15)
effectiveFrom: "2024-12-01T00:00:00Z"
effectiveTo: "2024-12-31T23:59:59Z"
priority: 10
Gradual Rule Rollout
- Deploy new rules with future effective dates
- Test in staging environment first
- Monitor rule execution results after deployment
- Have rollback plan for problematic rules
2. Monitoring and Observability
Log Rule Execution Results
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
public void processOrder(Order order) {
RuleExecutionResult<OrderContextKey> result = orchestrator.executeAllMatchingRules(context);
// Log execution summary
logger.info("Executed {} rules for order {}, {} matched, {} failed",
result.getExecutedRules().size(),
order.getId(),
result.getMatchedRules().size(),
result.hasFailed() ? 1 : 0);
// Log individual rule results for debugging
result.getExecutedRules().forEach((rule, success) ->
logger.debug("Rule '{}' executed with result: {}", rule.getName(), success));
}
}
Monitor Performance Metrics
- Track rule execution times
- Monitor rule match rates
- Alert on execution failures
- Track context data completeness
3. Configuration Management
Externalize Rule Configuration
# application.properties
axiom.rules.customer-discount.file=customer_discount_rules.yaml
axiom.rules.customer-discount.reload-interval=300s
axiom.rules.performance.enable-caching=true
axiom.rules.performance.cache-size=1000
Environment-Specific Rules
# development.yaml
rules:
- name: "Debug Discount"
description: "Special discount for testing"
expression: alwaysTrue() then applyDiscount(50)
priority: 1
# production.yaml
rules:
# Only production-appropriate rules
Security Best Practices
1. Input Validation
Validate All Context Data
public Value execute(RuleContext<CustomerContextKey> ctx, @Arg("amount") Value amount) {
// Validate numeric ranges
BigDecimal amountValue = amount.asNumber();
if (amountValue.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
if (amountValue.compareTo(new BigDecimal("1000000")) > 0) {
throw new IllegalArgumentException("Amount exceeds maximum allowed value");
}
// Continue with business logic...
}
2. Access Control
Limit Rule Modification Access
- Control who can deploy rule changes
- Require code review for rule modifications
- Use audit logging for rule deployments
- Implement approval workflows for production changes
Performance Optimization
1. Rule Design for Performance
Order Rules by Frequency and Cost
rules:
# High frequency, low cost rules first
- name: "Simple Amount Check"
expression: orderAmount() > 10 then flagForReview()
priority: 1
# Lower frequency, higher cost rules later
- name: "Complex Customer Analysis"
expression: isHighRiskCustomer() and hasComplexHistory() then escalateToManager()
priority: 100
Minimize Expensive Operations
// Good: Cache expensive operations
private final Map<String, Boolean> customerCache = new ConcurrentHashMap<>();
public Value execute(RuleContext<CustomerContextKey> ctx) {
String customerId = ctx.getRequired(CustomerContextKey.CUSTOMER_ID, String.class);
// Use cache to avoid repeated expensive calls
Boolean isHighRisk = customerCache.computeIfAbsent(customerId,
id -> expensiveRiskCalculation(id));
return Value.of(isHighRisk);
}
2. Context Optimization
Provide Only Necessary Data
// Good: Minimal context with only required data
RuleContext<OrderContextKey> context = new RuleContext<>(OrderContextKey.class);
context.add(OrderContextKey.ORDER_AMOUNT, order.getAmount());
context.add(OrderContextKey.CUSTOMER_TYPE, customer.getType());
// Avoid: Loading unnecessary data
// context.add(OrderContextKey.FULL_CUSTOMER_HISTORY, loadEntireHistory(customer));
Maintenance Best Practices
1. Documentation
Document Business Rules
- Maintain clear documentation for each rule's business purpose
- Document rule interactions and dependencies
- Keep examples up-to-date with current implementation
- Document known limitations and edge cases
2. Rule Lifecycle Management
Regular Rule Review
- Periodically review rule effectiveness
- Remove obsolete or unused rules
- Update rules when business requirements change
- Monitor rule performance and optimize as needed
Version Control
- Store rule files in version control
- Tag rule releases
- Maintain release notes for rule changes
- Use branching strategy for rule development