Skip to content

Лучшие практики

Свод рекомендаций по работе с фиче-флагами в можно.: от именования до стратегии очистки и управления флаговым долгом.

Именование флагов

Хорошее имя флага — самодокументированное и однозначное. Плохое — требует телепатии.

Конвенция именования

<функциональность>-<действие/статус>
ШаблонПримерХорошо/Плохо
новый-компонентnew-checkoutХорошо
фича-enabledai-search-enabledХорошо
kill-компонентkill-payment-gwХорошо
эксперимент-описаниеexp-cta-colorХорошо
flag1Плохо
testПлохо
feature_flag_newПлохо (неинформативно)

Рекомендации

ПравилоПример
kebab-casenew-checkout, dark-mode-rollout
Только латиница, цифры и дефисыapi-v2, search-v3
Не используйте flag или feature в ключеfeature-new-checkout-flag → ✅ new-checkout
Kill switch — префикс kill-kill-payment-gateway, kill-third-party-api
Эксперименты — префикс exp-exp-pricing-layout, exp-cta-placement
Временные фичи — префикс tmp-tmp-holiday-banner-2026
Перманентные конфигурации — префикс cfg-cfg-rate-limit, cfg-max-upload-size

Именование флаговых ключей

ПравилоХорошоПлохо
Короткие осмысленные идентификаторыA, B, control, treatmentvariant1, variant2
Контрольная группа — control или Acontrolold, current
В описании флага — расшифровка вариантовA = старый дизайн, B = новый

Когда архивировать, а когда удалять

КритерийАрхивУдаление
Флаг отработал, старый код удалён✅ Да❌ Нет
Нужна история изменений для аудита✅ Да❌ Нет
Экспериментальный флаг, не пошёл в прод❌ Нет✅ Да
Флаг создан по ошибке (опечатка в ключе)❌ Нет✅ Да
Тестовый флаг для локальной разработки❌ Нет✅ Да
Флаг-дубликат❌ Нет✅ Да

Совет: Правило по умолчанию — архивируйте. Удаление необратимо. Если сомневаетесь, отправьте в архив и удалите через месяц, если флаг точно не понадобится.

Модель разрешений (Permission Model)

можно. использует ролевую модель доступа с иерархией ADMINDEVELOPERVIEWER (каждая роль включает права нижестоящих):

ДействиеAdminDeveloperViewer
Просмотр флагов и сегментов
Просмотр и экспорт аудит-лога
Создание флагов
Изменение стратегий и таргетинга
Архивация флагов
Управление сегментами
Удаление флагов
Управление окружениями
Управление API-ключами
Управление пользователями
Управление интеграциями (вебхуками)

Рекомендации по ролям

ПринципОписание
Принцип наименьших привилегийDeveloper не имеет доступа к управлению ключами, пользователями и окружениями
Управление инфраструктурой — только AdminAPI-ключи, пользователи, окружения и интеграции доступны только роли Admin
Viewer для стороннихАудиторы, менеджеры продукта — только просмотр
Регулярный аудитРаз в квартал проверяйте список пользователей и их роли

Стратегия очистки флагов

Флаги, оставленные в коде после полного роллаута, создают флаговый долг (flag debt) — технический долг, характерный для систем с фиче-флагами.

Признаки флагового долга

  • Условные конструкции if (flag) с мёртвой веткой старого кода
  • Флаги, включённые на 100% больше месяца
  • Сложные цепочки зависимостей между флагами
  • Код, который невозможно понять без знания состояния флагов

Процесс очистки

graph TD
    A[Флаг на 100%] --> B{Прошло > 2 недель?}
    B -->|Нет| C[Ждём]
    B -->|Да| D[Удаляем старый код]
    D --> E[Мержим PR]
    E --> F[Архивируем флаг]
    F --> G{Прошёл месяц?}
    G -->|Да| H[Удаляем флаг]
    G -->|Нет| F

Чек-лист очистки флага

  1. Флаг на 100% минимум 2 недели
  2. Метрики стабильны — нет регрессий
  3. Старый код не нужен — никто не планирует откат
  4. Удалить if (flag): оставить только новый код, удалить старый
  5. Удалить импорт/зависимость SDK, если флаг был последним
  6. Заархивировать флаг в веб-панели
  7. Документировать удаление в описании флага: дата, причина

Архитектурные паттерны

Паттерны организации флагов в коде

ПаттернКодКогда применять
Инлайнif (client.isEnabled("flag", ctx)) { ... }Единичные флаги, быстрый старт
Feature WrapperfeatureService.ifEnabled("flag", ctx, () -> newCode())Много флагов в одном сервисе — убирает повторяющийся if
Фабрика контекстаMozhnoContexFactory.forUser(user)Один и тот же набор атрибутов передаётся в десятках мест
MiddlewareПерехватчик HTTP/gRPC, добавляющий атрибуты в контекстАтрибуты из заголовков запроса (userId, tenantId, country)
ContextProviderclient сам получает контекст через MozhnoContextProviderSpring-приложения — не передавать контекст в каждый isEnabled()

Пример: Feature Wrapper на Java

java
@Service
public class FeatureService {
    private final MozhnoClient client;

    public <T> T ifEnabled(String flag, MozhnoContext ctx,
                           Supplier<T> newCode, Supplier<T> oldCode) {
        return client.isEnabled(flag, ctx) ? newCode.get() : oldCode.get();
    }
}

// Использование:
var result = featureService.ifEnabled("new-checkout", ctx,
    () -> processNew(order),   // новый код
    () -> processOld(order)    // старый код
);

Пример: Фабрика контекста на Java

java
public class MozhnoContextFactory {
    public static MozhnoContext forRequest(HttpServletRequest req) {
        return MozhnoContext.builder()
            .userId(req.getHeader("X-User-Id"))
            .addProperty("tenantId", req.getHeader("X-Tenant-Id"))
            .addProperty("country", req.getHeader("X-Country"))
            .addProperty("device", req.getHeader("X-Device"))
            .build();
    }
}

Пример: MozhnoContextProvider

java
@Configuration
public class MozhnoConfig {

    @Bean
    public MozhnoContextProvider contextProvider() {
        return () -> {
            var request = ((ServletRequestAttributes)
                RequestContextHolder.currentRequestAttributes()).getRequest();
            return MozhnoContext.builder()
                .userId(request.getHeader("X-User-Id"))
                .addProperty("tenantId", request.getHeader("X-Tenant-Id"))
                .addProperty("country", request.getHeader("X-Country"))
                .build();
        };
    }
}

// Использование — контекст подставляется автоматически:
boolean enabled = client.isEnabled("new-checkout");

Тестирование с фиче-флагами

Модульное тестирование

Тестируйте обе ветки кода — с флагом и без флага:

java
@Test
void testNewCheckoutFlow() {
    var ctx = MozhnoContext.builder().userId("test-user").build();
    when(client.isEnabled("new-checkout", ctx)).thenReturn(true);

    var result = checkoutService.process(order, ctx);

    assertThat(result.getFlow()).isEqualTo("new");
}

@Test
void testOldCheckoutFlow() {
    var ctx = MozhnoContext.builder().userId("test-user").build();
    when(client.isEnabled("new-checkout", ctx)).thenReturn(false);

    var result = checkoutService.process(order, ctx);

    assertThat(result.getFlow()).isEqualTo("old");
}

Совет: Мокайте SDK-клиент в тестах, а не сервер можно.. Тесты должны быть быстрыми и не зависеть от сети.

Антипаттерны

АнтипаттернПочему плохоКак исправить
Флаг на флагеif (flagA && flagB) — невозможно отлаживатьОбъединить в сегмент или один флаг
Флаги в циклахПроверка флага на каждой итерации — накладные расходыПроверить флаг до цикла
Флаг как конфигif (flag) timeout = 30 else timeout = 60Использовать настоящий конфиг, не фиче-флаг
Вечные флагиФлаг существует 6+ месяцевЗапланировать удаление или пометить как перманентный
Флаги без владельцаНикто не отвечает за очисткуНазначить ответственного в описании флага
Копипаста контекстаДублирование MozhnoContext.builder()...Вынести в фабричный метод или middleware

Что дальше?

Released under the BSL 1.1 License.