Often, developers create technical solutions using whatever is available to get the functionality up and running. Especially when it is necessary to test some product hypothesis and it doesn't make much sence to spend a lot of time and resources on it. After the hypothesis is confirmed, an even more interesting time begins - the search for tradeoffs: you need to release features quickly, adapting to the users number growth.
It is very handy to be able to change the behavior of the service at any time without restarting the servers, or to turn off a broken functionality or to conduct AB-testing. To do this, developers usually use Feature Toggles. If your startup has not a giant, but a small group of enthusiasts - you will have to solve this problem using available tools. In this article, I will show you a simple way of how to implement Feature Toggle using a shared SQL database.
No matter how much effort is put into the development of fault-tolerant self-sustainable systems, manual interventions are inevitable:
If a problem could be fixed easily, a developer can adjust data directly in the database, or write a bash/python/whatever script. We usually avoid direct accessing the data, but circumstances could make you do it. But there is a more reliable way - an application with internal use - Administration Panel.
Benefits of having Admin Panel:
Often admin panel is designed to access database of business-logic servers. This approach has some disadvantages, but is a good tradeoff: providing an acceptable level of security through simple implementation. Thus, most probably, you can use the main database to store the settings, including the feature-toggle and access them from your servers.
The simplest use case is a two position toggle - either enable for all or disable for all.
private final FeatureToggleService featureToggleService;
public void businessLogic() {
if (featureToggleService.isFeatureEnabled(IMPORTANT_FEATURE)) {
doImportantFeatureStuff();
} else {
log.info("Feature disabled {}", IMPORTANT_FEATURE);
}
}
If I implemented it in memory, I'd check if there is a key in a hash set. If there is one - feature is enabled, otherwise - disabled. In an SQL database there could be a table, which contains the names of the features - that's all what we need here. We can query the keys and that's how it could look like:
public class FeatureToggleService {
private static final String STATE_QUERY = "SELECT id FROM features WHERE name = (?)";
private final JdbcOperations jdbcOperations;
private final LoadingCache<String, Boolean> toggleStateCache = CacheBuilder.newBuilder()
.expireAfterWrite(60, TimeUnit.SECONDS)
.build(new CacheLoader<>() {
public Boolean load(String featureName) {
return jdbcOperations.queryForList(STATE_QUERY, String.class, featureName)
.size() > 0;
}
});
public boolean isFeatureEnabled(String featureName) {
boolean state = toggleStateCache.getUnchecked(featureName);
return Boolean.TRUE.equals(state);
}
}
In the example above, I used a cache from Google Guava. It’s a simple local cache, but in 99% of cases it will probably meet your requirements. Feature are toggled infrequently, there is no point in making requests often. I chose 60 seconds as an example - but it’s a good trade off, not too many requests and not too long switching.
A slightly more complicated case. You want to test functionality in production, but you don't want it to be available to all users. Instead, you want to enable the feature only for a set of users,
who are considered to be testers.
private final FeatureToggleService featureToggleService;
public void businessLogic(LoggedUser loggedUser) {
if (featureToggleService.isFeatureEnabled(IMPORTANT_FEATURE, loggedUser.id())) {
doImportantFeatureStuff();
} else {
log.info("Feature disabled {}", IMPORTANT_FEATURE);
}
}
The third state of the feature has appeared - testing. So states are:
If feature is enabled for testing - you should check if current user is a tester. In other words whether user is in a list of persons who should have access to the feature.
The database will require a second column that will store the state of the feature (it could be a number or a meaningful name). And you will also need to store a list of testers somewhere, for example, it could be another table in the database.
And the implementation could be:
public class FeatureToggleService {
private static final String STATE_QUERY =
"SELECT enabled_for FROM features WHERE name = (?)";
private static final String TESTERS_QUERY = "SELECT id FROM test_users";
private final JdbcOperations jdbcOperations;
private final Supplier<Set<Long>> testUsersSupplier =
memoizeWithExpiration(this::extractTesters, 60, TimeUnit.SECONDS);
private final LoadingCache<String, String> toggleStateCache = CacheBuilder.newBuilder()
.expireAfterWrite(60, TimeUnit.SECONDS)
.build(new CacheLoader<>() {
public String load(String featureName) {
return jdbcOperations.queryForList(STATE_QUERY, String.class, featureName)
.stream().findFirst().orElse("NONE");
}
});
public boolean isFeatureEnabled(String featureName, long userId) {
String state = toggleStateCache.getUnchecked(featureName);
return "ALL".equals(state) ||
("TEST".equals(state) && testUsersSupplier.get().contains(userId));
}
private Set<Long> extractTesters() {
return Set.copyOf(jdbcOperations.queryForList(TESTERS_QUERY, Long.class));
}
}
First we fetch state of the feature by it’s name (either from database or cache), then if it’s ALL
consider it enabled for all, or if it’s TEST
- consider it only enabled for testers. If so we fetch list of testers (either from database or from cache) and check if current user is in the list. If none of above - consider feature disabled for all.
There is one un-obvious problem with local cache which you should be aware of:
Why is that so? Local caches don’t know anything about other ot servers, in most cases it could not be a problem at all. Otherwise, you should do one of following:
Many developers think that it’s cool to use a fancy framework, but you need to assess if it's really justified. It's often easier to write a simple module that satisfies specific needs of your application, develop it your own way. But here you need to feel the boundary - writing your own HashMap from scratch does not make any sense.