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
- 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)?
- 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?
- 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?
- 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?
- What latency sensitivity is expected for rule evaluations?
- Is sub-100ms required for all evaluations, or is batch processing acceptable?
- What authentication and authorization model is in place?
- Is rule creation/modification restricted to admin users or can any user create rules?
- 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
| Method | Path | Description | Auth |
|---|---|---|---|
| CreateRule | POST /v1/rules | Creates a new rule with conditions and actions | JWT |
| GetRule | GET /v1/rules/{ruleId} | Retrieves a specific rule by ID | JWT |
| UpdateRule | PUT /v1/rules/{ruleId} | Updates an existing rule | JWT |
| DeleteRule | DELETE /v1/rules/{ruleId} | Deletes a rule by ID | JWT |
| EvaluateRule | POST /v1/rules/{ruleId}/evaluate | Evaluates a rule against input data | JWT |
| EvaluateBatch | POST /v1/rules/evaluate/batch | Evaluates multiple rules in batch mode | JWT |
| ListRules | GET /v1/rules | Lists all rules with pagination | JWT |
| ValidateRule | POST /v1/rules/validate | Validates rule syntax and structure | JWT |
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 Code | Description |
|---|---|
| 200 | Success - GET, PUT, DELETE |
| 201 | Created - POST |
| 400 | Bad Request - Invalid input |
| 401 | Unauthorized - Missing or invalid auth |
| 403 | Forbidden - Insufficient permissions |
| 404 | Not Found - Resource not found |
| 422 | Unprocessable Entity - Validation errors |
| 429 | Too Many Requests - Rate limiting |
| 500 | Internal 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:
cursorparameter for continuationlimitparameter (default: 20, max: 100)sortparameter for ordering (by creation date, priority)
Authentication and Authorization
All endpoints require JWT authentication with scopes:
rule:read- Read access to rulesrule:write- Write access to rulesrule:evaluate- Evaluate rule capability
Rate Limiting
Rate limiting per endpoint:
CreateRule: 100 requests/minuteEvaluateRule: 500 requests/minute (per user)ListRules: 200 requests/minuteValidateRule: 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 Name | Fields | Type | Purpose |
|---|---|---|---|
| idx_status | status | Standard | Fast filtering by rule status |
| idx_created_at | created_at | Standard | Efficient time-based queries |
| idx_priority | priority | Standard | Fast rule set ordering |
| idx_rule_id | rule_id | Standard | Fast lookup of evaluation results by rule |
| idx_context_id | context_id | Standard | Fast lookup of evaluation results by context |
| idx_matched | matched | Standard | Efficient 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_atfield in 30-day buckets - Allows for time-based data lifecycle management and analytics
- Partition by
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_attimestampscreated_by/updated_byuser 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:
- RuleEngineService - Main entry point for rule operations
- RuleValidator - Validates rule syntax and semantics
- ExpressionEvaluator - Evaluates condition expressions
- 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:
- Read-Only: Rule fetch and context validation
- Write-Only: Action execution (asynchronous)
- 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:
- Immutable Rule Objects: All rule definitions are immutable once created, preventing concurrent modification.
- Thread-Local Context: Evaluation context is maintained per-thread to prevent cross-contamination.
- 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:
- High Concurrency: The system expects many concurrent rule evaluations with minimal write operations.
- Read-Heavy Workload: Most operations are read-based (rule lookups, evaluation).
- Low Contention: Rule definitions change infrequently, making optimistic locking suitable.
- 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:
- Fixed Ordering: All rule evaluation threads acquire locks in a consistent order.
- Timeout-Based Lock Acquisition: Implement timeouts on lock acquisition to prevent indefinite waits.
- 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:
- Request Deduplication: Unique request identifiers are used to identify and prevent duplicate processing.
- Idempotency Keys: For rule evaluation operations, idempotency keys ensure that identical evaluations are not processed multiple times.
- 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
- Rule Repository: Uses concurrent collections and proper synchronization for thread-safe lookups.
- Evaluation Context: Thread-local storage ensures each thread has its own context copy.
- Result Cache: Uses
ConcurrentHashMapwith 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
- Thread Pool Configuration: Thread pool size is tuned based on CPU cores and I/O characteristics.
- Memory Management: Avoiding memory leaks through proper resource cleanup in evaluation contexts.
- 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 ruleversion- Rule version to handle updatescontextHash- MD5 hash of input context parameters
TTL Strategy
| Data Type | TTL | Reason |
|---|---|---|
| Rule Definitions | 24h | Rules change infrequently, 24h provides good balance |
| Rule Evaluation Results | 5m | Recent evaluations should be cached for performance |
| Context Metadata | 1h | Context 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:
- Probabilistic early expiry: 80% TTL for cache entries
- Distributed locks: For critical rule loading operations using Redis distributed locks
- 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:
- Rule updates: Publish
rule.updatedevents to invalidate related cache entries - Context changes: Invalidate all rule evaluations for changed contexts
- 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:
- Clean separation of data access concerns
- Support for both read and write operations with appropriate optimizations
- CQRS pattern implementation to handle different query patterns efficiently
- N+1 query prevention through eager loading and batch operations
- Performance optimization through connection pooling and native queries where needed
- 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:
- Validation Errors - Input data doesn't meet business rules (e.g., invalid JSON, missing fields)
- Domain Errors - Business logic violations during rule evaluation (e.g., unsupported operator, invalid condition)
- 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
RuleEngineExceptionwithVALIDATIONtype - 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 Code | Severity | Alert Threshold | Description |
|---|---|---|---|
| RULE_EVALUATION_FAILED | CRITICAL | >5 occurrences/minute | Infrastructure issues |
| INVALID_RULE_JSON | WARNING | >10 occurrences/minute | Input validation |
| UNSUPPORTED_OPERATOR | WARNING | >5 occurrences/minute | Business 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
- Retry with exponential backoff for transient database/cache issues
- Fallback to default evaluation when rule engine is unavailable
- Graceful degradation - partial rule evaluation with warnings for failed rules
- 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:
-
Rule Evaluation Span
- Operation name:
rule_evaluation - Attributes:
rule.id: "rule-123"rule.type: "business_rule"input.size.bytes: 1024evaluation.duration.ms: 150evaluation.result: "passed|failed|error"
- Operation name:
-
Rule Compilation Span
- Operation name:
rule_compilation - Attributes:
rule.id: "rule-123"compiler.version: "v1.2.0"compilation.duration.ms: 50
- Operation name:
-
Rule Validation Span
- Operation name:
rule_validation - Attributes:
rule.id: "rule-123"validation.type: "syntax|semantic"validation.duration.ms: 20
- Operation name:
Metrics Design
RED Method Metrics (Rate, Errors, Duration)
| Metric Name | Type | Labels | Description |
|---|---|---|---|
rule_evaluations_total | Counter | rule_id, status, error_code | Total number of rule evaluations |
rule_evaluations_errors_total | Counter | rule_id, error_type | Errors during rule evaluation |
rule_evaluation_duration_seconds | Histogram | rule_id, status | Time taken to evaluate rules |
rule_compilation_duration_seconds | Histogram | rule_id, status | Time taken to compile rules |
rule_validation_duration_seconds | Histogram | rule_id, status | Time taken to validate rules |
active_rules_count | Gauge | rule_type | Number of active rules in system |
rule_engine_queue_size | Gauge | queue_type | Size of rule evaluation queues |
Additional Metrics:
rule_engine_memory_usage_bytes(Gauge): Memory usage by enginerule_engine_cpu_percent(Gauge): CPU utilization of engine processrule_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
| Metric | Warning Level | Critical Level | Escalation Path |
|---|---|---|---|
rule_evaluations_errors_total | > 5 errors/minute | > 20 errors/minute | On-call engineer |
rule_evaluation_duration_seconds (p95) | > 200ms | > 500ms | SRE team |
rule_engine_queue_size | > 100 items | > 500 items | SRE team |
rule_engine_error_rate | > 1% | > 5% | SRE team |
active_rules_count | < 100 rules | < 50 rules | SRE team |
rule_engine_memory_usage_bytes | > 80% of limit | > 95% of limit | SRE 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.