Skip to content

Commit cb258f9

Browse files
committed
Blogpost: Quarkus feature flags
1 parent 795fe14 commit cb258f9

File tree

1 file changed

+264
-0
lines changed

1 file changed

+264
-0
lines changed
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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

Comments
 (0)