Feature flags в Java: как включать фичи без перезапуска приложения
Представьте: вы неделю пилили новую фичу — форму обратной связи с магическим автодополнением. Замержили, выкатили на прод. И тут — баг. Пользователи не могут отправить сообщение. Приходится срочно откатывать коммит, пересобирать JAR, перезапускать сервер. Все ненавидят этот момент.
А что если бы вы могли просто нажать кнопку "выключить" прямо в админке, не трогая код? Или включить фичу только для 10% пользователей, чтобы протестировать нагрузку? Или запустить A/B тест, где половина видит новый дизайн, а половина — старый?
Всё это — магия feature flags (они же feature toggles). В этой статье мы разберем, как реализовать их на Java — от примитивного Enum до промышленных решений вроде Togglz и LaunchDarkly. Поехали!
Что такое feature flag и зачем он вам?
Feature flag (или feature toggle) — это условный переключатель в коде, который позволяет включать или выключать функциональность без изменения исходного кода и переразвертывания приложения.
Звучит как банальный if? Но сила — в управлении. Вы храните состояние флагов в конфиге, базе данных или внешнем сервисе. Меняете значение — фича включается или отключается мгновенно.
Зачем это нужно в Java-проектах?
- Trunk-based development — мержите недоделанные фичи в main, пряча их за флагом. Коллеги не увидят баги, а вы спокойно доделываете.
- Canary releases — включаете фичу для 5% пользователей, смотрите метрики, потом для 50%, потом для всех.
- Kill switch — если фича падает на проде, вы просто выключаете её одной кнопкой.
- A/B тестирование — сравниваете поведение пользователей с фичей и без.
- Персонализация — включаем премиум-фичи только для платящих клиентов.
Главный риск — технический долг. Если не удалять старые флаги, код превращается в паутину из if-else. Об этом мы тоже поговорим.
Самый простой способ: Enum + Properties
Начнём с базового подхода. Создаём перечисление флагов и читаем их из application.properties.
public enum FeatureFlag {
NEW_CHECKOUT_FLOW,
AI_RECOMMENDATIONS,
DARK_MODE
}
Загружаем флаги в сервис:
@Component
public class FeatureFlagService {
private final Map<FeatureFlag, Boolean> flags = new HashMap<>();
public FeatureFlagService(@Value("${features.new-checkout-flow:false}") boolean newCheckout,
@Value("${features.ai-recommendations:false}") boolean aiRec,
@Value("${features.dark-mode:false}") boolean darkMode) {
flags.put(FeatureFlag.NEW_CHECKOUT_FLOW, newCheckout);
flags.put(FeatureFlag.AI_RECOMMENDATIONS, aiRec);
flags.put(FeatureFlag.DARK_MODE, darkMode);
}
public boolean isEnabled(FeatureFlag flag) {
return flags.getOrDefault(flag, false);
}
}
Использование в коде:
if (featureFlagService.isEnabled(FeatureFlag.NEW_CHECKOUT_FLOW)) {
// новый поток оформления заказа
} else {
// старый поток
}
Плюсы: простота, нет лишних зависимостей. Минусы: чтобы изменить значение, нужно перезапустить приложение. Это не совсем "без перезапуска", но для небольших проектов — норм.
Динамические флаги: база данных или Redis
Если хотите менять флаги на лету — храните их в БД или Redis. Сделаем таблицу:
CREATE TABLE feature_flags (
name VARCHAR(100) PRIMARY KEY,
enabled BOOLEAN NOT NULL DEFAULT FALSE,
updated_at TIMESTAMP
);
Сервис с кэшированием (чтобы не долбить БД при каждом запросе):
@Service
public class DynamicFeatureFlagService {
private final JdbcTemplate jdbcTemplate;
private final CacheManager cacheManager;
public boolean isEnabled(String flagName) {
Cache cache = cacheManager.getCache("featureFlags");
Boolean cached = cache.get(flagName, Boolean.class);
if (cached != null) return cached;
Boolean dbValue = jdbcTemplate.queryForObject(
"SELECT enabled FROM feature_flags WHERE name = ?",
Boolean.class, flagName);
cache.put(flagName, dbValue != null && dbValue);
return dbValue != null && dbValue;
}
@CacheEvict(value = "featureFlags", key = "#flagName")
public void setFlag(String flagName, boolean enabled) {
jdbcTemplate.update(
"INSERT INTO feature_flags (name, enabled, updated_at) VALUES (?, ?, NOW()) " +
"ON CONFLICT (name) DO UPDATE SET enabled = ?, updated_at = NOW()",
flagName, enabled, enabled);
}
}
Теперь достаточно вызвать setFlag("NEW_CHECKOUT_FLOW", true) через REST-эндпоинт, и фича включится без перезапуска. Кэш протухнет через настроенное время (или инвалидируется сразу).
Промышленный стандарт: Togglz в Spring Boot
Изобретать велосипед не обязательно. Библиотека Togglz специально создана для feature flags в Java. Она дружит со Spring Boot, поддерживает несколько бэкендов (Properties, DB, Redis, StateFile) и умеет rollout по процентам.
Подключаем зависимость:
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-spring-boot-starter</artifactId>
<version>3.3.3</version>
</dependency>
Определяем enum флагов:
import org.togglz.core.Feature;
import org.togglz.core.annotation.EnabledByDefault;
import org.togglz.core.annotation.Label;
public enum MyFeatures implements Feature {
@Label("Новый checkout")
NEW_CHECKOUT,
@EnabledByDefault
@Label("Тёмная тема")
DARK_MODE,
@Label("AI рекомендации (только 10%)")
AI_RECOMMENDATIONS;
}
Конфигурация в application.yml:
togglz:
features:
NEW_CHECKOUT:
enabled: false
DARK_MODE:
enabled: true
AI_RECOMMENDATIONS:
enabled: true
auto-config: true
strategy: gradual
parameter: 10
Использование в коде — через статический метод:
import static org.togglz.core.manager.FeatureManagerFactory.getFeatureManager;
if (getFeatureManager().isActive(MyFeatures.AI_RECOMMENDATIONS)) {
// показать AI-рекомендации
}
Или через инжект FeatureManager:
@Service
public class RecommendationService {
@Autowired
private FeatureManager featureManager;
public List<Product> getRecommendations() {
if (featureManager.isActive(MyFeatures.AI_RECOMMENDATIONS)) {
// сложная AI-логика
}
return Collections.emptyList();
}
}