Feature flags в Java: как включать фичи без перезапуска приложения

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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();
    }
}

                
                

Блоги

Рекомендаций по книгам