Quarkus Feature Flags
This projects aims to provide a lightweight and extensible feature flag Quarkus extension.
To get started, add the io.quarkiverse.flags:quarkus-flags extension to your build file first.
For instance, with Maven, add the following dependency to your POM file:
<dependency>
<groupId>io.quarkiverse.flags</groupId>
<artifactId>quarkus-flags</artifactId>
<version>{project-version}</version>
</dependency>
| This dependency is not necessary if you use one of the extending modules, such as Qute, Hibernate ORM, Security, etc., as they already depend on the core module. |
More specifically, the extension provides:
-
An API to access feature flags.
-
An SPI to provide flags and externalize the computation of a flag value.
-
Several built-in flag providers
-
Leverage Quarkus config to define feature flags,
-
In-memory repository (useful for testing and dynamic registration).
-
-
Hibernate ORM module, where feature flags are mapped from an annotated entity and are automatically loaded from the database.
-
Hibernate Reactive module, the reactive variant of the Hibernate ORM module.
-
Security module, so that it’s possible to evaluate flags based on the current
SecurityIdentity. -
Qute module, so that it’s possible to use the flags directly in templates.
-
Cron module with a flag evaluator that matches a specific CRON expression.
-
OpenFeature module, an integration with the OpenFeature standard.
What is a feature flag?
A feature flag makes it possible to turn on/off or configure a specific functionality in your application.
It’s also referred to as toggles or switches.
In this extension, a feature flag is represented by the io.quarkiverse.flags.Flag interface.
It refers to a specific feature with Flag#feature() and provides several convenient methods to compute the current value.
The value of a feature flag can be represented as boolean, string, integer or decimal.
A flag can also define metadata with Flag#metadata().
Metadata enable further configuration which can be leveraged in SPI.
Accessing feature flags
The io.quarkiverse.flags.Flags represents a central point to access feature flags.
A CDI bean that implements Flags is automatically registered.
import io.quarkiverse.flags.Flags;
import io.quarkiverse.flags.Feature;
import jakarta.inject.Inject;
@ApplicanScoped
public class MyService {
@Inject
Flags flags; (1)
@Inject
@Feature("my-feature-alpha")
Flag alpha; (2)
@Inject
@Feature
Flag bravo; (3)
void serviceCall1() {
if (flags.isEnabled("my-feature-alpha")) { (4)
// Business logic...
}
}
void serviceCall2() {
if (alpha.isEnabled()) { (5)
// Business logic...
}
}
void serviceCall3() {
if (Flag.get("my-feature-alpha").isEnabled()) { (6)
// Business logic...
}
}
}
| 1 | You can inject Flags in any CDI bean. |
| 2 | You can also inject a Flag for the given feature. The injected flag is never null but subsequent computations can throw NoSuchElementException if no such feature flag exists. |
| 3 | If the @Feature value is not specified then the feature name is derived from the name of the annotated element, i.e. in this case the feature name is bravo. |
| 4 | Flags#isEnabled(String) is a shortcut for findAndAwait(feature).orElseThrow().isEnabled(). |
| 5 | Flag#isEnabled() computes the current value and returns its boolean representation. It blocks the caller thread. |
| 6 | Flag#get(String) is a static convenience method to obtain a flag by feature name. It blocks the caller thread and throws NoSuchElementException if no such feature flag exists. |
Other value types
The value of a feature flag can also be retrieved as a string, integer or decimal.
String name = flags.getString("my-feature-alpha"); (1)
int count = flags.getInt("my-feature-alpha"); (2)
BigDecimal ratio = flags.getDecimal("my-feature-alpha"); (3)
| 1 | Flags#getString(String) is a shortcut for findAndAwait(feature).orElseThrow().getString(). |
| 2 | Flags#getInt(String) is a shortcut for findAndAwait(feature).orElseThrow().getInt(). |
| 3 | Flags#getDecimal(String) is a shortcut for findAndAwait(feature).orElseThrow().getDecimal(). |
The equivalent methods are also available on the Flag interface:
String name = alpha.getString();
int count = alpha.getInt();
BigDecimal ratio = alpha.getDecimal();
Default values
All value accessor methods have overloads that accept a default value. The default value is returned if the flag is not found or if the underlying value cannot be converted to the requested type.
boolean enabled = flags.isEnabled("my-feature-alpha", false); (1)
String name = flags.getString("my-feature-alpha", "default-name"); (2)
int count = flags.getInt("my-feature-alpha", 42); (3)
BigDecimal ratio = flags.getDecimal("my-feature-alpha", BigDecimal.ZERO); (4)
| 1 | Returns false if the flag does not exist or the value cannot be converted to boolean. |
| 2 | Returns "default-name" if the flag does not exist or the value cannot be converted to string. |
| 3 | Returns 42 if the flag does not exist or the value cannot be converted to integer. |
| 4 | Returns BigDecimal.ZERO if the flag does not exist or the value cannot be converted to decimal. |
The equivalent methods are also available on the Flag interface:
boolean enabled = alpha.isEnabled(false); (1)
String name = alpha.getString("default-name"); (2)
int count = alpha.getInt(42); (3)
BigDecimal ratio = alpha.getDecimal(BigDecimal.ZERO); (4)
| 1 | Returns false if the value cannot be converted to boolean. |
| 2 | Returns "default-name" if the value cannot be converted to string. |
| 3 | Returns 42 if the value cannot be converted to integer. |
| 4 | Returns BigDecimal.ZERO if the value cannot be converted to decimal. |
Non-blocking computation
The convenience methods shown above block the caller thread.
If you need to compute the value without blocking, use Flag#compute() which returns a Uni<Flag.Value>.
alpha.compute()
.subscribe().with(value -> {
boolean enabled = value.asBoolean();
String str = value.asString();
int num = value.asInt();
BigDecimal dec = value.asDecimal();
});
If the quarkus-flags-qute module is present, you can access feature flags directly in templates, e.g. {#if flag:enabled('my-feature-alpha')}…{/if}
|
Flags provider SPI
The feature flags are collected at runtime.
More specifically, the extension injects all CDI beans that implement io.quarkiverse.flags.spi.FlagProvider and calls the FlagProvider#getFlags() method to collect all flags and FlagProvider#getFlag(String) to find a flag for a specific feature.
The result of getFlags() must not contain flags with duplicate feature names.
Each FlagProvider bean must be annotated with @io.smallrye.common.annotation.Identifier to define a unique identifier.
If multiple flag providers with the same identifier exist then the application fails to start.
The ordering of providers is defined with the @io.quarkiverse.flags.spi.ComponentOrder annotation.
A provider listed in before is processed after this provider, i.e., this provider takes precedence.
A provider listed in after is processed before this provider, i.e., it takes precedence over this provider.
Both before and after reference provider identifiers.
Cycles in the ordering are detected at build time and result in a deployment error.
By default, getFlag(String) filters the result of getFlags().
Implementations backed by a database or remote service should override this method with an optimized lookup.
The core extension provides two built-in flag providers.
The first one is built on top of Quarkus config service.
It allows users to configure build time and runtime feature flags.
The build time feature flags are configured through properties with prefix quarkus.flags.build and the initial value is fixed at build time.
The runtime feature flags are configured through properties with prefix quarkus.flags.runtime and the initial value is determined when the application starts.
quarkus.flags.build."my-feature-alpha".value=true (1)
quarkus.flags.runtime."my-feature-bravo".value=false (2)
quarkus.flags.runtime."my-feature-bravo".meta.foo=bar (3)
| 1 | Define a build time flag for feature my-feature-alpha with initial value true. |
| 2 | Define a runtime flag for feature my-feature-bravo with value false. |
| 3 | The metadata of my-feature-bravo will contain key "foo" with value "bar". |
The other built-in flag provider provides an in-memory repository that can be useful for testing and dynamic registration.
import io.quarkiverse.flags.InMemoryFlagProvider;
import jakarta.inject.Inject;
@ApplicanScoped
public class MyService {
@Inject
InMemoryFlagProvider provider; (1)
void addFlag() {
provider.addFlag(Flag.builder("my-feature-alpha")
.setEnabled(true)); (2)
}
void removeFlag() {
provider.removeFlag("my-feature-alpha"); (3)
}
}
| 1 | You can inject InMemoryFlagProvider in any CDI bean. |
| 2 | It’s possible to register a new feature flag at any time. |
| 3 | You can also remove the flag. |
Flag evaluator SPI
The io.quarkiverse.flags.spi.FlagEvaluator SPI makes it possible to externalize the computation of the current value of a feature flag.
This allows for more dynamic evaluation logic based on some external state, such as the current SecurityIdentity.
Flag evaluators must be CDI beans annotated with @io.smallrye.common.annotation.Identifier to define a unique identifier.
@Dependent beans are reused.
By default, a Flag can reference one FlagEvaluator in its metadata with a key evaluator.
This evaluator is automatically used to compute the current value for any Flag produced by means of Flag.Builder (i.e. created by Flag#builder(String)).
Time span flag evaluator
The core extension provides a built-in flag evaluator - io.quarkiverse.flags.TimeSpanFlagEvaluator that evaluates a flag based on the current date-time obtained from the system clock in the default time-zone.
quarkus.flags.runtime."my-feature-alpha".value=true
quarkus.flags.runtime."my-feature-alpha".meta.evaluator=quarkus.time-span (1)
quarkus.flags.runtime."my-feature-alpha".meta.start-time=2001-01-01T10:15:30+01:00[Europe/Prague] (2)
| 1 | The TimeSpanFlagEvaluator is used to compute the current value of the feature flag. |
| 2 | The current date-time must be after the specified start-time. The java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME is used to parse the start-time value. |
Composite flag evaluator
Sometimes it might be useful to combine some evaluators to compute the value of a flag.
The core extension provides io.quarkiverse.flags.CompositeFlagEvaluator that evaluates a flag with the specified sub-evaluators.
quarkus.flags.runtime."my-feature-alpha".value=true
quarkus.flags.runtime."my-feature-alpha".meta.evaluator=quarkus.composite (1)
quarkus.flags.runtime."my-feature-alpha".meta.sub-evaluators=quarkus.time-span, quarkus.security.identity (2)
quarkus.flags.runtime."my-feature-alpha".meta.start-time=2026-01-01T12:00:00+01:00[Europe/Prague] (3)
quarkus.flags.runtime."my-feature-alpha".meta.roles-allowed=admin (4)
| 1 | The CompositeFlagEvaluator is used to compute the current value of the feature flag. |
| 2 | The value of sub-evaluators represents a comma-separated list of sub-evaluator identifiers. They are executed in the specified order. In this particular case, first the TimeSpanFlagEvaluator is executed and then the SecurityIdentityFlagEvaluator. |
| 3 | The current date-time must be after the specified start-time. |
| 4 | The current user must have the role admin. |
Variant flag evaluator
The core extension provides io.quarkiverse.flags.VariantFlagEvaluator that selects a value from a set of named variants based on the ComputationContext.
Variants are defined in the flag metadata with the variant- key prefix.
The variant-key metadata specifies which context key is used to select a variant.
The default-variant metadata specifies the fallback variant when the context key is absent or does not match any variant.
quarkus.flags.runtime.checkout.value=Buy Now
quarkus.flags.runtime.checkout.meta.evaluator=quarkus.variant (1)
quarkus.flags.runtime.checkout.meta.variant-key=group (2)
quarkus.flags.runtime.checkout.meta.default-variant=control (3)
quarkus.flags.runtime.checkout.meta.variant-control=Buy Now (4)
quarkus.flags.runtime.checkout.meta.variant-treatment=Add to Cart
quarkus.flags.runtime.checkout.meta.variant-holiday=Gift This!
| 1 | The VariantFlagEvaluator is used to compute the current value of the feature flag. |
| 2 | The value of variant-key specifies the context key used to select a variant. In this case, ctx.get("group") is used. |
| 3 | The default-variant is used when the context key is absent or does not match any defined variant. |
| 4 | Each variant-<name> metadata entry defines a named variant with its value. |
Flag checkout = flags.findAndAwait("checkout").orElseThrow();
// No context - returns the default variant "control" -> "Buy Now"
checkout.getString();
// Context selects the "treatment" variant -> "Add to Cart"
checkout.computeAndAwait(ComputationContext.of("group", "treatment")).asString();
The VariantFlagEvaluator can be combined with other evaluators using the CompositeFlagEvaluator.
For example, variants can be selected first and then further processed by another evaluator:
quarkus.flags.runtime.greeting.value=hello
quarkus.flags.runtime.greeting.meta.evaluator=quarkus.composite
quarkus.flags.runtime.greeting.meta.sub-evaluators=quarkus.variant, my-custom-evaluator
quarkus.flags.runtime.greeting.meta.variant-key=locale
quarkus.flags.runtime.greeting.meta.default-variant=en
quarkus.flags.runtime.greeting.meta.variant-en=Hello
quarkus.flags.runtime.greeting.meta.variant-es=Hola
Caching
Some flag providers may involve long running operations.
In that case, caching of flags might be useful.
Let’s use the quarkus-cache extension and CDI to implement a simple caching.
First, you need to add the io.quarkus:quarkus-cache extension to your project.
Then create a simple CDI decorator like this:
@Priority(1)
@Decorator
public class CachingDecorator implements FlagProvider {
@Inject
@Any
@Delegate
FlagProvider delegate;
@Inject
@Decorated
Bean<FlagProvider> decoratedBean; (1)
@CacheName("quarkus-flags-cache") (2)
Cache cache;
@Override
public Uni<Collection<Flag>> getFlags() {
String cacheKey = decoratedBean.getQualifiers().stream()
.filter(q -> q instanceof Identifier)
.map(q -> ((Identifier) q).value())
.findFirst()
.orElse(decoratedBean.getBeanClass().getName()); (3)
return cache.getAsync(cacheKey, k -> {
return delegate.getFlags().memoize().indefinitely(); (4)
});
}
}
| 1 | The @Decorated injection point gives access to the metadata of the decorated bean, including its qualifiers. |
| 2 | Inject the cache named quarkus-flags-cache. You can configure the cache based on the cache implementation used. For example, quarkus.cache.caffeine."quarkus-flags-cache".expire-after-write=10m. See https://quarkus.io/guides/cache for more information. |
| 3 | The @Identifier value of the decorated provider is used as the cache key. |
| 4 | The returned Uni is memoized so that subsequent subscriptions do not trigger computation again. |
Extension configuration reference
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property |
Type |
Default |
|---|---|---|
The value. Environment variable: |
string |
|
The metadata. Environment variable: |
Map<String,String> |
|
The value. Environment variable: |
string |
|
The metadata. Environment variable: |
Map<String,String> |