Feature Flags for Coordinated Spring API and Mobile App Rollouts
Feature flags enable coordinated releases across back-end APIs and mobile apps, ensuring smooth rollouts of new features without disrupting the user experience.
Join the DZone community and get the full member experience.
Join For FreeAs part of our FinTech mobile application deployment, we usually faced challenges coordinating the releases across backend APIs and mobile applications on iOS and Android platforms whenever we released significant new features. Typically, we will first deploy the backend APIs, as they are quick to deploy along with the database changes. Once the backend APIs are deployed, we publish the mobile applications for both iOS and Android platforms. The publishing process often takes time. The mobile application gets approved within a few hours and sometimes within a few days. If we raise the tickets with stores, the SLA (Service Level Agreement) for those tickets will span multiple days. The delays we saw were predominantly with Android in the last year or so.
Once the back-end APIs are deployed, they initiate new workflows related to the new features. These could be new screens or a new set of data. However, the mobile application version available at that time for both platforms is not ready to accept these new screens as the newer app version has not been approved yet and would be in the store review process. This inconsistency can lead to a poor user experience which can manifest in various ways, such as the app not functioning correctly, the application crashing, or displaying an oops page or some internal errors. This can be avoided by implementing feature flags on the backend APIs.
Feature Flags
The feature flags are the configurations stored in the database that help us turn specific features on or off in an application without requiring code changes. By wrapping new functionality behind feature flags, we can deploy the code to production environments while keeping the features hidden from end-users until they are ready to be released.
Once the newer versions of the mobile apps are available, we enable these new features from the database so that backend APIs can orchestrate the new workflows or data for the new set of features. Additionally, we need to consider that both iOS and Android apps would get published at different times, so we need to ensure that we have platform-specific feature flags. In our experience, we have seen iOS apps get approved in minutes or hours, and Android apps sometimes take a day to a few hours.
In summary, backend APIs need to orchestrate new workflows and data when the corresponding platform application's latest version is available in the app store. For existing users who have the app installed already, we force a version upgrade at the app launch. To avoid version discrepancy issues during the new feature rollout, we follow a coordinated release strategy using feature flags, as explained below.
Coordinated Release
Backend APIs Release With Feature Flags Off
We first deploy the backend APIs with feature flags with the value Off
for all the platforms. Typically, when we create the feature flags, we keep the default value as Off
or 0
.
Mobile Application Publishing
The mobile application teams for iOS and Android submit the latest validated version to the App Store and Play Store, respectively. The respective teams monitor the publishing process for rejections or clarifications during the review process.
Enable New Feature
Once the respective mobile application team confirms that the app has been published, then we enable the new feature for that platform.
Monitoring
After the new feature has been enabled across the platforms, we monitor the production environment for backend APIs for any errors and mobile applications for any crashes. If any significant issue is identified, we turn off the feature entirely across all platforms or specific platforms, depending on the type of the issue. This allows us to instantaneously roll back a new feature functionality, minimizing the impact on user experience.
Feature Flags Implementation in Spring Boot Application
Feature Service
Below is an example of a FeatureServiceV1Impl
Spring service in the Spring Boot application, which handles feature flags configuration.
We have defined the bean's scope as the request scope. This ensures a new service instance is created for each HTTP request, thus ensuring that the updated configuration data is available for all new requests.
The initializeConfiguration
method is annotated with @PostConstruct
, meaning it is called after the bean's properties have been set. This method fetches the configuration data from the database when the service is first instantiated for each request. With request scope, we only fetch the feature flags configuration from the database once. If there are feature checks at multiple places while executing that request, there would be only one database call to fetch the configuration.
This service's main functionality is to check whether a specific feature is available. It does this by checking the feature flag configuration values from the database. In the example below, the isCashFlowUWAvailable
method checks if the "Cash Flow Underwriting" feature is available for a given origin (iOS, Android, or mobile web app).
@RequestScope
@Service
@Qualifier("featureServiceV1")
public class FeatureServiceV1Impl implements FeatureServiceV1 {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private List<Config> configs;
@Autowired
ConfigurationRepository configurationRepository;
@PostConstruct
private void initializeConfiguration() {
logger.info("FeatureService::initializeConfiguration - Initializing configuration");
if (configs == null) {
logger.info("FeatureService::initializeConfiguration - Fetching configuration");
GlobalConfigListRequest globalConfigListRequest = new GlobalConfigListRequest("ICW_API");
this.configs = this.configurationRepository.getConfigListNoError(globalConfigListRequest);
}
}
@Override
public boolean isCashFlowUWAvailable(String origin) {
boolean result = false;
try {
if (configs != null && configs.size() > 0) {
if (origin.toLowerCase().contains("ios")) {
result = this.isFeatureAvailableBasedOnConfig("feature_cf_uw_ios");
} else if (origin.toLowerCase().contains("android")) {
result = this.isFeatureAvailableBasedOnConfig("feature_cf_uw_android");
} else if (origin.toLowerCase().contains("mobilewebapp")) {
result = this.isFeatureAvailableBasedOnConfig("feature_cf_uw_mobilewebapp");
}
}
} catch (Exception ex) {
logger.error("FeatureService::isCashFlowUWAvailable - An error occurred detail error:", ex);
}
return result;
}
private boolean isFeatureAvailableBasedOnConfig(String configName) {
boolean result = false;
if (configs != null && configs.size() > 0) {
Optional<Config> config = Optional
.of(configs.stream().filter(o -> o.getConfigName().equals(configName)).findFirst()).orElse(null);
if (config.isPresent()) {
String configValue = config.get().getConfigValue();
if (configValue.equalsIgnoreCase("1")) {
result = true;
}
}
}
return result;
}
}
Consuming Feature Service
We will then reference and auto-wire the FeatureServiceV1
in the controller or other service in the Spring Boot application, as shown below. We annotate the FeatureServiceV1
with the @Lazy
annotation. The @Lazy
annotation will ensure that the FeatueServiceV1
is instantiated when the FeatrueServiceV1
method is invoked from particular methods of the controller or service. This will prevent the unnecessary loading of the feature-specific database configurations if any other method of the controller or service is invoked where the feature service is not referenced. This helps improve the application start-up time.
@Autowired
@Lazy
private FeatureServiceV1 featureServiceV1;
We then leverage FeatureServiceV1
to check the availability of the feature and then branch our code accordingly. Branching allows us to execute feature-specific code when available or default to the normal path. Below is an example of how to use the feature availability check and to branch the code:
if (this.featureServiceV1.isCashFlowUWAvailable(context.origin)) {
logger.info("Cashflow Underwriting Path");
// Implement the logic for the Cash Flow Underwriting path
} else {
logger.info("Earlier Normal Path");
// Implement the logic for the normal path
}
Here’s how we can implement this conditional logic in a controller or service method:
@RestController
@RequestMapping("/api/v1/uw")
public class UnderwritingController {
@Autowired
@Lazy
private FeatureServiceV1 featureServiceV1;
@RequestMapping("/loan")
public void processLoanUnderwriting(RequestContext context) {
if (this.featureServiceV1.isCashFlowUWAvailable(context.origin)) {
logger.info("Cashflow Underwriting Path");
// Implement the logic for the Cash Flow Underwriting path
} else {
logger.info("Earlier Normal Path");
// Implement the logic for the normal path
}
}
}
Conclusion
Feature flags play is important, particularly when coordinating releases across multiple platforms. In our case, we have four channels: two native mobile applications (iOS and Android), a mobile web application (browser-based), and an iPad application. Feature flags help in smooth and controlled rollouts, minimizing disruptions to the user experience. They ensure that new features are only activated when the corresponding platform-specific latest version of the application is available in the app stores.
Opinions expressed by DZone contributors are their own.
Comments