The Feature Flag Evaluator is a modern, high-performance rules engine designed to determine whether specific feature flags are enabled or disabled for given target audiences. Built with Spring Boot 3.5.x and leveraging Camunda Platform 7.24.0, the system provides two distinct pathways: a sub-millisecond, low-latency API evaluation mechanism for real-time application requests, and an interactive BPMN workflow simulation path for workflow modeling, compliance, operator audit, and human-in-the-loop operational checks.
The application is structured into clearly bounded layers. Communication between the user browser, the Spring core container, the Camunda embedded workflow engine, and the persistent storage database is mapped in the diagram below:
graph TB
subgraph Client Layer
Browser[Web Browser / API Client]
Modeler[Camunda Modeler]
end
subgraph Flag Evaluator Spring Boot Application Boundary
subgraph REST API Controller Layer
FC[FlagController]
JerseyREST[Camunda Jersey REST Servlet]
end
subgraph Spring Managed Business Context
RulesService[RulesService <br> - Reads YAML Feature Config <br> - Implements Rule Evaluation]
Cache[FeatureDetailService <br> - Concurrent In-Memory Cache <br> - Thread-Safe Map]
Jackson[JacksonConfig <br> - Custom Serialization Mapping]
end
subgraph Embedded Camunda 7.24 Engine Layer
Runtime[RuntimeService <br> - Starts process instances]
TaskService[TaskService <br> - Manages User Tasks & Claims]
JobExec[SpringJobExecutor <br> - Handles asyncBefore/asyncAfter background jobs <br> - Thread Pool: 3-10 workers]
Deployer[BPMN Resource Deployer <br> - Deploys classpath:processes/*.bpmn]
end
subgraph Data & Persistence Infrastructure Layer
Hikari[HikariDataSource <br> - Connection Pool]
JPA[Hibernate JPA / Hibernate ORM]
end
end
subgraph Database Storage Layer
H2[(H2 Local File DB <br> camunda-h2-database)]
end
%% Network & Interface Protocols
Browser -->|HTTP REST / JSON / api/eval| FC
Browser -->|HTTP REST / JSON / api/executetask| FC
Browser -->|HTTP REST / HTML / Web Console| JerseyREST
Modeler -->|BPMN 2.0 XML File Write| Deployer
%% Engine & Service Wiring
FC -->|Java API: Query Cache| Cache
FC -->|Java API: Trigger Process| Runtime
Runtime -->|Service Delegation / JavaDelegate| RulesService
RulesService -->|Cache Writes| Cache
TaskService -->|Context Variables Read/Write| Hikari
JobExec -->|Async Job Thread| Runtime
%% Storage connections
Hikari -->|JDBC Connections| H2
JPA -->|ORM Transactions| Hikari
Deployer -->|Saves Deployed XML Definitions| JPA
Runtime -->|Persists Token State| JPA
The backend is built as a lightweight, reactive-friendly monolithic Spring Boot application, optimized for execution under Java 25. It incorporates modern language paradigms such as Module Import Declarations (import module java.base;) and Java Records to achieve optimal type safety and readable, concise code structures.
jdbc:h2:file:./camunda-h2-database) net.ironoc.rules.engine
├── ApiApplication.java # Main Application Bootstrapper
├── config
│ └── JacksonConfig.java # Customized JSON Serializers & ObjectMapper
├── controller
│ └── FlagController.java # Inbound REST API Endpoint Handler
├── service
│ ├── DetailCacheI.java # In-Memory Cache Interface
│ ├── FeatureDetailService.java # Concurrent In-Memory Cache Implementation
│ ├── RuleServiceI.java # Core Rules Engine Evaluation Interface
│ └── RulesService.java # Bind Configuration & Matches Rules Logic
├── dto
│ ├── Feature.java # Feature Flag DTO Record
│ ├── RuleGroups.java # All & Any Rules Record
│ └── Rule.java # Individual Rule Condition Record
└── enums
├── FeatureFlag.java # Enumerated Evaluated Attributes (TIER, APPVERSION, COUNTRY)
├── RuleOperator.java # Evaluation Operators (IN, EQ, GTE, GT)
├── RuleGroup.java # Logic Groups (ALL, ANY)
└── Country.java # Region Enums
Exposes three distinct endpoints:
GET /api/test: Health verification endpoint that maps the standard supported countries (Country enum).GET /api/executetask: Manually fires up a Rules_matcher Camunda process instance. It registers current request inputs, loads initial rule sets into the execution context, and returns the unique processInstanceId.GET /api/eval: The high-performance direct flag evaluation path. It consumes request parameters (feature, country, appVersion, tier), checks the configuration in FeatureDetailService, executes the matching algorithm, and returns 200 OK with the array of matched rules, or a 400 Bad Request with an empty list if rules do not pass.Implements DetailCacheI using a ConcurrentHashMap. To eliminate slow external configurations or DB reads during flag evaluation, this cache holds pre-compiled feature and rule records in memory.
The brain of the evaluation system. It handles two jobs:
execute method): Programmatically binds the prefix feature configurations in application.yml directly into rich Java record representations (Feature, RuleGroups, Rule) using Spring’s Binder API.rulesMatcher method): Matches incoming client parameters against target rule attributes (FeatureFlag attributes) utilizing targeted criteria operators. Supported operations include:
IN & EQ: Validates exact string containment/equality (e.g., verifying a country is in [ES, PT] or the user’s tier matches gold).GTE & GT: Evaluates numerical strings (e.g., validating the user’s appVersion is greater than or equal to 120).By relying on Java 16+ records (Feature, RuleGroups, Rule), the application guarantees structural immutability, thread safety, and standard serialization behaviors with minimal boilerplate.
The system supports automatic bootstrapping of rules on system boot, alongside multiple operational pathways for manual execution, reloads, and configurations.
┌────────────────────────────────────────────────────────────────────────────────────────────┐
│ BOOTSTRAPPING & TRIGGERING PATHWAYS │
│ │
│ [Startup Lifecycle] │
│ │ │
│ ▼ │
│ (PostDeployEvent) ──> [Auto-Trigger Rules_matcher Process] ──> [Memory Cache Loaded] │
│ │
│ │
│ [Operational Inputs] │
│ │ │
│ ├─► (REST Endpoint GET /api/executetask) ────────┐ │
│ │ ├─► [Trigger Rules_matcher] │
│ ├─► (Camunda REST API: /process-definition/...) ─┘ │
│ │ │
│ └─► (Camunda Tasklist UI) ─────────────────────────► [Interactively Run Process] │
└────────────────────────────────────────────────────────────────────────────────────────────┘
To ensure that rule evaluations are ready for immediate consumption upon application launch, the system implements an automated deployment-event listener:
src/main/resources/processes/ (such as rules-matcher.bpmn).ApiApplication.java, a Spring-managed event listener intercepts the deployment phase using the @EventListener annotation bound to Camunda’s PostDeployEvent:
@EventListener
public void processPostDeploy(PostDeployEvent event) {
runtimeService.startProcessInstanceByKey("Rules_matcher");
}
PostDeployEvent, Camunda starts an instance of the Rules_matcher process in the background.rules-init (linked to RulesService). This service task executes RulesService.execute(...), programmatically binds the rules prefix feature configurations directly from application.yml, and populates the FeatureDetailService in-memory concurrency cache.Activity_UserConfirmInit (“Review rules-init output”), parking a token there for manual operator validation in the Camunda Tasklist, concluding the startup bootstrap safely.When configurations change in real-time, or operators need to run manual verification loops, they can trigger rule loading through three pathways:
GET /api/executetask)Designed for testing, programmatic refreshes, and quick re-trigger loops:
http://localhost:8080/api/executetask targets the FlagController.RuntimeService to start a new instance of the process key "Rules_matcher" and retrieves process variables synchronously upon execution:
ProcessInstanceWithVariables result = runtimeService
.createProcessInstanceByKey("Rules_matcher")
.executeWithVariablesInReturn();
rules-init delegate, rebuilding the concurrent map cache with updated environment values, and registers a new tracking token in the engine.Enterprise orchestration platforms or external CD pipelines can manually trigger a rule execution flow using Camunda’s native REST interface:
POST http://localhost:8080/engine-rest/process-definition/key/Rules_matcher/startapplication/json{
"variables": {
"feature": { "value": "new-checkout", "type": "String" },
"country": { "value": "ES", "type": "String" },
"appVersion": { "value": "130", "type": "String" },
"tier": { "value": "gold", "type": "String" }
}
}
Operators can visually trigger manual execution flows with custom, interactive values:
http://localhost:8080/app/tasklist/.When manual testing or operations personnel execute, interact with, or audit the Rules_matcher workflow process, they navigate through a structured graphical interface. This end-to-end user experience flow across the Camunda Tasklist and Cockpit dashboards is detailed in the state chart below:
stateDiagram-v2
[*] --> Operator_Login : Access http://localhost:8080/
state Operator_Login {
[*] --> Enter_Credentials : sa / passw
Enter_Credentials --> Dashboard_Redirect : Click Login
}
Dashboard_Redirect --> Camunda_Tasklist : Select Tasklist App
Dashboard_Redirect --> Camunda_Cockpit : Select Cockpit App
state Camunda_Tasklist {
[*] --> Start_Process_Modal : Click "Start process"
Start_Process_Modal --> Select_Rules_Matcher : Select "Rules Matcher Workflow"
Select_Rules_Matcher --> Input_Start_Variables : (Optional) Input "feature", "country", etc.
Input_Start_Variables --> Process_Started : Click "Start"
state Task_Lifecycle_Phase_1 {
Process_Started --> Fetch_Pending_Tasks : Apply "All tasks" Filter
Fetch_Pending_Tasks --> Claim_Init_Task : Select "Review rules-init output"
Claim_Init_Task --> View_Feature_Details : Inspect Process Variables
View_Feature_Details --> Complete_Init_Task : Click "Complete"
}
state Task_Lifecycle_Phase_2 {
Complete_Init_Task --> Token_Routing_In_Engine : (System routes via Gateways & Delegates)
Token_Routing_In_Engine --> Fetch_New_Review_Task : Poll / Refresh list
Fetch_New_Review_Task --> Claim_Review_Task : Select "Review matched rules JSON"
Claim_Review_Task --> Inspect_rulesJson : View matched rules in serialized output
Inspect_rulesJson --> Complete_Review_Task : Click "Complete"
}
Complete_Review_Task --> Process_Completed : Process Instance terminates
}
state Camunda_Cockpit {
[*] --> Navigate_Process_Definitions : Click "Processes"
Navigate_Process_Definitions --> Select_Rules_Matcher_Def : Click "Rules_matcher"
Select_Rules_Matcher_Def --> Inspect_Live_Tokens : View heat map / token locations
Inspect_Live_Tokens --> Inspect_Variables : View runtime process variable values
Inspect_Live_Tokens --> Inspect_Historic_Audit : Track completed delegates & paths taken
}
The system relies on executable Business Process Model and Notation (BPMN 2.0) specifications deployed to the embedded Camunda engine.
The application deploys three separate workflows on boot:
rules-init (Process ID rules-init via first.bpmn): Fully automated startup helper process. Triggered automatically or via internal hooks, it executes a service task linked directly to RulesService.execute(...) to read from physical files (application.yml), perform binding, and load target configurations directly into the concurrent cache map.Rules_matcher (Process ID Rules_matcher via rules-matcher.bpmn): The comprehensive execution workflow implementing conditional logical routing, custom java delegate evaluations, async state persistence boundaries, and interactive user checkpoint nodes.loanApproval (Process ID loanApproval via loanApproval.bpmn): A lightweight demo user-task assignment workflow.The Rules_matcher workflow incorporates specific structures to govern execution based on live evaluation states:
graph TD
Start([Start Event]) --> Init[rules-init <br> RulesService]
Init --> UserConfirm[User Task: Review rules-init output <br> Pause for Tasklist]
UserConfirm --> CheckEnabled[Enabled <br> FeatureEnabledDelegate]
CheckEnabled --> Gateway{Feature Enabled?}
%% Enabled branch
Gateway -->|Yes / featureEnabled| AppVer[Application Version Supported? <br> AppVersionDelegate]
AppVer --> Tier[Tier Valid for User? <br> TierDelegate]
Tier --> Country[Country Supported? <br> CountryDelegate]
Country --> Merge[Gateway: MergeBeforeAggregator]
%% Disabled branch
Gateway -->|No / !featureEnabled| Disabled[Disabled <br> FeatureDisabledDelegate]
Disabled --> Pass[Empty Rule Set <br> PassEngineDelegate]
Pass --> Merge
Merge --> Aggregator[Return Rules Set <br> RulesAggregatorDelegate]
Aggregator --> UserReview[User Task: Review matched rules JSON <br> Pause for Tasklist]
UserReview --> End([End Event])
Gateway_0ejhppi and Gateway_MergeBeforeAggregator)Gateway_0ejhppi): Acts as a deterministic fork. It evaluates the process variable ${featureEnabled} populated by FeatureEnabledDelegate. If true, it diverts the token down the evaluation path. If false, it redirects the token to the cleanup/disabled path.Gateway_MergeBeforeAggregator): Serves as an un-synchronized convergence node. Whether the process resolved rules or skipped them entirely, both paths converge at this merge node prior to triggering rule aggregation.userTask)BPMN user tasks represent operational safety checkpoints:
Activity_UserConfirmInit (“Review rules-init output”): Positioned immediately after rule loading. The process stops and presents the task in the Camunda Tasklist. This ensures that operators verify that features and rule definitions are properly loaded from YAML configurations before actual criteria matching starts.Activity_UserReviewRulesJson (“Review matched rules JSON”): Positioned immediately after aggregate evaluation. This blocks completion until an operator claims and completes the task in the Camunda Tasklist. This acts as a manual audit boundary to inspect the serialized rulesJson result.camunda:async)The Service Task Activity_1pygerh (“Return Rules Set”) is configured with camunda:asyncBefore="true" and camunda:asyncAfter="true":
asyncBefore=true: Before entering the delegate, the engine commits the current database transaction. The execution thread is released back to the caller (e.g. the HTTP request), and a background job is scheduled. The Camunda Job Executor picks up the task and run RulesAggregatorDelegate in a background worker thread.asyncAfter=true: Immediately after the delegate completes its work, the engine commits the state variables and saves the updated matched rules back to the database, ensuring zero data loss before transitioning to the subsequent user task checkpoint.Each service node in the Rules_matcher process is backed by a specific Java class implementing org.camunda.bpm.engine.delegate.JavaDelegate. These classes orchestrate process state transitions by reading, updating, and removing execution variables.
Below is an exhaustive account of each delegate’s responsibilities, input/output variables, and internal execution logic:
┌────────────────────────────────────────────────────────────────────────────────────────────┐
│ RULES_MATCHER PROCESS │
│ │
│ [Start] ──> [rules-init] │
│ │ │
│ ▼ │
│ (Task: Review rules-init) │
│ │ │
│ ▼ │
│ [FeatureEnabled] ─────────────────────────────────────────┐ │
│ │ │ │
│ (featureEnabled == true) (featureEnabled == false) │
│ │ │ │
│ ▼ ▼ │
│ [AppVersion] [FeatureDisabled] │
│ │ │ │
│ ▼ ▼ │
│ [Tier] [PassEngine] │
│ │ │ │
│ ▼ │ │
│ [Country] │ │
│ │ │ │
│ └─────────────────────────► ◄──────────────────────┘ │
│ │ │
│ ▼ │
│ [RulesAggregator] │
│ │ │
│ ▼ │
│ (Task: Review rules JSON) │
│ │ │
│ ▼ │
│ [End] │
└────────────────────────────────────────────────────────────────────────────────────────────┘
FeatureEnabledDelegatenet.ironoc.rules.engine.delegate.FeatureEnabledDelegatefeature (String): The ID of the feature flag to evaluate.featureEnabled (Boolean): Flag representing whether the requested feature is active.featureDto (Feature - Java Record): The fully populated Feature object containing logical groups.ruleGroupsAll (Map<String, Map<String, Object»): Rule sets that must pass AND conditions (mapped from feature.ruleGroups().all()).ruleGroupsAny (Map<String, Map<String, Object»): Rule sets that must pass OR conditions (mapped from feature.ruleGroups().any()).featureDto, ruleGroupsAll, and ruleGroupsAny from the execution context to prevent stale configuration pollution."feature". If null, defaults to empty.featureDetailsService.getFeaturesById()) using the feature ID.feature.enabled() is true.execution.setVariable("featureEnabled", enabled).enabled is true, extracts the underlying logical ruleGroups configurations and registers them as serialized process variables ("featureDto", "ruleGroupsAll", "ruleGroupsAny") so downstream delegates can access them.disabled or missing, calls execution.removeVariable(...) for all feature-specific parameters and logs the cleanup.FeatureDisabledDelegatenet.ironoc.rules.engine.delegate.FeatureDisabledDelegatefeatureEnabled (Boolean): Overridden to false.featureDto, ruleGroupsAll, and ruleGroupsAny from the execution scope.execution.setVariable("featureEnabled", false) to ensure that any conflicting upstream evaluation is overridden."featureDto", "ruleGroupsAll", and "ruleGroupsAny" to prevent evaluation.PassEngineDelegatenet.ironoc.rules.engine.delegate.PassEngineDelegateskipRulesEngine (Boolean): Set to true.matchedRules (List"skipRulesEngine" to true to signal downstream aggregators that rule matching should be skipped.ArrayList<Rule> and binds it to the process variable "matchedRules".AppVersionDelegatenet.ironoc.rules.engine.delegate.AppVersionDelegateappVersion (String): Raw client-submitted application version."appVersion".TierDelegatenet.ironoc.rules.engine.delegate.TierDelegatetier (String): Raw client-submitted subscription tier."tier".CountryDelegatenet.ironoc.rules.engine.delegate.CountryDelegatecountry (String): Raw client-submitted country code."country".RulesAggregatorDelegatenet.ironoc.rules.engine.delegate.RulesAggregatorDelegateskipRulesEngine (Boolean): Check to skip criteria matching.featureEnabled (Boolean): Check if feature is active.country (String): Client’s country code.appVersion (String): Client’s application version.tier (String): Client’s subscription tier.feature (String): Target feature ID.matchedRules (ListrulesJson (String): JSON serialized string containing matching rules, rendered directly in Camunda Tasklist."skipRulesEngine" and "featureEnabled".skipRulesEngine is true or featureEnabled is false, sets matchedRules to an empty ArrayList<Rule>().country, appVersion, tier, and featureId.rulesService.getRuleMatchByRuleGroup(...) for logical group ALL (representing criteria that must all match / AND).rulesService.getRuleMatchByRuleGroup(...) for logical group ANY (representing criteria where at least one must match / OR).rulesService.createResponseFromMatches(...)."matchedRules".objectMapper."rulesJson".The integration of Camunda provides a robust framework to visualize, monitor, audit, and walk through business rules interactively.
Architects, product owners, and developers use the Camunda Modeler to edit .bpmn files. Modeler features utilized in this codebase include:
camunda:class="net.ironoc.rules.engine.delegate.FeatureEnabledDelegate").${featureEnabled} vs ${!featureEnabled}).Review rules-init output or Review matched rules JSON) to allow administrators to examine intermediate results via the web UI.camunda:asyncBefore="true" or camunda:asyncAfter="true". This instructs the engine to commit the current transaction to the database, allowing background job executors to handle execution, preventing long-running operations from blocking HTTP request threads.Upon booting the application, the Camunda Platform Cockpit, Tasklist, and Admin interfaces are hosted at http://localhost:8080/ (Admin credentials default to: sa / passw).
Provides an overview of running processes. Operators use it to:
country or appVersion during live execution).The interactive interface for operations teams. Because the Rules_matcher workflow incorporates User Tasks, executing a workflow creates a task entry here.
rulesJson before completing the process.Controls user authentication, authorizations, and filter creation (e.g., configures the “All tasks” filter used to display pending user tasks in the Tasklist).
The Feature Flag Evaluator supports two distinct execution paths depending on performance and audit requirements:
/api/eval)Designed for live production traffic requiring sub-millisecond responses.
sequenceDiagram
autonumber
actor Client as API Caller
participant Controller as FlagController
participant Cache as FeatureDetailService (In-Memory Cache)
participant Service as RulesService
Client->>Controller: GET /api/eval (feature, country, appVersion, tier)
Controller->>Cache: getFeaturesById()
Cache-->>Controller: Feature DTO (enabled, ruleGroups)
alt Feature not found or disabled
Controller-->>Client: 400 Bad Request (empty list)
else Feature is enabled and has ruleGroups
Controller->>Service: getRuleMatchByRuleGroup(RuleGroup.ALL)
Service->>Service: Evaluate GTE, GT, IN, EQ conditions
Service-->>Controller: allRuleMatch List
Controller->>Service: getRuleMatchByRuleGroup(RuleGroup.ANY)
Service->>Service: Evaluate GTE, GT, IN, EQ conditions
Service-->>Controller: anyRuleMatch List
Controller->>Service: createResponseFromMatches(allRuleMatch, anyRuleMatch)
Service-->>Controller: ResponseEntity<ApiResponse>
alt Matches found
Controller-->>Client: 200 OK (ApiResponse with matched Rules JSON)
else No matches
Controller-->>Client: 400 Bad Request (empty list)
end
end
/api/executetask)Designed for process tracing, visual auditing, and manual user checkpoints.
GET /api/executetask. The FlagController starts the Rules_matcher BPMN process via the Camunda Runtime Service.rules-init Service Task): Executes RulesService. It binds rule specifications from application.yml and updates FeatureDetailService.Review rules-init output User Task): The process pauses. The operator claims and completes the task in Camunda Tasklist.Enabled Service Task): Runs FeatureEnabledDelegate. It extracts the process variable feature. It checks the cache, sets featureEnabled to true or false, and pushes rule configurations into process variables (ruleGroupsAll, ruleGroupsAny).Feature Enabled? Exclusive Gateway):
AppVersionDelegate: Captures and logs appVersion.TierDelegate: Captures and logs tier.CountryDelegate: Captures and logs country.FeatureDisabledDelegate: Formally overrides featureEnabled to false and clears remaining cache variables.PassEngineDelegate: Signals that rules should be skipped (skipRulesEngine=true) and initializes an empty matched rules array.Return Rules Set Service Task): Merges both flows and routes to RulesAggregatorDelegate.
country, appVersion, tier, feature), uses RulesService to match criteria against rule attributes, aggregates all valid rules, saves them to matchedRules, and serializes the list to a process variable string rulesJson.Review matched rules JSON User Task): Pauses the process. An operator reviews the compiled rules in rulesJson inside the Tasklist. Once approved, the task is marked complete, and the instance finishes.The system guarantees robust operations via its comprehensive, self-contained test suite containing 9 distinct unit and integration tests distributed across these target segments:
JacksonConfigTest: Validates customized JSON serialization configurations, ensuring records and complex maps serialize smoothly.ContextLoadsTest: Boots up the full Spring Application Context, verifies Hibernate mappings, initializes the Hikari Connection Pool to the local H2 file database, and checks that Camunda BPMN processes (rules-matcher.bpmn, first.bpmn, loanApproval.bpmn) deploy cleanly.RulesServiceTest (Unit Tests): Tests the parsing engine logic in isolation. It verifies:
IN).GTE, GT).FlagControllerTest (Controller Mock Tests): Exercises flag-evaluation REST endpoints directly, validating appropriate HTTP response codes (200 OK vs 400 Bad Request) for diverse execution scenarios (e.g., missing features, disabled flags, or composite rule criteria matches across both ALL and ANY groups).This comprehensive architecture maintains highly performant runtime capabilities alongside rigorous operational oversight, meeting both enterprise-grade API performance demands and corporate compliance goals.