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
Use FF4j through a WEB API and connect with others languages, specially javascript frontends.( also avoid microservices to connect to DB directly). Click the image for doc and demo, it might take 30s to start.
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.11
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.