Skip to content

vimalkumartech/feature-flags

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

feature-flags-service

Backend-driven feature flag service built with Spring Boot 4 and Java 25.

Overview

Exposes a single endpoint that evaluates a set of feature flags for the authenticated customer and returns which features are available to them. Flag configuration lives in application.yaml and is overridden at runtime by Azure Key Vault secrets.

Architecture

feature-flags-api.yaml (OpenAPI spec)
        │
        ▼  ./gradlew openApiGenerate
FeatureFlagsApi.java    ←── implemented by ──→  FeatureFlagController.java
FeatureFlagsResponse.java  ◄── assembled by ──  FeatureFlagEvaluationService.java
FeatureFlag.java
FeatureName.java (enum — CUSTOMER_DIALOGUE_CHAT, VOICE_SUPPORT_TOKEN, ONBOARDING_FLOW)
        ▲
        │  reads
FeatureFlagProperties.java  ←── bound from ──  application.yaml / Key Vault

FeatureFlagEvaluationService fails fast at startup if application.yaml doesn't have a config entry for every value in the generated FeatureName enum — the response always contains exactly those three flags, never more or fewer.

The OpenAPI spec is the contract source of truth. DTOs and the controller interface are generated from it — never edit build/generated/ by hand. When the contract changes, update the spec and re-run ./gradlew openApiGenerate; the compiler will flag any breaking changes in the controller or service immediately.

Prerequisites

  • Java 25
  • Gradle (wrapper included — no local install needed)

Running locally

./gradlew bootRun

The server starts on port 8080.

Override any flag at startup via environment variables:

FEATURE_FLAG_CUSTOMER_DIALOGUE_CHAT_ENABLED=true ./gradlew bootRun

API

GET /api/v1/feature-flags

Returns the evaluated feature set for the calling customer. Takes no parameters — it's not part of the OpenAPI contract, but internally the controller still reads the following request headers to build the customer context (MVP stub — will come from auth token / SecurityContext in production):

Header Description
X-Customer-SSN Customer SSN for allowlist targeting.
X-App-Version Client app version in semver format (e.g. 2.1.0).
X-Platform Client platform: ios, android, or web. Reserved for platform check (next version).

Response 200 OK

{
  "features": [
    { "name": "CUSTOMER_DIALOGUE_CHAT", "enabled": false },
    { "name": "VOICE_SUPPORT_TOKEN",    "enabled": true },
    { "name": "ONBOARDING_FLOW",        "enabled": true }
  ]
}

Response 400 Bad Request — malformed X-App-Version header.

Example

curl http://localhost:8080/api/v1/feature-flags \
  -H "X-Customer-SSN: 199001011234" \
  -H "X-App-Version: 2.0.0" \
  -H "X-Platform: ios"

Flag evaluation logic

Flags are evaluated in this order for each request:

  1. Global switch — if enabled: false, the flag is off for everyone.
  2. Version check — if the client's X-App-Version is below min-app-version, the flag is off.
  3. Allowlist check — if use-allowlist: true, the flag is on only if the customer's SSN appears in allowlist. If use-allowlist: false, the flag is on for all users who passed the previous checks.

Configuration

Flag configuration lives in src/main/resources/application.yaml. Each scalar field accepts an environment variable with a local default:

feature-flags:
  flags:
    customer-dialogue-chat:
      enabled:         ${FEATURE_FLAG_CUSTOMER_DIALOGUE_CHAT_ENABLED:false}
      min-app-version: ${FEATURE_FLAG_CUSTOMER_DIALOGUE_CHAT_MIN_VERSION:1.0.0}
      use-allowlist:   ${FEATURE_FLAG_CUSTOMER_DIALOGUE_CHAT_USE_ALLOWLIST:true}
      allowlist:
        - "199001011234"

    voice-support-token:
      enabled:         ${FEATURE_FLAG_VOICE_SUPPORT_TOKEN_ENABLED:true}
      min-app-version: ${FEATURE_FLAG_VOICE_SUPPORT_TOKEN_MIN_VERSION:1.0.0}
      use-allowlist:   ${FEATURE_FLAG_VOICE_SUPPORT_TOKEN_USE_ALLOWLIST:false}
      allowlist: []

    onboarding-flow:
      enabled:         ${FEATURE_FLAG_ONBOARDING_FLOW_ENABLED:true}
      min-app-version: ${FEATURE_FLAG_ONBOARDING_FLOW_MIN_VERSION:1.0.0}
      use-allowlist:   ${FEATURE_FLAG_ONBOARDING_FLOW_USE_ALLOWLIST:false}
      allowlist: []

Adding a new flag

The flag set is a fixed enum shared with the OpenAPI contract, so the frontend can validate features[].name against a known list rather than an open-ended set of keys. features is an array of { name, enabled }name is $ref'd straight to the FeatureName schema, so adding a flag doesn't require touching the response schema itself. Adding one requires:

  1. Add the new name to the FeatureName enum in openapi/feature-flags-api.yaml.
  2. Add a matching entry under feature-flags.flags in application.yaml (kebab-case of the enum name). FeatureFlagEvaluationService fails fast at startup if the two are out of sync.
  3. Run ./gradlew openApiGenerate. FeatureFlagEvaluationService.evaluate already loops over FeatureName.values(), so no code change is needed there.

Azure Key Vault integration

Flag values are injected by Azure Key Vault via Spring Cloud Azure's property source, which overrides application.yaml values at startup. The ${ENV_VAR:default} placeholders in application.yaml serve as local fallbacks when Key Vault is not configured.

Secret naming convention

Azure Key Vault secret names use double-dash (--) as a property path separator (Key Vault disallows dots and underscores):

Key Vault secret name Spring property path
feature-flags--flags--voice-support-token--enabled feature-flags.flags.voice-support-token.enabled
feature-flags--flags--voice-support-token--min-app-version feature-flags.flags.voice-support-token.min-app-version
feature-flags--flags--voice-support-token--use-allowlist feature-flags.flags.voice-support-token.use-allowlist
feature-flags--flags--voice-support-token--allowlist[0] feature-flags.flags.voice-support-token.allowlist[0]

Enabling Key Vault

Uncomment the Spring Cloud Azure block in build.gradle and set AZURE_KEYVAULT_URI:

export AZURE_KEYVAULT_URI=https://my-vault.vault.azure.net/
./gradlew bootRun

Authentication is handled by DefaultAzureCredential — managed identity in Azure, az login locally.

Note: Spring Cloud Azure 5.x targets Spring Boot 3.x / Spring Framework 6.x. The dependency block in build.gradle is currently commented out pending Spring Boot 4 support. Until then, inject secrets as environment variables via Azure App Service Key Vault References or Kubernetes External Secrets Operator — the ${ENV_VAR:default} placeholders support this with no code changes.

Testing

./gradlew test

Unit tests cover all evaluation branches in FeatureFlagEvaluationServiceTest:

Test Scenario
globally_disabled_flag_is_always_off enabled: false overrides everything
enabled_flag_with_no_restrictions_is_on open flag returns true for any user
allowlisted_flag_is_on_for_allowed_user SSN in allowlist → enabled
allowlisted_flag_is_off_for_non_allowed_user SSN not in allowlist → disabled
allowlisted_flag_is_off_when_ssn_is_null null SSN treated as not allowlisted
version_gated_flag_is_off_when_client_version_too_low version below min → disabled
version_gated_flag_is_on_when_client_version_meets_minimum exact min version → enabled
version_gated_flag_is_on_when_client_version_exceeds_minimum version above min → enabled
missing_app_version_bypasses_version_check null version skips check → enabled
response_always_contains_all_three_flags response always has all three named flags
missing_flag_configuration_throws_illegal_state config missing an enum flag → fails fast at startup
malformed_version_throws_illegal_argument invalid version string → 400

Project structure

src/main/
├── java/com/poc/featureflags/
│   ├── FeatureFlagsApplication.java
│   ├── config/
│   │   ├── FeatureFlagConfig.java       # per-flag settings record
│   │   └── FeatureFlagProperties.java   # @ConfigurationProperties root
│   ├── context/
│   │   └── CustomerContext.java         # customer identity stub
│   ├── controller/
│   │   └── FeatureFlagController.java   # implements generated FeatureFlagsApi
│   ├── exception/
│   │   └── GlobalExceptionHandler.java
│   └── service/
│       └── FeatureFlagEvaluationService.java
└── resources/
    ├── application.yaml
    └── openapi/
        └── feature-flags-api.yaml       # OpenAPI spec (source of truth)

build/generated/openapi/                 # generated — never edit, not committed
└── com/poc/featureflags/
    ├── api/FeatureFlagsApi.java
    └── dto/
        ├── FeatureFlagsResponse.java
        ├── FeatureFlag.java
        └── FeatureName.java

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages