Feature Flags Starter
A tech starter that provides feature flag evaluation through the OpenFeature specification, with Flipt as the default provider.
What are Feature Flags?
Feature flags (also called feature toggles) are a technique to enable or disable features at runtime without deploying new code. They decouple deployment from release, allowing teams to ship code continuously while controlling which features are visible to users.
Use Cases:
- Gradual rollout of new features to a subset of users
- A/B testing and experimentation
- Kill switches for unstable features in production
- Environment-specific feature availability
Architecture Overview
This starter abstracts feature flag evaluation behind a simple FeatureFlags interface. Business code never interacts with OpenFeature or the underlying provider directly.
Evaluation Flow
Component Architecture
- Business services depend only on the
FeatureFlagsinterface - The internal implementation translates calls to OpenFeature SDK
- OpenFeature SDK delegates to the configured provider (Flipt)
- Flipt evaluates the flag and returns the result
- Non-technical users manage flags through the Flipt Web UI
Key Features
Evaluation with User Context
Pass user information to enable per-user targeting, percentage rollouts, and A/B tests:
val context = FeatureFlagContext(
userId = currentUser.id,
sessionId = session.id,
email = currentUser.email,
attributes = mapOf("plan" to "premium"),
)
if (featureFlags.isEnabled("new-checkout", context)) {
// new checkout flow
}User & Session Shortcuts
Avoid constructing a full FeatureFlagContext when you only need to target by user or session:
if (featureFlags.isEnabledForUser("beta-feature", userId)) {
// user-targeted rollout
}
if (featureFlags.isDisabledForSession("maintenance-mode", sessionId)) {
// session-scoped check
}Automatic Context Injection via Enrichers
Instead of manually passing a FeatureFlagContext to every call, register one or more FeatureFlagContextEnricher beans. The starter collects them all and injects their combined output before every evaluation via an OpenFeature before-hook. Errors in one enricher never affect the others.
Example: Spring Security enricher (in the consuming application, not the starter):
@Bean
fun securityEnricher(): FeatureFlagContextEnricher = FeatureFlagContextEnricher {
val auth = SecurityContextHolder.getContext().authentication
val principal = auth?.principal as? InternalUserDetails
FeatureFlagContext(
userId = principal?.id,
email = principal?.username,
admin = auth?.authorities?.any { it.authority == "ROLE_ADMIN" } == true,
)
}
@Bean
fun httpRequestEnricher(request: HttpServletRequest): FeatureFlagContextEnricher = FeatureFlagContextEnricher {
FeatureFlagContext(
sessionId = request.session?.id,
correlationId = request.getHeader("X-Correlation-ID"),
)
}Now isEnabled("flag") automatically evaluates with both enrichers' context merged in:
// No manual context construction — Flipt receives userId, email, admin, sessionId, correlationId
if (featureFlags.isEnabled("beta-feature")) { ... }Health Indicator (Actuator)
When Spring Boot Actuator is on the classpath, the starter exposes the Flipt provider state at /actuator/health:
{
"components": {
"featureFlags": {
"status": "UP",
"details": {
"provider": "flipt",
"state": "READY"
}
}
}
}Provider-Agnostic
The starter uses the OpenFeature specification. Switching from Flipt to another provider (LaunchDarkly, Flagsmith, etc.) only requires changing the internal configuration, not the business code.
Usage
1. Add Dependency
dependencies {
implementation(project(":feature-flags-starter"))
}2. Use from Business Code
With a FeatureFlagContextProvider bean, context is injected automatically:
@Service
class CheckoutService(private val featureFlags: FeatureFlags) {
fun checkout(cart: Cart): CheckoutResult {
// context (userId, email, admin, sessionId) is auto-injected
return if (featureFlags.isEnabled("new-checkout-flow")) {
newCheckoutFlow(cart)
} else {
legacyCheckoutFlow(cart)
}
}
}You can still pass explicit context when needed (e.g., evaluating for a different user):
featureFlags.isEnabled("feature-x", FeatureFlagContext(userId = otherUserId))3. Use in Tests (testFixtures)
MockFeatureFlags provides an in-memory store with state inspection and evaluation tracking:
class CheckoutServiceTest {
private val featureFlags = MockFeatureFlags()
private val service = CheckoutService(featureFlags)
@Test
fun `uses new checkout when flag is enabled`() {
featureFlags.enableFlag("new-checkout-flow")
val result = service.checkout("user-1", cart)
result.flow shouldBe "new"
featureFlags.wasEvaluated("new-checkout-flow") shouldBe true
}
@Test
fun `falls back to legacy checkout when flag is disabled`() {
// flags default to disabled — no configuration needed
val result = service.checkout("user-1", cart)
result.flow shouldBe "legacy"
}
@Test
fun `inspect mock state`() {
featureFlags.enableFlag("feature-a")
featureFlags.disableFlag("feature-b")
featureFlags.enabledFlags() shouldBe setOf("feature-a")
featureFlags.disabledFlags() shouldBe setOf("feature-b")
service.checkout("user-1", cart)
featureFlags.evaluationCount() shouldBe 1
featureFlags.lastEvaluation()?.flag shouldBe "new-checkout-flow"
}
}API Reference
Feature Flags
Class: com.drinkit.featureflags.FeatureFlags
Methods (4)
isDisabled()isEnabled()isEnabled()isEnabledForUser()
Generated automatically from code annotations