Maven
License: MIT
CI Status

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

  1. Define an enum with your feature flags (it must implement the Feature interface):

enum class Features: Feature {
    MY_FEATURE,
    MY_OTHER_FEATURE
}
  1. 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’.

  1. 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
    }
}

GitHub

View Github