Import ff4j-spring-boot-starter dependency in your microservices to get the web console and rest api working immediately. (To be used for the backend app. Now compliant with Spring Boot 2x: 👉 SAMPLES
1.a - Using DOCKER
If you already have Docker install on your machine you can simply run the demo with the command below, it should start at http://localhost:8080. 🤘
docker run -p 8080:8080 ff4j/ff4j-sample-springboot2x:1.8.5
git clone https://github.com/ff4j/ff4j-samples.git cd spring-boot-2x/ff4j-sample-springboot2x mvn spring-boot:run
As FF4j provides REST-API and a web console we may want to create a web application to get the most of this tutorial. We will use Spring-Boot but the same steps could be adapted for application.
You can create your project in multiple ways using your IDE or a maven archetype. For this tutorial we will use the website https://start.spring.io/ entering some package name, project name and adding the Spring WEB module and then click GENERATE to download the app.
<dependency> <groupId>org.ff4j</groupId> <artifactId>ff4j-spring-boot-starter</artifactId> <version>${ff4j.version}</version> </dependency> <dependency> <groupId>org.ff4j</groupId> <artifactId>ff4j-web</artifactId> <scope>test</scope> <version>${ff4j.version}</version> </dependency>
<properties> <ff4j.version>...</ff4j.version> </properties>
2.c - Define FF4j Object
With the Spring framework we use the annotations @Configuration and @Bean to define our FF4j object. FeatureStore, PropertyStore and EventRepository are all initialized with in-memory implementations by default.At the FF4j level exist some flags to enable features like audit or feature autocreation (default behaviour is throwing FeatureNotFoundException)
import org.ff4j.FF4j; import org.ff4j.audit.repository.InMemoryEventRepository; import org.ff4j.property.store.InMemoryPropertyStore; import org.ff4j.store.InMemoryFeatureStore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FF4jConfig { @Bean public FF4j getFF4j() { FF4j ff4j = new FF4j(); /* * Implementation of each store. Here this is boiler plate as if nothing * is specified the inmemory is used. Those are really the one that will * change depending on your technology. */ ff4j.setFeatureStore(new InMemoryFeatureStore()); ff4j.setPropertiesStore(new InMemoryPropertyStore()); ff4j.setEventRepository(new InMemoryEventRepository()); // Enabling audit and monitoring, default value is false ff4j.audit(true); // When evaluting not existing features, ff4j will create then but disabled ff4j.autoCreate(true); // To define RBAC access, the application must have a logged user //ff4j.setAuthManager(...); // To define a cacher layer to relax the DB, multiple implementations //ff4j.cache([a cache Manager]); return ff4j; } }
2.d - Expose the Web Console
Exposing the web console is only relevant for backend applications (probably centralized) and not in each component using ff4j. Even you secure with user/password, there is not point to open that breach. That been said, let's do it.
@Configuration // Enable the REST API documentation @EnableFF4jSwagger // The class should be on classpath : FF4jDispatcherServlet @ConditionalOnClass({FF4jDispatcherServlet.class}) // Setup FF4j first, not is required @AutoConfigureAfter(FF4jConfig.class) public class FF4jWebConsoleConfiguration extends SpringBootServletInitializer { @Bean @ConditionalOnMissingBean public FF4jDispatcherServlet defineFF4jServlet(FF4j ff4j) { FF4jDispatcherServlet ff4jConsoleServlet = new FF4jDispatcherServlet(); ff4jConsoleServlet.setFf4j(ff4j); return ff4jConsoleServlet; } @Bean @SuppressWarnings({"rawtypes","unchecked"}) public ServletRegistrationBean registerFF4jServlet(FF4jDispatcherServlet ff4jDispatcherServlet) { return new ServletRegistrationBean(ff4jDispatcherServlet, "/ff4j-web-console/*"); } }
2.e - Define a sample controller
With the previous steps you do have a working sample but no features or properties to play with. In the demo we added a rest controller mapped to root path /. In the one we define 3 features (showWebConsoleURL, showRestApiURL, showUserName) and 1 property (username).
@RestController @RequestMapping("/") public class HomeController { private static final Logger LOGGER = LoggerFactory.getLogger(HomeController.class); private static final String FEATURE_SHOW_WEBCONSOLE = "showWebConsoleURL"; private static final String FEATURE_SHOW_REST_API = "showRestApiURL"; private static final String FEATURE_SHOW_USERNAME = "showUserName"; private static final String PROPERTY_USERNAME = "username"; @Autowired public FF4j ff4j; @PostConstruct public void populateDummyFeatureForMySample() { if (!ff4j.exist(FEATURE_SHOW_WEBCONSOLE)) { ff4j.createFeature(new Feature(FEATURE_SHOW_WEBCONSOLE, true)); } if (!ff4j.exist(FEATURE_SHOW_REST_API)) { ff4j.createFeature(new Feature(FEATURE_SHOW_REST_API, true)); } if (!ff4j.exist(FEATURE_SHOW_USERNAME)) { ff4j.createFeature(new Feature(FEATURE_SHOW_USERNAME, true)); } if (!ff4j.getPropertiesStore().existProperty(PROPERTY_USERNAME)) { ff4j.createProperty(new PropertyString(PROPERTY_USERNAME, "cedrick")); } LOGGER.info(" + Features and properties have been created for the sample."); }
@RequestMapping(value = "/", method = RequestMethod.GET, produces = "text/html") public String get() { LOGGER.info(" + Rendering home page..."); StringBuilder htmlPage = new StringBuilder("<html><body><ul>"); htmlPage.append("<h2>This is home page.</h2>"); htmlPage.append("<p>Displaying the links below is driven by features in FF4j." + "If you disable the feature " + "the link will disapear (but the servlet will still response, " + "this is just a trick to illustrate " + "response in the UI)</p>"); htmlPage.append("<p><b>List of resources for you :" + "</b><br/><ul>"); if (ff4j.check(FEATURE_SHOW_WEBCONSOLE)) { htmlPage.append("<li> To access the To access the <b>REST API</b> " + "please go to <a href=\"./api/ff4j\" target=\"_blank\">ff4j-rest-api </a>"); } if (ff4j.check(FEATURE_SHOW_USERNAME)) { if (ff4j.getPropertiesStore().existProperty(PROPERTY_USERNAME)) { htmlPage.append("<li> " + PROPERTY_USERNAME + " value is "); htmlPage.append("<span style=\"color:blue;font-weight:bold\">"); htmlPage.append(ff4j.getPropertiesStore().readProperty(PROPERTY_USERNAME).asString()); htmlPage.append("</span>"); } else { htmlPage.append("<li> " + PROPERTY_USERNAME + " does not exist."); } } htmlPage.append("</ul></body></html>"); return htmlPage.toString(); }
Feature branches lead to conflict during merges. Use trunk-based-developpement to toggle-off unfinished code when develop continously.
Avoid clusters nodes inconsistency during deployment and deliver new features desactivated. Toggle "ON"" when all nodes are up-to-date and ready.
Do not create dedicated infrastructure to qualify new features. Open them for subset of beta-testers and directly into production environments.
Measure performance impacts of new features. Activate them dynamically on a defined ratio of incoming requests and observe system responses.
Tune and protect your system of heavy loads: focus on high business value requests and discard others dynamically (clients vs prospects, carts contents..).
Avoid annoying frequent deployments and downloads of mobile applications by providing empty shelf: request expected active features to YOUR servers.
Toggling is not only technical. Define your own rules and evaluate features based on business requirements like office hours, user profiles...
Split A and B populations using a business toggle. Measure business impacts not only with CRM but also hitcounts of very same framework.
Implement the circuit breaker pattern with a dedicated strategy and custom rules allowing to toggle off proactively not available features.