Custom feature flags (Spring Boot)

Introduction

Keywords: Feature Toggles pattern, Feature Flags pattern


This post shows a custom implementation in Spring Boot of the Feature Toggles pattern.

The need raised because Togglz was crashing the Spring Boot 2.7.8 with Java 17 application.


Feature Toggles are a very common agile development practices in the context of continuous deployment and delivery. The basic idea is to associate a toggle with each new feature you are working on. This allows you to enable or disable these features at application runtime, even for individual users.

Example using Spring Boot 2.7.8 with Java 17 and a relational database table for the state via Liquibase

FeatureFlagEndpoint.java

package edu.cou.myapp.featureflags;


import java.util.Collection;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

import java.util.Optional;


import javax.inject.Inject;


import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;

import org.springframework.boot.actuate.endpoint.annotation.Endpoint;

import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;

import org.springframework.boot.actuate.endpoint.annotation.Selector;

import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;

import org.springframework.lang.Nullable;

import org.springframework.stereotype.Component;


import lombok.AllArgsConstructor;

import lombok.Getter;

import lombok.NoArgsConstructor;

import lombok.NonNull;

import lombok.RequiredArgsConstructor;

import lombok.Setter;


/**

* Actuator endpoint to query, enable, and disable feature flags in this application.

*

* # Feature toggles (aka Feature flags) pattern for agile development only.

*

* Warning: Before setting the management.endpoints.web.exposure.include, ensure that the exposed

* actuators do not contain sensitive information, are secured by placing them behind a firewall, or

* are secured by something like Spring Security.

*

* <pre>

* == myapp endpoints==

*

* {GET} /r/actuator/featureflags

* {GET, POST, DELETE} /r/actuator/featureflags/{name}

* </pre>

*/

@AllArgsConstructor(onConstructor_ = { @Inject })

@Component

@Endpoint(id = "featureflags"/* , enableByDefault = false */)

public class FeatureFlagEndpoint {


// Injected by constructor

private FeatureFlagRepo featureFlagRepo;


/** HTTP GET */

@ReadOperation

public Map<String, FeatureFlag> features() {

List<FeatureFlagEntity> ffes = this.featureFlagRepo.findAll();

return FeatureFlag.of(ffes);

}


/** HTTP GET */

@ReadOperation

@Nullable

public FeatureFlag feature(@Selector String name) {

Optional<FeatureFlagEntity> ffeOpt = this.featureFlagRepo.findById(name);

if (ffeOpt.isEmpty()) {

// Actuator seems to expect a null

return null;

}

return FeatureFlag.of(ffeOpt.get());

}


/** HTTP POST */

@WriteOperation

public void configureFeature(@Selector String name, FeatureFlag feature) {

Optional<FeatureFlagEntity> ffeOpt = this.featureFlagRepo.findById(name);


FeatureFlagEntity ffe;

if (ffeOpt.isEmpty()) {

ffe = new FeatureFlagEntity(name, feature.getEnabled());

} else {

ffe = ffeOpt.get();

ffe.setEnabled(feature.getEnabled());

}


this.featureFlagRepo.save(ffe);

}


/** HTTP DELETE */

@DeleteOperation

public void deleteFeature(@Selector String name) {

this.featureFlagRepo.deleteById(name);

}


/**

* Class Feature.

*/

@NoArgsConstructor()

@RequiredArgsConstructor(staticName = "of")

@Getter

@Setter

public static class FeatureFlag {

@NonNull // For lombok constructor

private Boolean enabled;


/*-*/

static FeatureFlag of(FeatureFlagEntity e) {

return FeatureFlag.of(e.getEnabled());

}


/*-*/

static Map<String, FeatureFlag> of(Collection<FeatureFlagEntity> es) {

Map<String, FeatureFlag> ret = new HashMap<>();


for (FeatureFlagEntity e : es) {

ret.put(e.getName(), FeatureFlag.of(e.getEnabled()));

}


return ret;

}

} // Class FeatureFlag


}


FeatureFlagEntity.java

package edu.cou.myapp.featureflags;


import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.Table;

import javax.validation.constraints.Size;


import lombok.AllArgsConstructor;

import lombok.Data;

import lombok.NoArgsConstructor;


/**

* Entity FeatureFlag.

*

* NonNullApi and NonNullFields model used.

*

* <pre>

* Liquibase:

*

databaseChangeLog:


- changeSet:

id: "069"

author: thatsme

failOnError: true

comment: "Create table featureflag"

changes:


- createTable:

tableName: featureflag

columns:

- column:

name: name

type: java.sql.Types.VARCHAR

constraints:

nullable: false

- column:

name: enabled

type: boolean

- addPrimaryKey:

columnNames: name

constraintName: pk_featureflag

tableName: featureflag

* </pre>

*

* <pre>

* Inspired by Togglz.

*

* https://www.javadoc.io/doc/org.togglz/togglz-core/latest/org/togglz/core/repository/jdbc/JDBCStateRepository.html

*

* Togglez db table, as of version 3.3.3:

*

* CREATE TABLE <table> (

* FEATURE_NAME VARCHAR(100) PRIMARY KEY,

* FEATURE_ENABLED INTEGER,

* STRATEGY_ID VARCHAR(200),

* STRATEGY_PARAMS VARCHAR(2000)

* )

* </pre>

*/

@NoArgsConstructor

@AllArgsConstructor

@Data

@Entity

@Table(name = "featureflag")

public class FeatureFlagEntity {


/**

* Feature flag name.

*/

@Size(min = 1, max = 100)

@Id

private String name;


/** Is feature flag enabled? */

private Boolean enabled;


}


FeatureFlagRepo.java

package edu.cou.myapp.featureflags;


import org.springframework.data.jpa.repository.JpaRepository;


/**

* Feature flag table "featureflag".

*/

public interface FeatureFlagRepo extends JpaRepository<FeatureFlagEntity, String> {

}


package-info.java

/**

* <pre>

* (at)NonNullApi should be used at package level in association with

* org.springframework.lang.Nullable annotations at parameter and return value level.

*

* (at)NonNullFields should be used at package level in association with

* org.springframework.lang.Nullable annotations at field level.

* </pre>

*/

@NonNullApi

@NonNullFields

package edu.cou.myapp.featureflags;


import org.springframework.lang.NonNullApi;

import org.springframework.lang.NonNullFields;


069-thatsme-create-featureflag.yaml

# Crate table featureflag for Feature toggles (aka Feature flags) pattern

# Since it is used for Agile development only, no AUD nor technical fields needed

databaseChangeLog:


- changeSet:

id: "069"

author: thatsme

failOnError: true

comment: "Create table featureflag"

changes:


- createTable:

tableName: featureflag

columns:

- column:

name: name

type: java.sql.Types.VARCHAR

constraints:

nullable: false

remarks: "Feature flag name"

- column:

name: enabled

type: boolean

constraints:

nullable: false

remarks: "Is feature flag enabled?"


- addPrimaryKey:

columnNames: name

constraintName: pk_featureflag

tableName: featureflag