|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: 'Quarkus feature flags' |
| 4 | +date: 2025-12-15 |
| 5 | +tags: extension |
| 6 | +synopsis: 'We introduce a new Quarkiverse project - a lightweight and extensible feature flags extension.' |
| 7 | +author: mkouba |
| 8 | +--- |
| 9 | + |
| 10 | +Feature flags are a proven and popular technique. |
| 11 | +In essence, a feature flag makes it possible to turn on-off and/or configure a specific functionality in your application. |
| 12 | +It is also referred to as toggles or switches. |
| 13 | +There are many types of feature flags. |
| 14 | +You may have heard of a "killer switch" that simply disables a problematic feature instantly. |
| 15 | +Feature flags can be used to implement "gradual rollout" to deliver new features gradually to small groups of users (aka beta testers). |
| 16 | +Permission flags are used to control access for different users. |
| 17 | +And so on. |
| 18 | +However, that's not the subject of this blogpost. |
| 19 | +We would like to introduce [Quarkus Feature Flags](https://github.com/quarkiverse/quarkus-flags/) - a Quarkiverse project that aims to provide a _lightweight_ and _extensible_ feature flags Quarkus extension. |
| 20 | + |
| 21 | +More specifically, it provides: |
| 22 | + |
| 23 | +- A blocking/non-blocking API to access feature flags. |
| 24 | +- A non-blocking SPI to provide flags and externalize the computation of a flag value. |
| 25 | +- Several built-in flag providers: |
| 26 | + - Quarkus config can be used to define feature flags, |
| 27 | + - An in-memory flag repository which is useful for testing and dynamic registration of flags. |
| 28 | +- *Hibernate ORM* module, where feature flags are mapped from an annotated entity and are automatically loaded from the database. |
| 29 | +- *Security* module, so that it's possible to evaluate flags based on the current `SecurityIdentity`. |
| 30 | +- *Qute* module, so that it's possible to use the flags directly in templates. |
| 31 | +- *CRON* module with a flag evaluator that matches a specific CRON expression. |
| 32 | + |
| 33 | +Note that the goal of this extension is not to replace robust solutions such as [OpenFeature](https://openfeature.dev/) and [Unleash](https://www.getunleash.io/). |
| 34 | +Instead, we would like to offer a flexible option that integrates well with other parts of the Quarkus ecosystem. |
| 35 | + |
| 36 | +== Flag |
| 37 | + |
| 38 | +In this extension, a feature flag is represented by the `io.quarkiverse.flags.Flag` interface. |
| 39 | +It refers to a specific feature with a string identifier and provides several convenient methods to compute the _current value_. |
| 40 | +The value of a feature flag can be represented as `boolean`, `string` or `integer`. |
| 41 | +There can be only one flag for a given feature at a given time. |
| 42 | +A flag can also expose metadata which that can be leveraged in the SPI. |
| 43 | + |
| 44 | +== Simple example |
| 45 | + |
| 46 | +Let's start simple. |
| 47 | +We will create a flag for a feature called `my-feature-alpha` in the `application.properties` file. |
| 48 | + |
| 49 | +[source,properties] |
| 50 | +---- |
| 51 | +quarkus.flags.runtime."my-feature-alpha".value=true <1> |
| 52 | +---- |
| 53 | +<1> Define a _runtime_ flag for feature `my-feature-alpha` with initial value `true`. |
| 54 | + |
| 55 | +TIP: A `runtime` feature flag can be overriden at runtime, e.g. using a system property or an environment variable. You can also define a flag fixed at build time, i.e. `quarkus.flags.build."my-feature-alpha".value=true`. However, its values cannot be modified at runtime. |
| 56 | + |
| 57 | +The `io.quarkiverse.flags.Flags` interface represents the central point to access feature flags. |
| 58 | +The container registers a CDI bean that implements `Flags` automatically. |
| 59 | +Therefore, we will simply inject `Flags` and use one of its convenient methods. |
| 60 | + |
| 61 | +[source,java] |
| 62 | +---- |
| 63 | +package org.example; |
| 64 | +
|
| 65 | +import io.quarkiverse.flags.Flags; |
| 66 | +import jakarta.inject.Inject; |
| 67 | +
|
| 68 | +@ApplicanScoped |
| 69 | +public class MyService { |
| 70 | +
|
| 71 | + @Inject |
| 72 | + Flags flags; |
| 73 | +
|
| 74 | + void call() { |
| 75 | + if (flags.isEnabled("my-feature-alpha")) { |
| 76 | + // This business logic is executed only if "my-feature-alpha" value evaluates to true |
| 77 | + } |
| 78 | + } |
| 79 | +} |
| 80 | +---- |
| 81 | + |
| 82 | +You can also access the flag in your UI built with Qute: |
| 83 | + |
| 84 | +[source,html] |
| 85 | +---- |
| 86 | +<!DOCTYPE html> |
| 87 | +<html> |
| 88 | +<head> |
| 89 | + <title>Flags</title> |
| 90 | +</head> |
| 91 | +<body> |
| 92 | + <h1>Hello - Quarkus Club 2025</h1> |
| 93 | + {#if flag:enabled('my-feature-alpha')} <1> |
| 94 | + <p>Feature alpha is enabled! |
| 95 | + {/if} |
| 96 | +</body> |
| 97 | +</html> |
| 98 | +---- |
| 99 | +<1> The `flag:` namespace provides other useful properties and methods. |
| 100 | + |
| 101 | +NOTE: If you want to use the Qute integration in your application then you'll need to add the `io.quarkiverse.flags:quarkus-flags-qute` extension to your build file first. |
| 102 | + |
| 103 | +== Flag providers |
| 104 | + |
| 105 | +So far we defined the flag in the Quarkus config. |
| 106 | +This is not very flexible. |
| 107 | +Let's try some other built-in flag providers. |
| 108 | +We can use the `io.quarkiverse.flags.InMemoryFlagProvider` - an in-memory repository that can be useful for testing and dynamic registration. |
| 109 | + |
| 110 | +[source,java] |
| 111 | +---- |
| 112 | +import io.quarkiverse.flags.BooleanValue; |
| 113 | +import io.quarkiverse.flags.Flag; |
| 114 | +import io.quarkiverse.flags.InMemoryFlagProvider; |
| 115 | +import io.quarkus.runtime.Startup; |
| 116 | +import jakarta.inject.Inject; |
| 117 | +
|
| 118 | +@ApplicationScoped |
| 119 | +public class MyInitService { |
| 120 | +
|
| 121 | + AtomicBoolean alpha = new AtomicBoolean(); |
| 122 | +
|
| 123 | + @Inject |
| 124 | + InMemoryFlagProvider provider; <1> |
| 125 | +
|
| 126 | + @Startup <2> |
| 127 | + void addFlags() { |
| 128 | + provider.addFlag(Flag.builder("my-feature-alpha") <3> |
| 129 | + .setCompute(ctx -> BooleanValue.from(alpha.get())) <4> |
| 130 | + .build()); |
| 131 | + } |
| 132 | +} |
| 133 | +---- |
| 134 | +<1> Inject `InMemoryFlagProvider` to add/remove flags. |
| 135 | +<2> This method is automatically executed at application startup. |
| 136 | +<3> The `InMemoryFlagProvider` has higher priority and overrides the flag provided in config. |
| 137 | +<4> The current value of `my-feature-alpha` is calculated from `MyInitService#alpha`. |
| 138 | + |
| 139 | +This way we can control the current value of `my-feature-alpha" easily. |
| 140 | +However, in real use cases we will probably need to persist the feature flags in an external system. |
| 141 | + |
| 142 | +The [`quarkus-flags-hibernate-orm` extension](https://docs.quarkiverse.io/quarkus-flags/dev/hibernate-orm.html) provides integration with Hibernate ORM. |
| 143 | +It discovers all JPA entities annotated with `@io.quarkiverse.flags.hibernate.common.FlagDefinition` and generates a flag provider automatically. |
| 144 | +The generated provider simply loads all flags from the database. |
| 145 | +A mapping looks like: |
| 146 | + |
| 147 | +[source,java] |
| 148 | +---- |
| 149 | +import jakarta.persistence.Entity; |
| 150 | +
|
| 151 | +import io.quarkiverse.flags.hibernate.common.FlagDefinition; |
| 152 | +import io.quarkiverse.flags.hibernate.common.FlagFeature; |
| 153 | +import io.quarkiverse.flags.hibernate.common.FlagValue; |
| 154 | +import io.quarkus.hibernate.orm.panache.PanacheEntity; |
| 155 | +
|
| 156 | +@FlagDefinition <1> |
| 157 | +@Entity |
| 158 | +public class MyFlag extends PanacheEntity { |
| 159 | +
|
| 160 | + @FlagFeature <2> |
| 161 | + public String feature; |
| 162 | +
|
| 163 | + @FlagValue <3> |
| 164 | + public String value; |
| 165 | +
|
| 166 | +} |
| 167 | +---- |
| 168 | +<1> Marks a flag definition entity. |
| 169 | +<2> Defines the feature name of a feature flag. |
| 170 | +<3> Defines the value of a feature flag. |
| 171 | + |
| 172 | +TIP: 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. You can easily implement your own provider. |
| 173 | + |
| 174 | +== Flag evaluators |
| 175 | + |
| 176 | +In real application, you will very likely need some dynamic evaluation logic based on some application state, such as the current user or current time. |
| 177 | +There is the `io.quarkiverse.flags.spi.FlagEvaluator` SPI which makes it possible to externalize the computation of the current value of a feature flag. |
| 178 | +Flag evaluators must be CDI beans. |
| 179 | +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))`. |
| 180 | +There are also several built-in evaluators available. |
| 181 | + |
| 182 | +=== Current time |
| 183 | + |
| 184 | +The `io.quarkiverse.flags.TimeSpanFlagEvaluator` evaluates a flag based on the current date-time obtained from the system clock in the default time-zone. |
| 185 | +The current time must be after the `start-time` (exclusive) and before the `end-time` (exclusive). |
| 186 | + |
| 187 | +[source,properties] |
| 188 | +---- |
| 189 | +quarkus.flags.runtime."my-feature-alpha".meta.evaluator=quarkus.time-span <1> |
| 190 | +quarkus.flags.runtime."my-feature-alpha".meta.start-time=2001-01-01T10:15:30+01:00[Europe/Prague] <2> <3> |
| 191 | +---- |
| 192 | +<1> Assign the evaluator to the flag. Note that we do not specify the initial value for `my-feature-alpha` - it is `true` by default. |
| 193 | +<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. |
| 194 | +<3> We do not specify the `end-time` metadata value, so there is no upper bound for the time inverval. |
| 195 | + |
| 196 | +=== Current user |
| 197 | + |
| 198 | +The `SecurityIdentityFlagEvaluator` can be used to compute the current value of a feature flag based on the current `SecurityIdentity`. |
| 199 | +A typical feature flag configuration looks like: |
| 200 | + |
| 201 | +[source,properties] |
| 202 | +---- |
| 203 | +quarkus.flags.runtime."my-feature-alpha".meta.evaluator=quarkus.security.identity <1> |
| 204 | +quarkus.flags.runtime."my-feature-alpha".meta.authenticated=true <2> |
| 205 | +quarkus.flags.runtime."my-feature-alpha".meta.roles-allowed=foo,bar <3> |
| 206 | +---- |
| 207 | +<1> Assign the evaluator to the flag. Note that we do not specify the initial value for `my-feature-alpha` - it is `true` by default. |
| 208 | +<2> The current user must be authenticated. |
| 209 | +<3> The current user must have one of the defined roles. |
| 210 | + |
| 211 | +The `UsernameRolloutFlagEvaluator`, on the other hand, is an evaluator using a simple percentage-based rollout strategy, based on a consistent numerical representation of the current user name. |
| 212 | +It can be used to implement gradual rollout. |
| 213 | +A typical feature flag configuration may look like: |
| 214 | + |
| 215 | +[source,properties] |
| 216 | +---- |
| 217 | +quarkus.flags.runtime."my-feature-alpha".meta.evaluator=quarkus.security.username-rollout <1> |
| 218 | +quarkus.flags.runtime."my-feature-alpha".meta.authenticated=true <2> |
| 219 | +---- |
| 220 | +<1> Assign the evaluator to the flag. Note that we do not specify the initial value for `my-feature-alpha` - it is `true` by default. |
| 221 | +<2> Enable the flag for the given percentage of users. |
| 222 | + |
| 223 | +=== CRON |
| 224 | + |
| 225 | +The [quarkus-flags-cron](https://docs.quarkiverse.io/quarkus-flags/dev/cron.html) extension provides the `CronFlagEvaluator` that can be used to compute the current value of a feature flag based on the current date-time obtained from the system clock in the default time-zone and the configured CRON expression. |
| 226 | +A typical feature flag configuration may look like: |
| 227 | + |
| 228 | +[source,properties] |
| 229 | +---- |
| 230 | +quarkus.flags.runtime."my-feature-alpha".meta.evaluator=quarkus.cron <1> |
| 231 | +quarkus.flags.runtime."my-feature-alpha".meta.authenticated=* * * * mon <2> |
| 232 | +---- |
| 233 | +<1> Assign the evaluator to the flag. Note that we do not specify the initial value for `my-feature-alpha` - it is `true` by default. |
| 234 | +<2> The current date-time must match the given CRON expression. In this particular case, the flag will be enabled *every Monday*. |
| 235 | + |
| 236 | +NOTE: By default, the Unix/crontab syntax is used. However, it is also possible to use the syntax from Cron4j, Quartz and Spring. |
| 237 | + |
| 238 | +=== The case for multiple evaluators |
| 239 | + |
| 240 | +Sometimes it might be useful to combine several evaluators to compute the value of a flag. |
| 241 | +The core extension provides `io.quarkiverse.flags.CompositeFlagEvaluator` that evaluates a flag with the specified sub-evaluators. |
| 242 | + |
| 243 | +[source,properties] |
| 244 | +---- |
| 245 | +quarkus.flags.runtime."my-feature-alpha".meta.evaluator=quarkus.composite <1> |
| 246 | +quarkus.flags.runtime."my-feature-alpha".meta.sub-evaluators=quarkus.time-span, quarkus.security.identity <2> |
| 247 | +quarkus.flags.runtime."my-feature-alpha".meta.start-time=2026-01-01T12:00:00+01:00[Europe/Prague] <3> |
| 248 | +quarkus.flags.runtime."my-feature-alpha".meta.roles-allowed=admin <4> |
| 249 | +---- |
| 250 | +<1> Assign the evaluator to the flag. Note that we do not specify the initial value for `my-feature-alpha` - it is `true` by default. |
| 251 | +<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`. |
| 252 | +<3> The current date-time must be after the specified start-time. |
| 253 | +<4> The current user must have the role `admin`. |
| 254 | + |
| 255 | +== Extensibility |
| 256 | + |
| 257 | +Flag providers and flag evaluators are CDI beans. |
| 258 | +So all you have to do is add a bean that implements `FlagProvider` or `FlagEvaluator` in your application. |
| 259 | +You can use CDI interceptors and even decorate these components. |
| 260 | + |
| 261 | +== Conclusion |
| 262 | + |
| 263 | +Quarkus Feature Flags is a lightweight and extensible extension that can help you build more flexible applications. |
| 264 | +Feedback, feature requests and contributions are welcome. |
0 commit comments