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.
Reference
Implementing Custom Endpoints [spring-boot-actuator]
Spring Boot Actuator [Baeldung]
https://www.baeldung.com/spring-boot-actuators
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