Skip to content

Commit ca3ef42

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

File tree

1 file changed

+270
-0
lines changed

1 file changed

+270
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
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 provide 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 - `Flag#feature()` - 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 with `Flag#metadata()`.
43+
Metadata enable further configuration which can be leveraged in the SPI.
44+
45+
== Simple example
46+
47+
Let's start simple.
48+
We will create a flag for a feature called `my-feature-alpha` in the `application.properties` file.
49+
50+
[source,properties]
51+
----
52+
quarkus.flags.runtime."my-feature-alpha".value=true <1>
53+
----
54+
<1> Define a _runtime_ flag for feature `my-feature-alpha` with initial value `true`.
55+
56+
TIP: The runtime feature flags can be overriden at runtime, e.g. using a system property or an environment variable.
57+
58+
The `io.quarkiverse.flags.Flags` interface represents the central point to access feature flags.
59+
The container registers a CDI bean that implements `Flags` automatically.
60+
Therefore, we will simply inject `Flags` and use one of its convenient methods.
61+
62+
[source,java]
63+
----
64+
package org.example;
65+
66+
import io.quarkiverse.flags.Flags;
67+
import jakarta.inject.Inject;
68+
69+
@ApplicanScoped
70+
public class MyService {
71+
72+
@Inject
73+
Flags flags;
74+
75+
void call() {
76+
if (flags.isEnabled("my-feature-alpha")) {
77+
// This business logic is executed only if "my-feature-alpha" value evaluates to true
78+
}
79+
}
80+
}
81+
----
82+
83+
You can also access the flag in your UI built with Qute:
84+
85+
[source,html]
86+
----
87+
<!DOCTYPE html>
88+
<html>
89+
<head>
90+
<title>Flags</title>
91+
</head>
92+
<body>
93+
<h1>Hello - Quarkus Club 2025</h1>
94+
{#if flag:enabled('my-feature-alpha')} <1>
95+
<p>Feature alpha is enabled!
96+
{/if}
97+
</body>
98+
</html>
99+
----
100+
<1> The `flag:` namespace provides other useful properties and methods.
101+
102+
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.
103+
104+
== Flag providers
105+
106+
So far we defined the flag in the Quarkus config.
107+
This is not very flexible.
108+
Let's try some other built-in flag providers.
109+
We can use the `io.quarkiverse.flags.InMemoryFlagProvider` - an in-memory repository that can be useful for testing and dynamic registration.
110+
111+
[source,java]
112+
----
113+
import io.quarkiverse.flags.BooleanValue;
114+
import io.quarkiverse.flags.Flag;
115+
import io.quarkiverse.flags.InMemoryFlagProvider;
116+
import io.quarkus.runtime.Startup;
117+
import jakarta.inject.Inject;
118+
119+
@ApplicationScoped
120+
public class MyInitService {
121+
122+
AtomicBoolean alpha = new AtomicBoolean();
123+
124+
@Inject
125+
InMemoryFlagProvider provider; <1>
126+
127+
@Startup <2>
128+
void addFlags() {
129+
provider.addFlag(Flag.builder("my-feature-alpha") <3>
130+
.setCompute(ctx -> BooleanValue.from(alpha.get())) <4>
131+
.build());
132+
}
133+
}
134+
----
135+
<1> Inject `InMemoryFlagProvider` to add/remove flags.
136+
<2> This method is automatically executed at application startup.
137+
<3> The `InMemoryFlagProvider` has higher priority and overrides the flag provided in config.
138+
<4> The current value of `my-feature-alpha` is calculated from `MyInitService#alpha`.
139+
140+
This way we can control the current value of `my-feature-alpha" easily.
141+
However, in real use cases we will probably need to persist the feature flags in an external system.
142+
143+
The [`quarkus-flags-hibernate-orm` extension](https://docs.quarkiverse.io/quarkus-flags/dev/hibernate-orm.html) provides integration with Hibernate ORM.
144+
It discovers all JPA entities annotated with `@io.quarkiverse.flags.hibernate.common.FlagDefinition` and generates a flag provider automatically.
145+
The generated provider simply loads all flags from the database.
146+
A mapping looks like:
147+
148+
[source,java]
149+
----
150+
import jakarta.persistence.Entity;
151+
152+
import io.quarkiverse.flags.hibernate.common.FlagDefinition;
153+
import io.quarkiverse.flags.hibernate.common.FlagFeature;
154+
import io.quarkiverse.flags.hibernate.common.FlagValue;
155+
import io.quarkus.hibernate.orm.panache.PanacheEntity;
156+
157+
@FlagDefinition <1>
158+
@Entity
159+
public class MyFlag extends PanacheEntity {
160+
161+
@FlagFeature <2>
162+
public String feature;
163+
164+
@FlagValue <3>
165+
public String value;
166+
167+
}
168+
----
169+
<1> Marks a flag definition entity.
170+
<2> Defines the feature name of a feature flag.
171+
<3> Defines the value of a feature flag.
172+
173+
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.
174+
175+
== Flag evaluators
176+
177+
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.
178+
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.
179+
Flag evaluators must be CDI beans.
180+
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))`.
181+
There are also several built-in evaluators available.
182+
183+
=== Current time
184+
185+
The `io.quarkiverse.flags.TimeSpanFlagEvaluator` evaluates a flag based on the current date-time obtained from the system clock in the default time-zone.
186+
187+
[source,properties]
188+
----
189+
quarkus.flags.runtime."my-feature-alpha".value=true
190+
quarkus.flags.runtime."my-feature-alpha".meta.evaluator=quarkus.time-span <1>
191+
quarkus.flags.runtime."my-feature-alpha".meta.start-time=2001-01-01T10:15:30+01:00[Europe/Prague] <2>
192+
----
193+
<1> `quarkus.time-span` implies that `TimeSpanFlagEvaluator`` is used to compute the current value of the feature flag.
194+
<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.
195+
196+
TIP: It is also possible to specify the `end-time` metadata value to specify the upper bound of the time inverval.
197+
198+
=== Current user
199+
200+
The `SecurityIdentityFlagEvaluator` can be used to compute the current value of a feature flag based on the current `SecurityIdentity`.
201+
A typical feature flag configuration looks like:
202+
203+
[source,properties]
204+
----
205+
quarkus.flags.runtime."my-feature-alpha".value=true
206+
quarkus.flags.runtime."my-feature-alpha".meta.evaluator=quarkus.security.identity <1>
207+
quarkus.flags.runtime."my-feature-alpha".meta.authenticated=true <2>
208+
quarkus.flags.runtime."my-feature-alpha".meta.roles-allowed=foo,bar <3>
209+
----
210+
<1> Assign the evaluator to the flag.
211+
<2> The current user must be authenticated.
212+
<3> The current user must have one of the defined roles.
213+
214+
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.
215+
It can be used to implement gradual rollout.
216+
A typical feature flag configuration may look like:
217+
218+
[source,properties]
219+
----
220+
quarkus.flags.runtime."my-feature-alpha".value=true
221+
quarkus.flags.runtime."my-feature-alpha".meta.evaluator=quarkus.security.username-rollout <1>
222+
quarkus.flags.runtime."my-feature-alpha".meta.authenticated=true <2>
223+
----
224+
<1> Assign the evaluator to the flag.
225+
<2> Enable the flag for the given percentage of users.
226+
227+
=== CRON
228+
229+
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.
230+
A typical feature flag configuration may look like:
231+
232+
[source,properties]
233+
----
234+
quarkus.flags.runtime."my-feature-alpha".value=true
235+
quarkus.flags.runtime."my-feature-alpha".meta.evaluator=quarkus.cron <1>
236+
quarkus.flags.runtime."my-feature-alpha".meta.authenticated=* * * * mon <2>
237+
----
238+
<1> Assign the evaluator to the flag.
239+
<2> The current date-time must match the given CRON expression. In this particular case, the flag will be enabled *every Monday*.
240+
241+
NOTE: By default, the Unix/crontab syntax is used. However, it is also possible to use the syntax from Cron4j, Quartz and Spring.
242+
243+
=== The case for multiple evaluators
244+
245+
Sometimes it might be useful to combine several evaluators to compute the value of a flag.
246+
The core extension provides `io.quarkiverse.flags.CompositeFlagEvaluator` that evaluates a flag with the specified sub-evaluators.
247+
248+
[source,properties]
249+
----
250+
quarkus.flags.runtime."my-feature-alpha".value=true
251+
quarkus.flags.runtime."my-feature-alpha".meta.evaluator=quarkus.composite <1>
252+
quarkus.flags.runtime."my-feature-alpha".meta.sub-evaluators=quarkus.time-span, quarkus.security.identity <2>
253+
quarkus.flags.runtime."my-feature-alpha".meta.start-time=2026-01-01T12:00:00+01:00[Europe/Prague] <3>
254+
quarkus.flags.runtime."my-feature-alpha".meta.roles-allowed=admin <4>
255+
----
256+
<1> Assign the evaluator to the flag.
257+
<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`.
258+
<3> The current date-time must be after the specified start-time.
259+
<4> The current user must have the role `admin`.
260+
261+
== Extensibility
262+
263+
Flag providers and flag evaluators are CDI beans.
264+
So all you have to do is add a bean that implements `FlagProvider` or `FlagEvaluator` in your application.
265+
You can use CDI interceptors and even decorate these components.
266+
267+
== Conclusion
268+
269+
Quarkus Feature Flags is a lightweight and extensible extension that can help you build more flexible applications.
270+
Feedback, feature requests and contributions are welcome.

0 commit comments

Comments
 (0)