Skip to main content

Rule Evaluation Engine Design and Implementation

Comprehensive low-level design for a scalable rule evaluation engine supporting complex business logic with performance optimization and extensibility.

A rule evaluation engine is a core component for implementing complex business logic in enterprise systems, requiring careful design considerations for performance, scalability, and maintainability in high-throughput environments.

1. Problem Statement & Requirements

Clarifying questions

  1. What is the scale of rules and data volume expected?
    • Are we talking about hundreds/thousands of rules or millions? What's the expected data volume per rule evaluation (e.g., 100s of fields vs. millions)?
  2. What consistency requirements exist for rule evaluation?
    • Should evaluations be strongly consistent or eventually consistent? Is there a requirement for real-time evaluation vs. batch processing?
  3. What are the key entities and their relationships?
    • Are rules evaluated against a single data object or multiple entities (e.g., user + account + transaction)? How do rules reference each other?
  4. What is the read/write ratio for rule data?
    • Is it more common to query existing rules or create/update them? How often are rules evaluated?
  5. What latency sensitivity is expected for rule evaluations?
    • Is sub-100ms required for all evaluations, or is batch processing acceptable?
  6. What authentication and authorization model is in place?
    • Is rule creation/modification restricted to admin users or can any user create rules?
  7. Are there external systems that integrate with this engine?
    • Do we need to support rule evaluation over HTTP or via message queue, and how do we handle external API calls in rules?

Functional requirements

  • Rule Creation & Management:
    • Users can create, update, and delete rules with a DSL or UI.
  • Rule Evaluation:
    • Evaluate a rule against input data and return a boolean result or action.
  • Rule Versioning:
    • Support for versioned rules to prevent breaking changes.
  • Rule Dependencies:
    • Rules can reference other rules or data sources (e.g., external APIs, databases).
  • Rule Grouping:
    • Rules can be grouped and evaluated as a unit.
  • Audit Trail:
    • Track who created/modified rules and when.

Non-functional requirements

  • Latency Targets:
    • p50: <50ms
    • p95: <100ms
    • p99: <200ms
  • Throughput:
    • 10,000 rule evaluations per second (peak) with 500 concurrent rule engines.
  • Availability:
    • 99.9% uptime (5-minute SLA) with graceful degradation.
  • Scalability:
    • Horizontally scalable to support millions of rules and evaluations.

Constraints and assumptions

  • Assumptions:
    • Rule DSL is defined and stable.
    • Rules are evaluated in a sandboxed environment to prevent arbitrary code execution.
    • Rule engine is used internally within the organization, not exposed publicly.
  • Constraints:
    • Rules must be evaluated in under 200ms to meet SLA.
    • Rule engine will not support complex queries or joins in rule DSL.

Out of scope

  • External API integrations:
    • Rule evaluation will not support calling external services or APIs directly.
  • User interface:
    • UI for rule creation/editing is out of scope; only backend API is required.
  • Rule compilation to bytecode:
    • Rules will be interpreted at runtime, not compiled.
  • Machine learning models:
    • Rule engine does not support ML model inference or training.
  • Cross-service rule sharing:
    • Rules are isolated to this service and not shared across microservices.

2. API Design

API Style Selection

For a rule evaluation engine, I'll select gRPC as the primary API style with JSON over HTTP as a fallback for client compatibility. The decision is based on:

  • Performance: gRPC's binary protocol and HTTP/2 support provide better throughput and latency for rule evaluation workloads
  • Strong typing: Protobuf definitions ensure type safety between client and server
  • Streaming support: gRPC supports streaming for real-time rule evaluation scenarios
  • Efficient serialization: Binary format reduces payload size compared to JSON
  • Service-first design: gRPC's service-oriented approach aligns well with rule engine's modular nature

Endpoint Definitions

MethodPathDescriptionAuth
CreateRulePOST /v1/rulesCreates a new rule with conditions and actionsJWT
GetRuleGET /v1/rules/{ruleId}Retrieves a specific rule by IDJWT
UpdateRulePUT /v1/rules/{ruleId}Updates an existing ruleJWT
DeleteRuleDELETE /v1/rules/{ruleId}Deletes a rule by IDJWT
EvaluateRulePOST /v1/rules/{ruleId}/evaluateEvaluates a rule against input dataJWT
EvaluateBatchPOST /v1/rules/evaluate/batchEvaluates multiple rules in batch modeJWT
ListRulesGET /v1/rulesLists all rules with paginationJWT
ValidateRulePOST /v1/rules/validateValidates rule syntax and structureJWT

Request and Response Schemas

CreateRule Request

{
  "name": "CustomerTierEligibility",
  "description": "Determines customer tier based on purchase history",
  "conditions": [
    {
      "field": "totalPurchases",
      "operator": "gte",
      "value": 1000
    }
  ],
  "actions": [
    {
      "type": "set",
      "field": "tier",
      "value": "VIP"
    }
  ],
  "enabled": true,
  "priority": 1
}

CreateRule Response

{
  "ruleId": "uuid-12345",
  "name": "CustomerTierEligibility",
  "description": "Determines customer tier based on purchase history",
  "conditions": [
    {
      "field": "totalPurchases",
      "operator": "gte",
      "value": 1000
    }
  ],
  "actions": [
    {
      "type": "set",
      "field": "tier",
      "value": "VIP"
    }
  ],
  "enabled": true,
  "priority": 1,
  "createdAt": "2023-10-01T10:00:00Z",
  "updatedAt": "2023-10-01T10:00:00Z"
}

EvaluateRule Request

{
  "data": {
    "totalPurchases": 1500,
    "customerAge": 35
  },
  "context": {
    "userId": "user-123",
    "timestamp": "2023-10-01T10:05:00Z"
  }
}

EvaluateRule Response

{
  "ruleId": "uuid-12345",
  "evaluatedAt": "2023-10-01T10:05:00Z",
  "result": {
    "matched": true,
    "actions": [
      {
        "type": "set",
        "field": "tier",
        "value": "VIP"
      }
    ],
    "metadata": {
      "conditionMatched": "totalPurchases >= 1000"
    }
  },
  "metrics": {
    "evaluationTimeMs": 2,
    "conditionsEvaluated": 1
  }
}

HTTP Status Codes and Error Response Format

Status CodeDescription
200Success - GET, PUT, DELETE
201Created - POST
400Bad Request - Invalid input
401Unauthorized - Missing or invalid auth
403Forbidden - Insufficient permissions
404Not Found - Resource not found
422Unprocessable Entity - Validation errors
429Too Many Requests - Rate limiting
500Internal Server Error - Unexpected error

Standardized error response:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Rule name is required",
    "details": [
      {
        "field": "name",
        "message": "Rule name is required"
      }
    ],
    "timestamp": "2023-10-01T10:00:00Z"
  }
}

API Versioning Strategy

Versioning will be implemented using path-based versioning (/v1/rules) with:

  • Major version in URL path for breaking changes
  • Minor version in headers or query parameters for non-breaking changes
  • Backward compatibility maintained for at least 2 major versions

Pagination Design

Cursor-based pagination will be used with:

  • cursor parameter for continuation
  • limit parameter (default: 20, max: 100)
  • sort parameter for ordering (by creation date, priority)

Authentication and Authorization

All endpoints require JWT authentication with scopes:

  • rule:read - Read access to rules
  • rule:write - Write access to rules
  • rule:evaluate - Evaluate rule capability

Rate Limiting

Rate limiting per endpoint:

  • CreateRule: 100 requests/minute
  • EvaluateRule: 500 requests/minute (per user)
  • ListRules: 200 requests/minute
  • ValidateRule: 1000 requests/minute

Implementation strategy:

  • Token bucket algorithm per user
  • Distributed rate limiting using Redis
  • Client-side retry with exponential backoff

Idempotency Design

For mutation endpoints (POST /v1/rules, PUT /v1/rules/{ruleId}):

  • Idempotency key in header: X-Idempotency-Key
  • Key format: {user-id}:{operation-type}:{unique-suffix}
  • Server stores last successful operation result for key
  • Duplicate requests return same result with 200 status

gRPC Service Definition (Protobuf)

syntax = "proto3";

package ruleengine;

service RuleEngineService {
  rpc CreateRule(CreateRuleRequest) returns (CreateRuleResponse);
  rpc GetRule(GetRuleRequest) returns (GetRuleResponse);
  rpc UpdateRule(UpdateRuleRequest) returns (UpdateRuleResponse);
  rpc DeleteRule(DeleteRuleRequest) returns (DeleteRuleResponse);
  rpc EvaluateRule(EvaluateRuleRequest) returns (EvaluateRuleResponse);
  rpc EvaluateBatch(EvaluateBatchRequest) returns (stream EvaluateBatchResponse);
  rpc ListRules(ListRulesRequest) returns (ListRulesResponse);
  rpc ValidateRule(ValidateRuleRequest) returns (ValidateRuleResponse);
}

message Rule {
  string rule_id = 1;
  string name = 2;
  string description = 3;
  repeated Condition conditions = 4;
  repeated Action actions = 5;
  bool enabled = 6;
  int32 priority = 7;
  google.protobuf.Timestamp created_at = 8;
  google.protobuf.Timestamp updated_at = 9;
}

message Condition {
  string field = 1;
  Operator operator = 2;
  oneof value {
    string string_value = 3;
    int64 integer_value = 4;
    double double_value = 5;
    bool boolean_value = 6;
    google.protobuf.Timestamp timestamp_value = 7;
  }
}

message Action {
  ActionType type = 1;
  string field = 2;
  oneof value {
    string string_value = 3;
    int64 integer_value = 4;
    double double_value = 5;
    bool boolean_value = 6;
    google.protobuf.Timestamp timestamp_value = 7;
  }
}

message EvaluateRuleRequest {
  string rule_id = 1;
  map<string, Value> data = 2;
  map<string, string> context = 3;
}

message EvaluateRuleResponse {
  string rule_id = 1;
  google.protobuf.Timestamp evaluated_at = 2;
  EvaluationResult result = 3;
  EvaluationMetrics metrics = 4;
}

message EvaluationResult {
  bool matched = 1;
  repeated Action actions = 2;
  map<string, string> metadata = 3;
}

message EvaluationMetrics {
  int64 evaluation_time_ms = 1;
  int32 conditions_evaluated = 2;
}

3. Data Model Design

Entity-Relationship Diagram

erDiagram
    RULE {
        string id PK
        string name
        string description
        string condition
        string action
        string status
        timestamp created_at
        timestamp updated_at
        string created_by
        string updated_by
    }

    RULE_SET {
        string id PK
        string name
        string description
        timestamp created_at
        timestamp updated_at
        string created_by
    }

    RULE_SET_RULE {
        string rule_set_id PK, FK
        string rule_id PK, FK
        integer priority
        timestamp created_at
    }

    EVALUATION_CONTEXT {
        string id PK
        json data
        timestamp created_at
    }

    EVALUATION_RESULT {
        string id PK
        string rule_id FK
        string context_id FK
        boolean matched
        json result_data
        timestamp created_at
    }

    RULE ||--o{ RULE_SET_RULE : "contains"
    RULE_SET ||--o{ RULE_SET_RULE : "has"
    RULE_SET_RULE ||--o{ EVALUATION_RESULT : "triggers"
    RULE ||--o{ EVALUATION_RESULT : "evaluates"
    EVALUATION_CONTEXT ||--o{ EVALUATION_RESULT : "used_in"

Table / Collection Schema Definitions

Rules Table

CREATE TABLE rules (
    id VARCHAR(36) PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    condition TEXT NOT NULL, -- JSON or DSL string
    action TEXT NOT NULL, -- JSON or DSL string
    status VARCHAR(20) DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE', 'DELETED')),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    created_by VARCHAR(255),
    updated_by VARCHAR(255),
    
    INDEX idx_status (status),
    INDEX idx_created_at (created_at)
);

Rule Sets Table

CREATE TABLE rule_sets (
    id VARCHAR(36) PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    created_by VARCHAR(255),
    
    INDEX idx_created_at (created_at)
);

Rule Set Rules Junction Table

CREATE TABLE rule_set_rules (
    rule_set_id VARCHAR(36) NOT NULL,
    rule_id VARCHAR(36) NOT NULL,
    priority INTEGER NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    PRIMARY KEY (rule_set_id, rule_id),
    FOREIGN KEY (rule_set_id) REFERENCES rule_sets(id) ON DELETE CASCADE,
    FOREIGN KEY (rule_id) REFERENCES rules(id) ON DELETE CASCADE,
    INDEX idx_priority (priority),
    INDEX idx_rule_set_id (rule_set_id)
);

Evaluation Context Table

CREATE TABLE evaluation_contexts (
    id VARCHAR(36) PRIMARY KEY,
    data JSON NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_created_at (created_at)
);

Evaluation Results Table

CREATE TABLE evaluation_results (
    id VARCHAR(36) PRIMARY KEY,
    rule_id VARCHAR(36) NOT NULL,
    context_id VARCHAR(36) NOT NULL,
    matched BOOLEAN NOT NULL,
    result_data JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (rule_id) REFERENCES rules(id) ON DELETE CASCADE,
    FOREIGN KEY (context_id) REFERENCES evaluation_contexts(id) ON DELETE CASCADE,
    INDEX idx_rule_id (rule_id),
    INDEX idx_context_id (context_id),
    INDEX idx_matched (matched),
    INDEX idx_created_at (created_at)
);

Primary Key Strategy

UUID v4 is selected as the primary key strategy for all entities:

  • Justification:
    • Globally unique identifiers prevent conflicts in distributed systems
    • No dependency on database sequence generation for horizontal scaling
    • Better support for data replication and sharding scenarios
    • Avoids potential performance bottlenecks in high-concurrency environments

Index Design

Index NameFieldsTypePurpose
idx_statusstatusStandardFast filtering by rule status
idx_created_atcreated_atStandardEfficient time-based queries
idx_prioritypriorityStandardFast rule set ordering
idx_rule_idrule_idStandardFast lookup of evaluation results by rule
idx_context_idcontext_idStandardFast lookup of evaluation results by context
idx_matchedmatchedStandardEfficient filtering by match status

Data Partitioning / Sharding Strategy

Sharding by Rule Set ID with Time-based Bucketing:

  • Partition Key: rule_set_id (hash-based sharding)
  • Time-based Bucketing:
    • Partition by created_at field in 30-day buckets
    • Allows for time-based data lifecycle management and analytics

Rationale:

  • Rule sets are the natural unit for partitioning since they group related rules
  • Time-based buckets enable efficient archiving and analytics of older evaluations
  • Hash sharding ensures even distribution across shards

Soft Delete vs. Hard Delete Decision

Soft Delete is selected:

  • Justification:
    • Audit trail requirement for compliance and debugging
    • Ability to restore rules in case of accidental deletion
    • Historical analysis of rule performance and impact
    • Support for versioning and rollback scenarios

Audit Trail Design

All entities include:

  • created_at / updated_at timestamps
  • created_by / updated_by user identifiers

Data Archival Strategy and Retention Policy

Archival Strategy:

  • Retention Period: 1 year for evaluation results
  • Archive Location: Separate read-only database or data lake (e.g., S3)
  • Archival Trigger: Scheduled job running daily to move old records

Implementation:

-- Example archival query
INSERT INTO rule_evaluation_results_archive 
SELECT * FROM rule_evaluation_results 
WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);

Archive Cleanup:

  • Delete archived records after 5 years for compliance
  • Maintain metadata in primary database for reference

4. Class Design & Service Layer

classDiagram
    class RuleEngineService {
        +evaluateRule(Rule, Context) Result
        +createRule(RuleCreateRequest) Rule
        +updateRule(RuleUpdateRequest) Rule
        +deleteRule(String) void
        +validateRule(Rule) ValidationResult
    }
    
    class Rule {
        -id: String
        -name: String
        -description: String
        -condition: String
        -actions: List~Action~
        -status: RuleStatus
        -createdAt: Instant
        -updatedAt: Instant
        +evaluate(Context) Result
    }
    
    class Context {
        -data: Map~String, Object~
        -metadata: Map~String, String~
    }
    
    class Result {
        -success: boolean
        -message: String
        -payload: Map~String, Object~
    }
    
    class Action {
        -type: ActionType
        -config: Map~String, Object~
    }
    
    class RuleValidator {
        +validate(Rule) ValidationResult
    }
    
    class ExpressionEvaluator {
        +evaluate(String, Context) Object
    }
    
    class ActionExecutor {
        +execute(Action, Context) void
    }
    
    class RuleRepository {
        +save(Rule) Rule
        +findById(String) Optional~Rule~
        +findAll() List~Rule~
        +deleteById(String) void
    }
    
    class RuleEngineConfig {
        -maxEvaluationTimeMs: int
        -defaultTimeoutMs: int
        -allowedFunctions: Set~String~
    }
    
    class ValidationResult {
        -valid: boolean
        -errors: List~String~
    }
    
    RuleEngineService --> Rule : uses
    RuleEngineService --> RuleRepository : persists
    RuleEngineService --> RuleValidator : validates
    RuleEngineService --> ExpressionEvaluator : evaluates
    RuleEngineService --> ActionExecutor : executes
    
    Rule --> Context : uses
    Rule --> Action : has
    Rule --> ValidationResult : validates
    
    ExpressionEvaluator --> Context : reads
    ActionExecutor --> Context : uses
    
    RuleValidator --> Rule : validates
    RuleValidator --> ValidationResult : returns
    
    RuleRepository --> Rule : stores
stateDiagram-v2
    [*] --> DRAFT
    DRAFT --> ACTIVE : activate()
    ACTIVE --> INACTIVE : deactivate()
    INACTIVE --> ACTIVE : activate()
    ACTIVE --> ARCHIVED : archive()
    ARCHIVED --> [*]

Service Layer Overview

The Rule Engine service layer implements a robust evaluation system with clear separation of concerns:

  1. RuleEngineService - Main entry point for rule operations
  2. RuleValidator - Validates rule syntax and semantics
  3. ExpressionEvaluator - Evaluates condition expressions
  4. ActionExecutor - Executes actions based on evaluation results

Business Logic Patterns

Strategy Pattern for Action Execution

public interface ActionExecutor {
    void execute(Action action, Context context);
}

@Component
public class EmailActionExecutor implements ActionExecutor {
    private final EmailService emailService;
    
    @Override
    public void execute(Action action, Context context) {
        String template = (String) action.getConfig().get("template");
        String recipient = (String) action.getConfig().get("recipient");
        
        // Evaluate template with context data
        String content = TemplateEngine.process(template, context.getData());
        
        emailService.send(recipient, content);
    }
}

@Component
public class LogActionExecutor implements ActionExecutor {
    private final Logger logger = LoggerFactory.getLogger(LogActionExecutor.class);
    
    @Override
    public void execute(Action action, Context context) {
        String message = (String) action.getConfig().get("message");
        logger.info("Action triggered: {}", message);
    }
}

@Component
public class RuleEngineService {
    private final Map<ActionType, ActionExecutor> executors;
    
    public RuleEngineService(List<ActionExecutor> executorList) {
        this.executors = executorList.stream()
            .collect(Collectors.toMap(
                e -> e.getClass().getAnnotation(ActionType.class).value(),
                Function.identity()
            ));
    }
    
    public void executeAction(Action action, Context context) {
        ActionExecutor executor = executors.get(action.getType());
        if (executor != null) {
            executor.execute(action, context);
        }
    }
}

Critical Path Implementation: evaluateRule()

@Transactional
public Result evaluateRule(Rule rule, Context context) {
    try {
        // 1. Validate rule state
        if (rule.getStatus() != RuleStatus.ACTIVE) {
            return Result.failure("Rule is not active");
        }
        
        // 2. Evaluate condition with timeout protection
        Object evaluationResult = evaluateConditionWithTimeout(
            rule.getCondition(), 
            context, 
            config.getMaxEvaluationTimeMs()
        );
        
        // 3. Check if condition is met
        boolean conditionMet = Boolean.TRUE.equals(evaluationResult);
        
        // 4. If condition met, execute actions
        if (conditionMet) {
            List<CompletableFuture<Void>> actionFutures = new ArrayList<>();
            
            for (Action action : rule.getActions()) {
                CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                    try {
                        actionExecutor.execute(action, context);
                    } catch (Exception e) {
                        // Log error but don't fail entire evaluation
                        logger.error("Failed to execute action: {}", action.getType(), e);
                    }
                });
                
                actionFutures.add(future);
            }
            
            // Wait for all actions to complete (or timeout)
            CompletableFuture.allOf(actionFutures.toArray(new CompletableFuture[0]))
                .get(config.getDefaultTimeoutMs(), TimeUnit.MILLISECONDS);
        }
        
        return Result.success(
            "Rule evaluation completed", 
            Map.of("conditionMet", conditionMet)
        );
        
    } catch (TimeoutException e) {
        return Result.failure("Rule evaluation timed out");
    } catch (Exception e) {
        logger.error("Error evaluating rule: {}", rule.getId(), e);
        return Result.failure("Internal error during rule evaluation");
    }
}

private Object evaluateConditionWithTimeout(String condition, Context context, int timeoutMs) 
    throws TimeoutException {
    
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Future<Object> future = executor.submit(() -> {
        return expressionEvaluator.evaluate(condition, context);
    });
    
    try {
        return future.get(timeoutMs, TimeUnit.MILLISECONDS);
    } catch (ExecutionException e) {
        throw new RuntimeException("Condition evaluation failed", e.getCause());
    } finally {
        executor.shutdown();
    }
}

Transaction Boundary Design

Single transaction scope for rule evaluation:

  1. Read-Only: Rule fetch and context validation
  2. Write-Only: Action execution (asynchronous)
  3. Idempotent: Re-evaluation with same context is safe

Service Interface Contracts

public interface RuleEngineService {
    Result evaluateRule(Rule rule, Context context);
    Rule createRule(RuleCreateRequest request);
    Rule updateRule(String ruleId, RuleUpdateRequest request);
    void deleteRule(String ruleId);
    ValidationResult validateRule(Rule rule);
}

Saga Pattern for Distributed Transactions

For complex actions involving external systems:

public class RuleEvaluationSaga {
    private final List<SagaStep> steps;
    
    public void execute(Rule rule, Context context) {
        SagaContext sagaContext = new SagaContext();
        
        for (SagaStep step : steps) {
            try {
                step.execute(sagaContext, context);
                sagaContext.addStepResult(step.getName(), "SUCCESS");
            } catch (Exception e) {
                // Compensate previous steps
                compensate(sagaContext, step);
                throw new RuleEvaluationException("Saga failed", e);
            }
        }
    }
}

Domain Object Design

public class Rule {
    private String id;
    private String name;
    private String description;
    private String condition;
    private List<Action> actions;
    private RuleStatus status;
    private Instant createdAt;
    private Instant updatedAt;
    
    // Getters and setters
}

public class Context {
    private Map<String, Object> data;
    private Map<String, String> metadata;
    
    // Getters and setters
}

public class Result {
    private boolean success;
    private String message;
    private Map<String, Object> payload;
    
    // Getters and setters
}

5. Concurrency & Thread Safety

Concurrency Model

The rule evaluation engine will operate in a multi-threaded environment where multiple concurrent rule evaluations can occur simultaneously. The system will leverage Java's ExecutorService with a fixed thread pool to manage concurrent evaluation requests. Each rule evaluation will run in its own thread, allowing for efficient parallel processing without blocking the main application thread.

// Thread pool configuration for rule evaluation
private final ExecutorService ruleEvaluationExecutor = 
    Executors.newFixedThreadPool(
        Runtime.getRuntime().availableProcessors() * 2,
        Thread.ofVirtual().factory()
    );

Race Condition Prevention Strategy

Race conditions in rule evaluation can occur when multiple threads attempt to read or modify shared state simultaneously. To prevent these:

  1. Immutable Rule Objects: All rule definitions are immutable once created, preventing concurrent modification.
  2. Thread-Local Context: Evaluation context is maintained per-thread to prevent cross-contamination.
  3. Synchronized Access to Shared Resources: Critical shared resources like rule caches and configuration are protected using read-write locks.

Optimistic vs. Pessimistic Locking Selection

For rule evaluation engine, optimistic locking is preferred over pessimistic locking due to:

  1. High Concurrency: The system expects many concurrent rule evaluations with minimal write operations.
  2. Read-Heavy Workload: Most operations are read-based (rule lookups, evaluation).
  3. Low Contention: Rule definitions change infrequently, making optimistic locking suitable.
  4. Better Performance: Avoids blocking threads and reduces latency.
// Example of optimistic locking in rule versioning
public class Rule {
    private final String id;
    private final String version;
    private final String content;
    
    // Version field for optimistic locking
    private volatile int versionNumber;
    
    public boolean updateIfVersionMatches(String expectedVersion, String newContent) {
        if (this.version.equals(expectedVersion)) {
            // Update logic here
            return true;
        }
        return false;
    }
}

Deadlock Prevention Strategy

Deadlock prevention is achieved through:

  1. Fixed Ordering: All rule evaluation threads acquire locks in a consistent order.
  2. Timeout-Based Lock Acquisition: Implement timeouts on lock acquisition to prevent indefinite waits.
  3. Hierarchical Locking: Define clear lock hierarchy (e.g., global rule cache lock before individual rule locks).
// Deadlock prevention in rule evaluation context
public class RuleEvaluationContext {
    private final ReadWriteLock cacheLock = new ReentrantReadWriteLock();
    private final Lock evaluationLock = new ReentrantLock();
    
    public void evaluateWithDeadlockPrevention(Rule rule, EvaluationContext context) {
        try {
            // Acquire locks in fixed order to prevent deadlocks
            cacheLock.readLock().lock();
            evaluationLock.lock();
            
            // Perform evaluation logic here
            evaluateRule(rule, context);
            
        } finally {
            evaluationLock.unlock();
            cacheLock.readLock().unlock();
        }
    }
}

Idempotency Implementation for Concurrent Duplicate Requests

Duplicate concurrent requests are handled through:

  1. Request Deduplication: Unique request identifiers are used to identify and prevent duplicate processing.
  2. Idempotency Keys: For rule evaluation operations, idempotency keys ensure that identical evaluations are not processed multiple times.
  3. In-Memory Cache: Recent evaluation results are cached with TTL to avoid reprocessing.
// Idempotency handling in rule evaluation service
public class RuleEvaluationService {
    private final Map<String, CompletableFuture<EvaluationResult>> activeRequests = 
        new ConcurrentHashMap<>();
    
    public CompletableFuture<EvaluationResult> evaluateRuleAsync(
            String ruleId, 
            EvaluationContext context,
            String idempotencyKey) {
        
        return activeRequests.computeIfAbsent(idempotencyKey, key -> {
            CompletableFuture<EvaluationResult> future = new CompletableFuture<>();
            
            ruleEvaluationExecutor.submit(() -> {
                try {
                    EvaluationResult result = evaluateRule(ruleId, context);
                    future.complete(result);
                } catch (Exception e) {
                    future.completeExceptionally(e);
                }
            });
            
            return future;
        });
    }
}

Thread Safety in Rule Engine Components

  1. Rule Repository: Uses concurrent collections and proper synchronization for thread-safe lookups.
  2. Evaluation Context: Thread-local storage ensures each thread has its own context copy.
  3. Result Cache: Uses ConcurrentHashMap with proper atomic operations for safe concurrent access.
// Thread-safe rule repository implementation
public class RuleRepository {
    private final ConcurrentHashMap<String, Rule> rules = new ConcurrentHashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public Rule getRule(String ruleId) {
        return rules.get(ruleId);
    }
    
    public void updateRule(Rule rule) {
        lock.writeLock().lock();
        try {
            rules.put(rule.getRuleId(), rule);
        } finally {
            lock.writeLock().unlock();
        }
    }
}

Performance Considerations

  1. Thread Pool Configuration: Thread pool size is tuned based on CPU cores and I/O characteristics.
  2. Memory Management: Avoiding memory leaks through proper resource cleanup in evaluation contexts.
  3. Garbage Collection: Minimizing object allocation during rule evaluation to reduce GC pressure.

The concurrency model is designed to provide high throughput for concurrent rule evaluations while maintaining thread safety and preventing race conditions through careful design patterns and proper synchronization primitives.

6. Sequence Diagrams for Key Flows

sequenceDiagram
    participant U as User/Client
    participant API as RuleEngineAPI
    participant Service as RuleEvaluationService
    participant Engine as RuleEngine
    participant DB as Database
    participant Cache as RedisCache

    U->>API: POST /evaluate
    API->>Service: evaluateRule(request)
    Service->>Cache: get(ruleId)
    Cache-->>Service: cachedRule
    alt Rule not in cache
        Service->>DB: get(ruleById)
        DB-->>Service: ruleDefinition
        Service->>Cache: put(ruleId, rule)
    end
    Service->>Engine: evaluate(rule, context)
    Engine-->>Service: evaluationResult
    Service-->>API: response(result)
    API-->>U: 200 OK
sequenceDiagram
    participant U as User/Client
    participant API as RuleEngineAPI
    participant Service as RuleEvaluationService
    participant Engine as RuleEngine
    participant DB as Database
    participant Cache as RedisCache
    participant EventPublisher as EventPublisher

    U->>API: POST /evaluate
    API->>Service: evaluateRule(request)
    Service->>Cache: get(ruleId)
    Cache-->>Service: cacheMiss
    Service->>DB: get(ruleById)
    DB-->>Service: ruleDefinition
    Service->>Cache: put(ruleId, rule)
    Service->>Engine: evaluate(rule, context)
    Engine-->>Service: evaluationResult
    alt Result is "triggered"
        Service->>EventPublisher: publish(ruleTriggered)
    end
    Service-->>API: response(result)
    API-->>U: 200 OK
sequenceDiagram
    participant U as User/Client
    participant API as RuleEngineAPI
    participant Service as RuleEvaluationService
    participant Engine as RuleEngine
    participant DB as Database
    participant Cache as RedisCache
    participant EventPublisher as EventPublisher
    participant AsyncQueue as Kafka/EventBus

    U->>API: POST /evaluate
    API->>Service: evaluateRule(request)
    Service->>Cache: get(ruleId)
    Cache-->>Service: cacheMiss
    Service->>DB: get(ruleById)
    DB-->>Service: ruleDefinition
    Service->>Cache: put(ruleId, rule)
    Service->>Engine: evaluate(rule, context)
    Engine-->>Service: evaluationResult
    alt Result is "triggered"
        Service->>EventPublisher: publish(ruleTriggered)
        EventPublisher->>AsyncQueue: send(event)
    end
    Service-->>API: response(result)
    API-->>U: 200 OK

7. Caching Design

Cache Strategy Selection

For the Rule Evaluation Engine, we'll use a cache-aside pattern with LRU eviction policy. This approach provides the best balance between performance and consistency for rule evaluation workloads where:

  • Rules are frequently accessed but updated infrequently
  • Cache misses are acceptable for performance-critical operations
  • We need to maintain strong consistency with the rule store

Cache Key Design

Cache keys will follow a structured namespace pattern:

rule-engine:{ruleId}:{version}:{contextHash}

Where:

  • ruleId - Unique identifier for the rule
  • version - Rule version to handle updates
  • contextHash - MD5 hash of input context parameters

TTL Strategy

Data TypeTTLReason
Rule Definitions24hRules change infrequently, 24h provides good balance
Rule Evaluation Results5mRecent evaluations should be cached for performance
Context Metadata1hContext metadata changes less frequently than evaluations

Cache Eviction Policy

We'll implement LRU (Least Recently Used) eviction policy with:

  • Size limit: 100,000 entries per cache instance
  • Memory-based eviction with periodic cleanup
  • Support for soft TTLs to handle stale entries gracefully

Cache Stampede Prevention

To prevent cache stampedes:

  1. Probabilistic early expiry: 80% TTL for cache entries
  2. Distributed locks: For critical rule loading operations using Redis distributed locks
  3. Cache warming: Pre-warm hot rules during system startup

Distributed Cache Architecture

We'll use Redis Cluster with:

  • 3 master nodes for high availability
  • 3 replica nodes per master for read scaling
  • Automatic failover with Redis Sentinel monitoring
  • Cross-region replication for disaster recovery

Cache Invalidation Strategy

We'll implement event-driven invalidation:

  1. Rule updates: Publish rule.updated events to invalidate related cache entries
  2. Context changes: Invalidate all rule evaluations for changed contexts
  3. Manual invalidation: Admin endpoint to force cache clearing

Java Implementation Example

@Component
public class RuleEvaluationCache {
    private final RedisTemplate<String, Object> redisTemplate;
    private final ObjectMapper objectMapper;
    
    public RuleEvaluationCache(RedisTemplate<String, Object> redisTemplate, 
                              ObjectMapper objectMapper) {
        this.redisTemplate = redisTemplate;
        this.objectMapper = objectMapper;
    }
    
    public CachedEvaluationResult getRuleEvaluation(String ruleId, 
                                                   Map<String, Object> context) {
        String cacheKey = buildCacheKey(ruleId, context);
        
        // Try to get from cache
        String cachedValue = (String) redisTemplate.opsForValue().get(cacheKey);
        if (cachedValue != null) {
            try {
                return objectMapper.readValue(cachedValue, CachedEvaluationResult.class);
            } catch (Exception e) {
                // Log error but continue with fresh evaluation
                log.warn("Failed to deserialize cached result for key: {}", cacheKey);
            }
        }
        
        return null;
    }
    
    public void putRuleEvaluation(String ruleId, 
                                 Map<String, Object> context,
                                 CachedEvaluationResult result) {
        String cacheKey = buildCacheKey(ruleId, context);
        
        try {
            String jsonValue = objectMapper.writeValueAsString(result);
            redisTemplate.opsForValue().set(
                cacheKey, 
                jsonValue,
                Duration.ofMinutes(5).toMillis(),
                TimeUnit.MILLISECONDS
            );
        } catch (Exception e) {
            log.error("Failed to cache rule evaluation result", e);
        }
    }
    
    private String buildCacheKey(String ruleId, Map<String, Object> context) {
        String contextHash = generateContextHash(context);
        return String.format("rule-engine:%s:%s", ruleId, contextHash);
    }
    
    private String generateContextHash(Map<String, Object> context) {
        try {
            String contextJson = objectMapper.writeValueAsString(context);
            return DigestUtils.md5Hex(contextJson);
        } catch (Exception e) {
            log.warn("Failed to generate context hash, using fallback", e);
            return "fallback";
        }
    }
}

8. Repository / Data Access Layer

Repository Pattern Implementation

The repository pattern is implemented to abstract data access logic and provide a clean separation between domain objects and database operations. Each domain entity has its own repository interface and implementation.

public interface RuleRepository {
    Optional<Rule> findById(UUID id);
    List<Rule> findAll();
    List<Rule> findByStatus(RuleStatus status);
    Rule save(Rule rule);
    void delete(UUID id);
    Page<Rule> findAll(Pageable pageable);
}

public interface RuleEvaluationLogRepository {
    void save(RuleEvaluationLog log);
    List<RuleEvaluationLog> findByRuleId(UUID ruleId, Pageable pageable);
}

ORM vs. Raw Query Decisions

For the rule evaluation engine, we use JPA/Hibernate for most operations due to:

  • Type safety and compile-time checking
  • Object-relational mapping reduces boilerplate code
  • Rich query capabilities with JPQL and Criteria API
  • Easy integration with Spring Data JPA

Raw SQL is used for:

  • Complex analytical queries (e.g., performance metrics)
  • Bulk operations where performance is critical
  • Queries requiring database-specific features

Read vs. Write Separation

We implement CQRS (Command Query Responsibility Segregation) for the rule evaluation engine:

// Write-side repositories
public interface RuleWriteRepository {
    Rule save(Rule rule);
    void delete(UUID id);
}

// Read-side repositories (optimized for querying)
public interface RuleReadRepository {
    RuleProjection findById(UUID id);
    List<RuleProjection> findByStatus(RuleStatus status);
    Page<RuleProjection> findAll(Pageable pageable);
}

// Read model projection
@Immutable
public class RuleProjection {
    private final UUID id;
    private final String name;
    private final String description;
    private final RuleStatus status;
    private final String expression;
    private final LocalDateTime createdAt;
    private final LocalDateTime updatedAt;
}

Connection Pool Configuration

# application.yml
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      validation-timeout: 5000

N+1 Query Prevention Strategy

We use eager loading with JOIN FETCH for critical paths and batch loading for collections:

@Repository
public class RuleRepositoryImpl {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public List<Rule> findAllWithEvaluations() {
        // Prevents N+1 queries for rule evaluations
        String jpql = """
            SELECT r FROM Rule r 
            LEFT JOIN FETCH r.evaluations e 
            WHERE r.status = :status
            """;
        
        return entityManager.createQuery(jpql, Rule.class)
                           .setParameter("status", RuleStatus.ACTIVE)
                           .getResultList();
    }
    
    public Page<RuleProjection> findAllProjected(Pageable pageable) {
        // Using native query for better performance on large datasets
        String sql = """
            SELECT r.id, r.name, r.description, r.status, r.expression,
                   r.created_at, r.updated_at
            FROM rules r
            ORDER BY r.created_at DESC
            LIMIT :limit OFFSET :offset
            """;
            
        Query query = entityManager.createNativeQuery(sql, "RuleProjection");
        query.setParameter("limit", pageable.getPageSize());
        query.setParameter("offset", pageable.getOffset());
        
        return new PageImpl<>(query.getResultList(), pageable, getTotalCount());
    }
}

Batch Operation Design

For bulk rule operations, we implement batch processing:

@Service
@Transactional
public class BatchRuleService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void batchUpdateRules(List<RuleUpdateRequest> requests) {
        // Batch size of 1000 to prevent memory issues
        int batchSize = 1000;
        
        for (int i = 0; i < requests.size(); i += batchSize) {
            List<RuleUpdateRequest> batch = 
                requests.subList(i, Math.min(i + batchSize, requests.size()));
            
            // Use bulk update with native SQL for better performance
            String sql = "UPDATE rules SET status = :status WHERE id IN (:ids)";
            Query query = entityManager.createNativeQuery(sql);
            
            List<UUID> ids = batch.stream().map(r -> r.id).collect(Collectors.toList());
            query.setParameter("status", RuleStatus.ACTIVE);
            query.setParameter("ids", ids);
            
            query.executeUpdate();
        }
    }
}

Data Access Layer Summary

The repository layer provides:

  1. Clean separation of data access concerns
  2. Support for both read and write operations with appropriate optimizations
  3. CQRS pattern implementation to handle different query patterns efficiently
  4. N+1 query prevention through eager loading and batch operations
  5. Performance optimization through connection pooling and native queries where needed
  6. Transaction boundary management at the service layer while maintaining data consistency

This design enables the rule evaluation engine to scale efficiently while maintaining clean architecture and preventing common performance pitfalls like N+1 queries.

9. Asynchronous Processing & Messaging

Event-driven boundaries in this feature

The rule evaluation engine will process rules asynchronously when:

  • Rules are evaluated against large datasets or complex conditions that would cause significant latency
  • Rule updates need to be propagated to downstream systems (e.g., notifications, analytics)
  • Batch rule processing is required for performance optimization

Synchronous processing will be used for:

  • Real-time rule evaluation requests from clients (e.g., real-time validation)
  • Immediate feedback for rule creation/editing operations

Message schema design with sample payload

{
  "version": "1.0",
  "eventId": "uuid-4f89a3b2-cd1e-4f5a-b67c-8d9e0f1a2b3c",
  "eventType": "rule.evaluated",
  "timestamp": "2023-10-15T14:30:00Z",
  "payload": {
    "ruleId": "rule-12345",
    "context": {
      "userId": "user-67890",
      "resourceType": "order",
      "resourceId": "order-54321"
    },
    "result": {
      "passed": true,
      "evaluatedAt": "2023-10-15T14:30:00Z",
      "evaluationTimeMs": 125
    }
  },
  "source": "rule-engine-service"
}

Dead letter queue strategy and poison message handling

Poison messages will be routed to a dead letter queue (DLQ) after 3 failed delivery attempts. The DLQ will be monitored by an alerting system that triggers manual investigation when messages accumulate beyond a threshold (e.g., 100 messages). Messages in the DLQ will be marked with metadata indicating the failure reason and retry count.

At-least-once vs. exactly-once delivery

At-least-once delivery is selected for rule evaluation events because:

  • Rule evaluation results are idempotent (re-evaluating same conditions with same data should produce same result)
  • The system can tolerate duplicate events without business impact
  • Simpler implementation and better performance

Retry policy for consumers

Retry policy:

  • Max attempts: 3
  • Backoff strategy: Exponential with jitter (1s, 2s, 4s)
  • Retryable errors: Network timeouts, service unavailability
  • Non-retryable errors: Invalid rule syntax, malformed context data

Outbox pattern for reliable event publishing

Outbox pattern is implemented to ensure event delivery reliability:

public class RuleEvaluationEvent {
    private String id;
    private String ruleId;
    private String context;
    private boolean processed;
    private LocalDateTime createdAt;
    private LocalDateTime processedAt;
}

@Service
public class RuleEvaluationService {
    
    @Autowired
    private EventOutboxRepository eventOutboxRepository;
    
    @Transactional
    public void evaluateRuleAndPublishEvent(String ruleId, Map<String, Object> context) {
        // Evaluate rule logic here
        boolean passed = evaluateRule(ruleId, context);
        
        // Create event in outbox before committing transaction
        RuleEvaluationEvent event = new RuleEvaluationEvent();
        event.setId(UUID.randomUUID().toString());
        event.setRuleId(ruleId);
        event.setContext(context.toString());
        event.setProcessed(false);
        event.setCreatedAt(LocalDateTime.now());
        
        eventOutboxRepository.save(event);
        
        // Transaction commits here - if it fails, event won't be published
    }
    
    @Scheduled(fixedDelay = 5000)
    public void processOutboxEvents() {
        List<RuleEvaluationEvent> events = eventOutboxRepository.findUnprocessedEvents();
        
        for (RuleEvaluationEvent event : events) {
            try {
                // Publish to message broker
                messageBroker.publish("rule.evaluated", event);
                
                // Mark as processed
                event.setProcessed(true);
                event.setProcessedAt(LocalDateTime.now());
                eventOutboxRepository.save(event);
            } catch (Exception e) {
                // Handle retry logic or move to DLQ
                handleFailedEvent(event, e);
            }
        }
    }
}

Sequence diagram for async flows

sequenceDiagram
    participant Client
    participant RuleEngineService
    participant EventOutboxRepository
    participant MessageBroker
    participant EventConsumer
    
    Client->>RuleEngineService: POST /rules/evaluate
    RuleEngineService->>EventOutboxRepository: Save event to outbox
    RuleEngineService-->>Client: 202 Accepted
    EventOutboxRepository->>MessageBroker: Publish event to queue
    MessageBroker->>EventConsumer: Deliver event
    EventConsumer->>EventConsumer: Process rule evaluation result

10. Error Handling Design

Error Classification Hierarchy

The rule evaluation engine follows a three-tier error classification system:

  1. Validation Errors - Input data doesn't meet business rules (e.g., invalid JSON, missing fields)
  2. Domain Errors - Business logic violations during rule evaluation (e.g., unsupported operator, invalid condition)
  3. Infrastructure Errors - System-level failures (e.g., database connectivity, cache unavailability)
public class RuleEngineException extends RuntimeException {
    private final ErrorType errorType;
    private final String errorCode;
    
    public RuleEngineException(ErrorType errorType, String errorCode, String message) {
        super(message);
        this.errorType = errorType;
        this.errorCode = errorCode;
    }
    
    public RuleEngineException(ErrorType errorType, String errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorType = errorType;
        this.errorCode = errorCode;
    }
    
    // Getters
}

public enum ErrorType {
    VALIDATION, 
    DOMAIN, 
    INFRASTRUCTURE
}

Error Propagation Strategy

Errors are handled at the service boundary and re-thrown with appropriate context:

  • Validation errors are caught early and converted to RuleEngineException with VALIDATION type
  • Domain errors are propagated up to service layer for business-specific handling
  • Infrastructure errors are wrapped with retry logic or circuit breaker patterns

Retry Strategy

For transient infrastructure issues, exponential backoff with jitter:

public class RetryConfig {
    private static final int MAX_RETRIES = 3;
    private static final long INITIAL_DELAY_MS = 1000;
    private static final double JITTER_FACTOR = 0.2;
    
    public static long calculateDelay(int attempt) {
        long delay = (long) (INITIAL_DELAY_MS * Math.pow(2, attempt));
        double jitter = JITTER_FACTOR * delay;
        return delay + (long) (Math.random() * 2 * jitter - jitter);
    }
}

Circuit Breaker Design

Using Resilience4j for circuit breaker pattern:

public class CircuitBreakerManager {
    private final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("rule-evaluation");
    
    public <T> T executeWithCircuitBreaker(Supplier<T> operation) {
        return circuitBreaker.executeSupplier(operation);
    }
    
    public void onRuleEvaluationFailure() {
        circuitBreaker.onError(1, new RuleEngineException(
            ErrorType.INFRASTRUCTURE, "RULE_EVALUATION_FAILED", 
            "Rule evaluation failed"));
    }
}

Fallback Behavior

For critical rule evaluation operations:

public class RuleEvaluationFallback {
    public EvaluationResult fallbackEvaluation(
            Rule rule, Map<String, Object> context, 
            Throwable cause) {
        // Log fallback trigger
        logger.warn("Falling back to default evaluation for rule {}", 
                   rule.getId(), cause);
        
        // Return safe default or error result
        return EvaluationResult.builder()
            .ruleId(rule.getId())
            .passed(false)
            .reason("Evaluation failed, using fallback")
            .build();
    }
}

Error Logging Format

Structured logging with trace context:

public class ErrorLogger {
    private static final ObjectMapper objectMapper = new ObjectMapper();
    
    public void logError(String traceId, String spanId, String userId, 
                        String errorCode, String errorMessage, 
                        Map<String, Object> context) {
        Map<String, Object> logEntry = Map.of(
            "timestamp", Instant.now(),
            "trace_id", traceId,
            "span_id", spanId,
            "user_id", userId,
            "error_code", errorCode,
            "message", errorMessage,
            "context", context,
            "stack_trace", getStackTrace()
        );
        
        logger.error("Rule engine error: {}", 
                    objectMapper.writeValueAsString(logEntry));
    }
}

Alerting Thresholds

Critical errors trigger immediate alerts:

Error CodeSeverityAlert ThresholdDescription
RULE_EVALUATION_FAILEDCRITICAL>5 occurrences/minuteInfrastructure issues
INVALID_RULE_JSONWARNING>10 occurrences/minuteInput validation
UNSUPPORTED_OPERATORWARNING>5 occurrences/minuteBusiness logic issues

Error Response Format

Standardized JSON error response:

{
  "error": {
    "code": "INVALID_RULE_JSON",
    "message": "Rule JSON is malformed",
    "details": {
      "field": "conditions",
      "reason": "Missing required field"
    },
    "timestamp": "2023-10-15T14:30:00Z",
    "request_id": "req-12345"
  }
}

Error Recovery Patterns

  1. Retry with exponential backoff for transient database/cache issues
  2. Fallback to default evaluation when rule engine is unavailable
  3. Graceful degradation - partial rule evaluation with warnings for failed rules
  4. Audit logging of all error conditions for post-mortem analysis

11. Security Design

Input Validation and Sanitization Strategy

All inputs are validated using a combination of:

  • Schema validation with JSON Schema for structured data (e.g., rule definitions, input payloads)
  • Regex validation for specific patterns (e.g., rule IDs, operator names)
  • Length limits on all string fields to prevent buffer overflow attacks
  • Type checking at the service boundary to ensure correct data types

SQL Injection Prevention

All database interactions use parameterized queries and prepared statements exclusively. No dynamic query building or string concatenation is performed. ORM usage is restricted to well-vetted libraries that enforce parameterization.

Authentication Middleware Design

Authentication is handled via JWT tokens passed in the Authorization header. The middleware:

  • Validates token signature using asymmetric keys (RSA-2048)
  • Checks token expiration and audience claims
  • Extracts user context (user_id, roles) from the payload
  • Enforces token refresh logic for long-running sessions

Authorization Enforcement (RBAC)

Role-Based Access Control is implemented at the service layer:

  • Users are assigned roles (e.g., ADMIN, RULE_EDITOR, READ_ONLY)
  • Each endpoint has role-based access control checks
  • Permissions are validated before processing any request using a centralized permission service

Sensitive Data Encryption at Rest and in Transit

  • At rest: All sensitive rule data is encrypted using AES-256-GCM encryption with keys managed by AWS KMS
  • In transit: All API communications use TLS 1.3 with HSTS header enforcement
  • Rule content: Rule definitions and input data are encrypted before storage

PII Handling and Masking Strategy

  • Personal identifiers in rule input data are masked or removed before logging
  • All PII fields are tagged with appropriate sensitivity labels for audit purposes
  • Logging includes a PII mask policy that redacts sensitive fields from structured logs

Secrets Management

  • API keys, database credentials, and encryption keys are stored in AWS Secrets Manager
  • Secrets are rotated automatically every 90 days using AWS Lambda functions
  • Access to secrets is controlled via IAM policies with least-privilege principle

Audit Logging Design

All security-relevant events are logged with:

{
  "timestamp": "2023-10-27T14:30:00Z",
  "event_type": "RULE_EVALUATION",
  "user_id": "user-12345",
  "principal": "admin@example.com",
  "action": "EVALUATE_RULE",
  "resource_id": "rule-abc123",
  "ip_address": "10.0.0.1",
  "user_agent": "Mozilla/5.0...",
  "result": "SUCCESS",
  "details": {
    "rule_name": "customer_segmentation_rule",
    "evaluation_time_ms": 42,
    "input_data_hash": "sha256:abc123..."
  }
}
  • Logs are retained for 90 days with automatic archiving to S3
  • Audit logs are encrypted at rest and indexed for search capability

12. Testing Strategy

Unit Testing Scope and Coverage Targets

  • 90%+ code coverage for business logic in service classes (target: 95% for critical paths)
  • 100% coverage for utility classes and helper methods (validation, conversion, formatting)
  • 95%+ coverage for rule engine core components (expression parser, evaluation engine, condition evaluator)
  • Target: 85%+ for domain object mutations and state transitions
  • Unit tests should validate all business logic branches, including edge cases, error conditions, and boundary values

Integration Testing Strategy

  • Testcontainers for database integration testing (PostgreSQL/MySQL) with real schema and migration scripts
  • Embedded Redis for cache integration testing (mocking external services)
  • WireMock for external service stubbing (e.g., notification service, third-party rule sources)
  • Embedded Kafka or Mockito-based for event-driven integration testing
  • Database transaction isolation tests to validate concurrent rule evaluation scenarios
  • End-to-end integration tests for complete rule evaluation flow with real data

Contract Testing

  • Pact for API contract testing between rule engine and consumer services (e.g., rule management UI, policy engine)
  • Versioned contract definitions using Pact Broker for multi-service integration
  • Consumer-driven contracts for rule evaluation API endpoints to ensure backward compatibility
  • Contract validation in CI pipeline before deployment

Performance Testing Tool and Key Scenarios

  • k6 for load testing rule evaluation engine with concurrent requests
  • JMeter for stress testing rule engine under high throughput (10k+ concurrent evaluations)
  • Key performance scenarios:
    • Concurrent rule evaluation (500 simultaneous rules)
    • Large rule set evaluation (10k+ rules with complex conditions)
    • Rule engine warm-up and cold-start performance
    • Memory usage under high concurrent load
  • Target: p95 latency < 100ms for single rule evaluation, p99 < 500ms

Test Data Management Strategy

  • Factory pattern for generating test data (Rule, Condition, Variable objects)
  • Fixtures for common rule configurations and evaluation scenarios
  • Seed data scripts to populate test database with realistic rule sets
  • Test data isolation using database schema per test environment (dev/staging)
  • Data generation tools for large-scale rule set testing (e.g., generating 10k+ rules with random conditions)
  • Rule template library to simplify creation of complex rule structures for testing
// Example unit test for rule evaluation logic
@Test
public void testRuleEvaluationWithValidConditions() {
    // Given
    Rule rule = createTestRule();
    Map<String, Object> context = Map.of("age", 25, "income", 50000);
    
    // When
    boolean result = ruleEngine.evaluate(rule, context);
    
    // Then
    assertTrue(result);
}

@Test
public void testRuleEvaluationWithInvalidConditions() {
    // Given
    Rule rule = createTestRule();
    Map<String, Object> context = Map.of("age", 17, "income", 50000);
    
    // When
    boolean result = ruleEngine.evaluate(rule, context);
    
    // Then
    assertFalse(result);
}

13. Observability Design

Structured Logging Schema

All logs must follow a structured format with consistent fields to enable effective querying and analysis:

{
  "timestamp": "2023-10-27T14:30:45.123Z",
  "level": "INFO|WARN|ERROR",
  "service": "rule-evaluation-engine",
  "trace_id": "12345678-1234-1234-1234-123456789012",
  "span_id": "abcdef12-3456-7890-abcd-ef1234567890",
  "user_id": "user-123",
  "entity_id": "rule-456",
  "operation": "evaluateRule",
  "message": "Rule evaluation completed successfully",
  "duration_ms": 150,
  "error_code": "RULE_EVALUATION_TIMEOUT",
  "error_message": "Timeout while evaluating rule",
  "stack_trace": "...",
  "rule_context": {
    "rule_id": "rule-456",
    "input_data_size_bytes": 1024,
    "evaluation_time_ms": 150
  }
}

PII masking rules:

  • User IDs and identifiers are masked in logs when not required for debugging
  • Input data is truncated or redacted if it contains sensitive fields (e.g., PII, credit card numbers)
  • All log messages are scanned for sensitive patterns before being written to logs

Distributed Tracing Instrumentation

Using OpenTelemetry with custom spans for key operations:

Span Types and Attributes:

  1. Rule Evaluation Span

    • Operation name: rule_evaluation
    • Attributes:
      • rule.id: "rule-123"
      • rule.type: "business_rule"
      • input.size.bytes: 1024
      • evaluation.duration.ms: 150
      • evaluation.result: "passed|failed|error"
  2. Rule Compilation Span

    • Operation name: rule_compilation
    • Attributes:
      • rule.id: "rule-123"
      • compiler.version: "v1.2.0"
      • compilation.duration.ms: 50
  3. Rule Validation Span

    • Operation name: rule_validation
    • Attributes:
      • rule.id: "rule-123"
      • validation.type: "syntax|semantic"
      • validation.duration.ms: 20

Metrics Design

RED Method Metrics (Rate, Errors, Duration)

Metric NameTypeLabelsDescription
rule_evaluations_totalCounterrule_id, status, error_codeTotal number of rule evaluations
rule_evaluations_errors_totalCounterrule_id, error_typeErrors during rule evaluation
rule_evaluation_duration_secondsHistogramrule_id, statusTime taken to evaluate rules
rule_compilation_duration_secondsHistogramrule_id, statusTime taken to compile rules
rule_validation_duration_secondsHistogramrule_id, statusTime taken to validate rules
active_rules_countGaugerule_typeNumber of active rules in system
rule_engine_queue_sizeGaugequeue_typeSize of rule evaluation queues

Additional Metrics:

  • rule_engine_memory_usage_bytes (Gauge): Memory usage by engine
  • rule_engine_cpu_percent (Gauge): CPU utilization of engine process
  • rule_engine_error_rate (Gauge): Error rate over time window

Health Check Endpoint Design

GET /health
Accept: application/json

Response Format:

{
  "status": "healthy|unhealthy",
  "checks": {
    "database": {
      "status": "healthy",
      "details": "Connected to primary DB"
    },
    "cache": {
      "status": "healthy",
      "details": "Redis cluster connected"
    },
    "rule_engine": {
      "status": "healthy",
      "details": "Engine is processing rules"
    }
  },
  "timestamp": "2023-10-27T14:30:45.123Z"
}

Endpoints:

  • /health - Overall system health (liveness probe)
  • /ready - Service ready to accept traffic (readiness probe)
  • /live - Process is alive but not necessarily ready
  • /startup - Detailed startup health check

Alerting Thresholds

MetricWarning LevelCritical LevelEscalation Path
rule_evaluations_errors_total> 5 errors/minute> 20 errors/minuteOn-call engineer
rule_evaluation_duration_seconds (p95)> 200ms> 500msSRE team
rule_engine_queue_size> 100 items> 500 itemsSRE team
rule_engine_error_rate> 1%> 5%SRE team
active_rules_count< 100 rules< 50 rulesSRE team
rule_engine_memory_usage_bytes> 80% of limit> 95% of limitSRE team

Alerting Logic:

  • Use sliding window for rate-based alerts (5-minute window)
  • Apply exponential backoff for alert suppression
  • Include contextual metadata in alerts (rule ID, error type, timestamp)
  • Implement auto-resolving when thresholds return to normal levels

14. Open Questions, Risks & Trade-offs

Decisions requiring product / engineering alignment

  • Rule complexity vs. performance trade-off: Should we support complex rule expressions with nested conditions, or limit to simple key-value comparisons? Complex rules may require more sophisticated parsing and evaluation engines but could increase latency.
  • Real-time vs. batch evaluation: Should rule evaluation happen synchronously during request processing or asynchronously in a batch queue? Real-time provides immediate feedback but can impact latency, while batch processing offers better throughput but introduces eventual consistency.
  • Rule versioning strategy: How should we handle rule updates that may break existing logic? Should we support atomic rule rollbacks or require manual version management?

Known trade-offs and justifications

  • Evaluation engine complexity vs. performance: We're choosing a lightweight rule engine over a full-fledged business rules engine to maintain low latency and simpler deployment. This trades flexibility for performance.
  • Memory vs. CPU usage: Using in-memory rule compilation and caching provides fast evaluation but increases memory consumption. We could reduce memory usage by using disk-based compilation but at the cost of slower evaluation.
  • Single-threaded vs. multi-threaded evaluation: For high-concurrency scenarios, we might consider parallel rule evaluation but this introduces complexity in managing shared state and potential race conditions.
  • Expression language vs. DSL: We're using a simple expression language (e.g., field1 > 5 && field2 < 10) rather than a full DSL to keep the system simpler and more maintainable, but this limits extensibility.

Risks and mitigation strategies

  • Rule evaluation timeout: Complex rules may cause timeouts during evaluation. Mitigation: Implement rule execution time limits and fallback to default behavior.
  • Infinite loops in rule conditions: Malformed rules with circular dependencies could cause infinite evaluation loops. Mitigation: Implement maximum recursion depth limits and circuit breaker patterns.
  • Performance degradation under high load: Concurrent rule evaluations could cause resource contention. Mitigation: Use thread pools, implement circuit breakers, and add rate limiting.
  • Security vulnerabilities in rule expressions: If rules allow arbitrary code execution, they could be exploited. Mitigation: Use sandboxed rule evaluation engines and restrict expression syntax.
  • Cache invalidation timing: Cached rule results may become stale if rules are updated. Mitigation: Implement TTL-based cache invalidation and event-driven cache clearing.

Technical debt acknowledged

  • Limited rule expression language: The current implementation only supports basic boolean logic and field comparisons. Extending to support complex functions or custom operators would require significant refactoring.
  • No rule validation framework: Rules are evaluated as-is without pre-validation, which could lead to runtime errors. A validation layer would improve robustness but adds complexity.
  • Hardcoded rule engine limits: Timeouts and memory limits are hardcoded, making them difficult to tune without code changes.
  • No rule dependency tracking: We don't track which rules depend on each other, making it harder to understand impact of rule changes.
  • Limited debugging capabilities: There's no built-in mechanism for debugging rule evaluation steps or tracing how a rule reached its conclusion.
  • No rule performance monitoring: We don't have built-in metrics to identify slow-performing rules that may need optimization.

Was this article helpful?

Topics in this article

More from Backend LLD

  • Backend LLD

    Decorator-Based Feature Extension Design Pattern

    Comprehensive low-level design for implementing extensible backend systems using decorator patterns with runtime feature composition and configuration management.

  • Backend LLD

    Policy Engine Design and Implementation

    Comprehensive low-level design for a flexible, scalable policy engine system supporting rule-based access control and business logic enforcement.

  • Backend LLD

    Command Pattern Framework Design and Implementation

    Comprehensive low-level design for a flexible command pattern framework with support for undo/redo, logging, and extensible command execution.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.