Axiom Business Rules Developer Guide

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:

  1. A clear understanding of Axiom business rules concepts
  2. Step-by-step instructions for creating and testing rules
  3. Best practices for rule development
  4. Real-world examples based on actual implementations
  5. 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:

  1. Define your context keys
  2. Create business checks and actions
  3. Create rule set YAML files
  4. Configure the Axiom module
  5. 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:

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:

  1. Configuration: Create an AxiomModule<K> to register rule sets, business checks, and business actions.
  2. Rule Definition: Define rules in YAML files with conditions and actions.
  3. Rule Loading: Use RuleSetLoader<K> to load rule sets from YAML files.
  4. Context Preparation: Create a RuleContext<K> with the necessary data for rule evaluation.
  5. Rule Execution: Use RuleOrchestrator<K> to evaluate and execute rules based on the context.
  6. 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:

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:

  1. Metadata: Rule set name and description
  2. Business Checks: Definitions of the checks that can be used in rule expressions
  3. Business Actions: Definitions of the actions that can be performed when rules match
  4. 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

  1. Organize Rules by Domain: Create separate rule sets for different domains or aspects of your application.

  2. Use Clear Priorities: Assign clear priorities to rules, with lower numbers for more important rules.

  3. Leverage Effective Dates: Use effective date ranges to automatically activate and deactivate rules based on time.

  4. Provide Clear Metadata: Include clear descriptions for your rule set, business checks, business actions, and rules.

  5. Validate Rule Sets: Validate rule sets at load time to ensure they are well-formed and reference valid business checks and actions.

  6. Keep Rule Sets Focused: Each rule set should have a clear, focused purpose. Avoid creating "catch-all" rule sets.

  7. Consider Versioning: If you need to maintain multiple versions of rules, consider using separate rule set files or effective dates.

Related Sections

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

  1. Use Descriptive Names: Give your rule set, checks, actions, and rules clear, descriptive names that indicate their purpose.

  2. Organize by Business Domain: Group rules that relate to the same business domain in the same rule set.

  3. Keep Rule Sets Focused: Each rule set should have a single responsibility or domain focus.

  4. Document Thoroughly: Use the description fields to thoroughly document the purpose and behavior of each component.

  5. Use Consistent Priority Schemes: Establish a consistent approach to rule priorities. For example:

  6. Use priority bands (e.g., 1-10 for critical rules, 11-20 for important rules, etc.)
  7. Leave gaps between priorities to allow for future insertions (e.g., 10, 20, 30, etc.)

  8. Leverage Effective Dates: Use effective dates to manage rule lifecycle, particularly for time-limited promotions or policy changes.

  9. Use Tags for Organization: Apply consistent tags to help categorize and filter rules, especially in large rule sets.

  10. Version Control: Keep rule set files in version control along with your application code.

  11. 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:

  1. Generate Java classes for all business checks and actions in your rule set YAML files
  2. Place them in appropriate packages based on your configuration
  3. 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:

  1. Business Check Classes - Placed in the checks package under your base package
  2. 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:

  1. com.example.rules.checks.IsHighValueCustomerCheck.java
  2. com.example.rules.checks.HasLoyaltyStatusCheck.java
  3. 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:

  1. Syntax validation: Checks the YAML structure and syntax
  2. Reference validation: Verifies references to business checks and actions
  3. Expression validation: Validates rule expressions syntax and parameters
  4. 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

  1. Validate Early: Validate rule sets during application startup to fail fast
  2. Detailed Logging: Log detailed validation errors to help identify issues
  3. Testing: Create tests that verify your rule sets pass validation
  4. CI/CD Integration: Add validation to your CI/CD pipeline to catch issues before deployment
  5. 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:

  1. Name: A unique identifier for the rule
  2. Description: A human-readable explanation of the rule's purpose
  3. Expression: A condition and action pair that defines when and what to execute
  4. Priority: A numeric value determining evaluation order (lower values = higher priority)
  5. 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:

  1. Simple checks: hasRiskScore(90)
  2. Negated checks: not hasFraudSignals()
  3. Compound expressions:
  4. AND: isHighValueOrder(1000) and isNewCustomer()
  5. OR: hasFraudSignals() or hasRiskScore(95)
  6. Complex expressions with parentheses:
  7. 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:

  1. Rules are filtered by their effective dates, keeping only currently active rules
  2. Remaining rules are sorted by priority (lower number = higher priority)
  3. 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. For executeAllMatchingRules(), 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

  1. 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.

  2. Condition Complexity: Complex conditions with many subconditions can impact performance. Consider breaking complex rules into multiple simpler rules when possible.

  3. 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.

  4. Context Size: Large contexts with many values can impact serialization/deserialization performance. Include only necessary data in the context.

Related Sections

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:

  1. Rules with the lowest priority numbers are evaluated first
  2. 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

  1. Lower Numbers for Higher Priority: Always remember that lower numbers indicate higher priority in Axiom.

  2. Establish a Priority System: Define a clear system for assigning priorities (like the ranges shown above) and document it.

  3. 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.

  4. Prioritize Security & Compliance: Always give security and compliance rules higher priority than business or operational rules.

  5. Be Cautious with Priority 1: Reserve the absolute highest priorities (1, 2, etc.) for truly critical rules that must trump all others.

  6. Document Priority Decisions: When assigning priorities, document the reasoning to help future maintainers understand why certain priorities were chosen.

  7. Review Priority Order Regularly: As rule sets grow, review the priority order regularly to ensure it still makes logical sense.

Related Sections

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:

  1. Effect From Date: The timestamp when the rule becomes active
  2. 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

  1. Use UTC Times: Always use UTC (Zulu) time zone for effective dates to avoid daylight saving time issues.

  2. 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.

  3. Plan for Transitions: When replacing an existing rule with a new version, consider an overlap period to ensure smooth transitions.

  4. 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.

  5. Test with Different Time Points: When testing rules with effective dates, test with times before, during, and after the effective period.

  6. Document Effective Dates: Include clear documentation about why particular effective dates were chosen, especially for regulatory or business-critical rules.

  7. Audit Effective Date Changes: Keep an audit trail of changes to effective dates, especially for compliance-related rules.

Related Sections

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:

  1. Compile-time validation of key names
  2. Clear documentation of available keys through the enum definition
  3. 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

  1. Define Clear Context Keys: Use descriptive names for your enum values and include comments to document their purpose and expected types.

  2. Keep Context Focused: Include only the data relevant to your rules to avoid cluttering the context.

  3. Handle Missing Values Gracefully: Use the get method when a value might not exist, and getRequired only when you're certain a value should be present.

  4. Type Safety: Always specify the expected type when retrieving values to ensure type safety.

  5. 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 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:

  1. get: Returns an Optional containing the value if present
  2. getRequired: 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

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:

  1. The @RuleMetadata annotation provides metadata about the check, including its name and description.
  2. The execute method takes a RuleContext and optional arguments, and returns a Value object.
  3. The @Arg annotation is used to name the arguments, which helps with validation and documentation.
  4. 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:

  1. The @RuleMetadata annotation provides metadata about the action, including its name and description.
  2. The execute method takes a RuleContext and optional arguments, and returns a Value object.
  3. The implementation modifies the context by adding a value indicating the request is blocked.
  4. The implementation may perform additional operations, such as logging.
  5. 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

  1. Keep Checks and Actions Focused: Each check or action should have a single, well-defined responsibility.

  2. Use Clear Names: Choose descriptive names for your checks and actions that clearly indicate their purpose.

  3. Provide Detailed Descriptions: Use the description field in the @RuleMetadata annotation to provide clear documentation.

  4. Validate Inputs: Always validate context values and arguments to ensure they are of the expected type.

  5. Handle Errors Gracefully: Catch and handle exceptions appropriately to prevent rule execution from failing unexpectedly.

  6. Document Expected Context Keys: Clearly document which context keys your checks and actions expect to be present.

  7. Return Meaningful Values: Ensure that your checks and actions return values that accurately reflect their execution status.

Related Sections

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:

  1. @RuleMetadata Annotation: Provides metadata about the check:
  2. name: The identifier used in rule expressions (must match the name in YAML)
  3. description: A description of what the check does

  4. Interface Implementation: The class must implement BusinessCheck<T> where T is your context key enum.

  5. execute Method: The core method that implements the check logic:

  6. context: Contains all data needed for the check
  7. @Arg parameters: Values provided when the check is called in a rule expression
  8. 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:

  1. @RuleMetadata Annotation: Similar to checks, provides metadata:
  2. name: The identifier used in rule expressions
  3. description: A description of what the action does

  4. Interface Implementation: The class must implement BusinessAction<T>.

  5. execute Method: Implements the action logic:

  6. Can have multiple @Arg parameters
  7. Typically modifies the context
  8. 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:

  1. Descriptive Names: Use clear, business-oriented names for your components.

  2. Thorough Descriptions: The description field should explain what the component does in business terms.

  3. Javadoc Documentation: Add detailed Javadoc to your classes explaining:

  4. Purpose and behavior
  5. Parameter details and valid values
  6. Return value meaning
  7. Context keys used and modified
  8. 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

  1. Single Responsibility: Each component should do one thing well.

  2. Immutability: Make your components immutable for thread safety.

  3. Error Handling: Validate inputs and handle errors gracefully.

  4. Testability: Design components to be easily testable in isolation.

  5. Performance: Be mindful of performance, especially for frequently used checks.

  6. Reusability: Design components to be reusable across different rule sets.

  7. Consistency: Follow a consistent naming convention for components.

  8. 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:

  1. Create unit tests for individual checks and actions
  2. Create integration tests for complete rule executions
  3. Test edge cases and boundary conditions
  4. 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

  1. Keep Checks Lightweight: Checks are evaluated first and potentially more often than actions.

  2. Cache Expensive Operations: For checks that need expensive operations:

```java public Value execute(RuleContext context, ...) { // Check if we've already calculated this result Optional cachedResult = context.getOptional( MyContextKey.CACHED_EXPENSIVE_CHECK_RESULT, Boolean.class );

   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);

} ```

  1. Optimize Order of Checks: In complex expressions, put faster/more likely to fail checks first:

isFastCheck() AND isExpensiveCheck() then doAction()

  1. 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:

  1. Identifying the component in rule expressions
  2. Validating rule expressions
  3. 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:

  1. Documentation and code readability
  2. Validation of rule expressions
  3. 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:

  1. Dependency Injection: Use constructor injection for dependencies to enable easy mocking in tests.

  2. Pure Functions: When possible, make your components pure functions that don't have side effects beyond the context.

  3. Clear Responsibilities: Keep components focused on a single responsibility for easier testing.

  4. 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

  1. Single Responsibility: Each business component should focus on a single responsibility.

  2. Clear Naming: Choose clear, descriptive names for your components that indicate their purpose.

  3. Comprehensive Documentation: Provide detailed descriptions in @RuleMetadata for self-documenting code.

  4. Argument Validation: Validate all arguments to ensure they are of the expected type and value range.

  5. Context Safety: Be careful when modifying the context; avoid removing or overwriting values unexpectedly.

  6. Error Handling: Implement robust error handling to prevent rule execution failures.

  7. Thread Safety: Ensure your components are thread-safe, especially if they maintain state.

  8. Performance Consciousness: Be mindful of performance, especially for components that interact with external services.

  9. Consistent Return Values: Always return Value objects consistently; use Value.of(true) to indicate success.

  10. Test Coverage: Write comprehensive tests for your business components to ensure they behave as expected.

Related Sections

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:

  1. Robustness: Components can handle unexpected inputs without crashing
  2. Correctness: Business logic operates on valid data only
  3. Clear Feedback: Users receive meaningful error messages when something goes wrong
  4. Security: Input validation helps prevent security vulnerabilities
  5. 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:

  1. Rule Execution: Evaluates rule conditions and executes rule actions
  2. Rule Selection: Determines which rules match a given context
  3. Result Handling: Provides detailed results of rule execution
  4. 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

  1. Use Named Orchestrators: When working with multiple rule sets, use named orchestrators to clearly identify which rule set is being used.

  2. Handle No Matches: Always check if a rule matched before accessing the matched rule to avoid NoSuchElementException.

  3. Consider Context Modifications: Remember that rule actions can modify the context, so the context after rule execution may be different from the input context.

  4. Choose the Right Execution Strategy: Use executeFirstMatchingRule when you want only one rule to apply, and executeAllMatchingRules when multiple rules should apply.

  5. 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

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:

  1. Configure the Axiom module with your rule sets
  2. Inject the appropriate rule orchestrators into your application components
  3. 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

  1. Name Consistently: Use consistent naming for your rule sets and orchestrators.

  2. Single Responsibility: Each orchestrator should handle a specific domain or function.

  3. Reuse Contexts: When using multiple orchestrators in sequence, reuse the same context to accumulate results.

  4. Error Handling: Add robust error handling around rule execution.

  5. Testing: Mock orchestrators in unit tests to isolate service logic.

  6. Documentation: Document the purpose and expected behavior of each orchestrator.

  7. Performance: Be mindful of initialization costs, especially with many rule sets.

  8. 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:

  1. Managing a specific rule set
  2. Executing rules against a context
  3. Applying rule priority and effective date filtering
  4. 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:

  1. Matched Rules: The rules that matched and were executed
  2. Context: The final rule context after execution
  3. 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

  1. Choose the Right Execution Method: Select the execution method that matches your business requirements.

  2. 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 }

  1. 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();

  1. 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()); }

  1. 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())); ```

  1. Testing: Write comprehensive tests for your orchestrator operations:

```java @Test public void testDiscountRules() { // Setup test context RuleContext context = new RuleContext<>(OrderContextKey.class); context.add(OrderContextKey.CUSTOMER_TYPE, "PREMIUM"); context.add(OrderContextKey.ORDER_AMOUNT, 200.0);

   // 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:

  1. Unit Tests: Testing individual business checks and actions
  2. Rule Tests: Testing individual rules with mock contexts
  3. Rule Set Tests: Testing sets of rules together
  4. 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

  1. Test Each Component: Test business checks, actions, and rules individually before testing them together.

  2. Use Descriptive Test Names: Name your tests clearly to describe what's being tested and the expected outcome.

  3. Create Test Utilities: Develop helper classes to simplify rule testing, especially for complex rule sets.

  4. Test Edge Cases: Test with extreme values, empty contexts, and other edge cases.

  5. Test Rule Interactions: Ensure that rules with overlapping conditions and different priorities work correctly together.

  6. Mock External Dependencies: Use mocking frameworks to isolate rule testing from external systems.

  7. Test Performance: For large rule sets, test performance to ensure rules evaluate efficiently.

  8. Test Effective Dates: Verify that rules are only active during their specified date ranges.

  9. Automate Tests: Include rule tests in your CI/CD pipeline to catch regressions early.

  10. Document Test Scenarios: Maintain clear documentation of what each test is verifying.

Related Sections

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