Asimov Flagz
Feature flags library based on Togglz library.
Installation
Gradle (Kotlin)
repositories {
mavenCentral()
}
dependencies {
implementation("com.nbottarini:asimov-flagz:0.2.1")
}
Gradle (Groovy)
repositories {
mavenCentral()
}
dependencies {
implementation 'com.nbottarini:asimov-flagz:0.2.1'
}
Maven
<dependency>
<groupId>com.nbottarini</groupId>
<artifactId>asimov-flagz</artifactId>
<version>0.2.1</version>
</dependency>
Quick start
- Define an enum with your feature flags (it must implement the Feature interface):
enum class Features: Feature {
MY_FEATURE,
MY_OTHER_FEATURE
}
- Initialize the library by configuring your enum:
initFlagz {
featureEnum<Features>()
repository(EnvironmentFeatureRepository())
}
You'll have to configure a feature repositories. A repository provides a way to store and search for feature flag states (enabled or disabled).
In this example the EnvironmentFeatureRepository looks for features in the system environment variables. The environment variable must be prefixed with 'FEATURE_'. For example 'FEATURE_MY_FEATURE=1', 'FEATURE_MY_OTHER_FEATURE=0'.
- Use that flags in your code:
if (Features.MY_FEATURE.isEnabled) {
// Do something...
}
EnabledByDefault
By default, if a feature flag is not set is disabled. You can change this behaviour per-feature with the EnabledByDefault
annotation.
enum class Features: Feature {
MY_FEATURE,
@EnabledByDefault
MY_OTHER_FEATURE
}
Feature repositories
Feature repositories allows you to store and retrieve feature flags state.
InMemoryFeatureRepository
Store features state in memory.
initFlagz {
featureEnum<Features>()
repository(InMemoryFeatureRepository())
}
EnvironmentFeatureRepository
This is a read-only repository. This repository read feature's state from environment variables. Each variable must be prefixed with "FEATURE_". For example "FEATURE_MY_AWESOME_FEATURE".
initFlagz {
featureEnum<Features>()
repository(EnvironmentFeatureRepository())
}
Values '1', 'true' and 'TRUE' are interpreted as enabled. '0', 'false' and 'FALSE' indicates that the feature is disabled.
By default, this repository uses the asimov-environment library to access the environment variables, so you can create a .env file to store your feature flags states for development.
FEATURE_ALLOW_REGISTRATION=1
FEATURE_NEW_BLOG=0
You can use a different environment variables provider by implemented the interface EnvironmentProvider
and passing you implementation on repository construction.
class MyEnvProvider: EnvironmentProvider {
override fun get(name: String) = MyEnvironmentLibrary.get[name]
}
initFlagz {
featureEnum<Features>()
repository(EnvironmentFeatureRepository(MyEnvProvider))
}
JdbcFeatureRepository
Store feature states in a database.
To use this repository you have to pass a jdbc datasource for your database.
val datasource = MysqlDataSource()
datasource.setURL(/* jdbc url */)
datasource.setUser(/* username */)
datasource.setPassword(/* password */)
initFlagz {
featureEnum<Features>()
repository(JdbcFeatureRepository(datasource))
}
By default, it creates a feature_flags table in the database if it doesn't exist. You can customize the table name:
JdbcFeatureRepository(datasource, "flags")
You can also disable the schema generation and create the schema by yourself:
JdbcFeatureRepository(datasource, generateSchema = false)
CREATE TABLE feature_flags (
name VARCHAR(100) PRIMARY KEY,
is_enabled INTEGER NOT NULL,
strategy_id VARCHAR(200),
strategy_params VARCHAR(2000)
)
This repository uses ansi sql so is compatible with most database providers.
CachedFeatureRepository
Wraps another repository by introducing a cache.
initFlagz {
featureEnum<Features>()
repository(CachedFeatureRepository(JdbcFeatureRepository(datasource)))
}
/** or **/
initFlagz {
featureEnum<Features>()
repository(JdbcFeatureRepository(datasource).cached())
}
You can specify the TTL in milliseconds for the cache:
initFlagz {
featureEnum<Features>()
repository(CachedFeatureRepository(JdbcFeatureRepository(datasource), 60_000))
}
/** or **/
initFlagz {
featureEnum<Features>()
repository(JdbcFeatureRepository(datasource).cached(60_000))
}
CompositeFeatureRepository
Allows to use more than one repository. The features are retrieved from the first matching repository.
initFlagz {
featureEnum<Features>()
repository(
CompositeFeatureRepository(
JdbcFeatureRepository(datasource),
EnvironmentFeatureRepository()
)
)
}
/** or **/
initFlagz {
featureEnum<Features>()
repositories(
JdbcFeatureRepository(datasource),
EnvironmentFeatureRepository()
)
}
When a feature flag is set you can customize if it is persisted in the first repository, the last or all of them.
initFlagz {
featureEnum<Features>()
repository(
CompositeFeatureRepository(
JdbcFeatureRepository(datasource),
EnvironmentFeatureRepository()
),
SetStrategies.ALL
)
}
Activation strategies
You can provide different strategies to enable/disable features dynamically based on dates, gradual rollout, per user, per user roles or attributes, etc. You can also create your own strategies.
To define a strategy for a feature you have to annotate it with the Activation
annotation. For example:
enum class Features: Feature {
MY_FEATURE,
@Activation(ReleaseDateActivationStrategy.ID, [ActivationParam(PARAM_DATE, "2020-09-06T10:00:00Z")])
MY_OTHER_FEATURE
}
ReleaseDateActivationStrategy
This strategy allows you to enable a feature in a certain date. The provided date must be in ISO 8601 format.
UsersActivationStrategy
This strategy allows you to enable a feature only to certain users.
enum class Features: Feature {
MY_FEATURE,
@Activation(UsersActivationStrategy.ID, [ActivationParam(PARAM_USERS, "alice, bob")])
MY_OTHER_FEATURE
}
Users must be set by using a userProvider. Read next section for details.
User providers
Allows the customization of feature flags per user or user's attributes.
You can pass a userProvider implementation to the initialization function. The default userProvider is ThreadLocalUserProvider
.
initFlagz {
featureEnum<Features>()
repository(EnvironmentFeatureRepository())
userProvider(MyUserProvider())
}
ThreadLocalUserProvider
This implementation allows you to set the current user per thread.
You can access the current user provider by calling FlagzContext.manager.userProvider
.
val userProvider = FlagzContext.manager.userProvider as ThreadLocalUserProvider
userProvider.bind(SimpleFeatureUser("alice", mapOf("roles" to "admin")))
// Do something and use feature flags
userProvider.release()
Users have to implement the FeatureUser interface, or you can use SimpleFeatureUser implementation.
If you are using a mediator / command-bus like CQBus you can add a middleware to set the current user.
class SetFeatureFlagsUserMiddleware: Middleware {
override fun <T: Request<R>, R> execute(request: T, next: (T) -> R, context: ExecutionContext): R {
val userProvider = FlagzContext.manager.userProvider as? ThreadLocalUserProvider ?: return next(request)
val user = SimpleFeatureUser(context.identity.name, mapOf("roles", context.identity.roles.joinToString(", ")))
userProvider.bind(user)
val response = next(request)
userProvider.release()
return response
}
}