Skip to content

Best Practices ​

Recommendations for working with feature flags in можно.: from naming conventions to cleanup strategies.

Flag Naming ​

A good flag name is self-documenting and unambiguous. A bad one requires telepathy.

Naming Convention ​

<functionality>-<action/status>
PatternExampleGood/Bad
new-componentnew-checkoutGood
feature-enabledai-search-enabledGood
kill-componentkill-payment-gwGood
experiment-descriptionexp-cta-colorGood
flag1—Bad
test—Bad
feature_flag_new—Bad (uninformative)

Recommendations ​

RuleExample
kebab-casenew-checkout, dark-mode-rollout
Latin letters, digits, and hyphens onlyapi-v2, search-v3
Don't use flag or feature in the key❌ feature-new-checkout-flag → ✅ new-checkout
Kill switch — prefix kill-kill-payment-gateway, kill-third-party-api
Experiments — prefix exp-exp-pricing-layout, exp-cta-placement
Temporary features — prefix tmp-tmp-holiday-banner-2026
Permanent configuration — prefix cfg-cfg-rate-limit, cfg-max-upload-size

Naming Flag Keys ​

RuleGoodBad
Short meaningful identifiersA, B, control, treatmentvariant1, variant2
Control group — control or Acontrolold, current
Explain variants in the flag descriptionA = old design, B = new—

When to Archive vs Delete ​

CriterionArchiveDelete
Flag served its purpose, old code removed✅ Yes❌ No
Audit history must be preserved✅ Yes❌ No
Experiment flag, never went to production❌ No✅ Yes
Flag created by mistake (typo in key)❌ No✅ Yes
Test flag for local development❌ No✅ Yes
Duplicate flag❌ No✅ Yes

Tip: Default rule — archive. Deletion is irreversible. If in doubt, archive and delete after a month if the flag is truly not needed.

Permission Model ​

можно. uses a role-based access model with the hierarchy ADMIN → DEVELOPER → VIEWER (each role includes the privileges of lower roles):

ActionAdminDeveloperViewer
View flags and segments✅✅✅
View and export audit log✅✅✅
Create flags✅✅❌
Modify strategies and targeting✅✅❌
Archive flags✅✅❌
Manage segments✅✅❌
Delete flags✅✅❌
Manage environments✅❌❌
Manage API keys✅❌❌
Manage users✅❌❌
Manage integrations (webhooks)✅❌❌

Role Recommendations ​

PrincipleDescription
Least privilegeDeveloper cannot manage keys, users, or environments
Infrastructure — admin onlyAPI keys, users, environments, and integrations are admin-only
Viewer for external partiesAuditors, product managers — view only
Regular auditReview the user list and roles quarterly

Cleanup Strategy ​

Flags left in code after full rollout create flag debt — technical debt specific to feature flag systems.

Signs of Flag Debt ​

  • if (flag) constructs with dead old-code branches
  • Flags at 100% for over a month
  • Complex dependency chains between flags
  • Code that is hard to understand without knowing flag state

Cleanup Process ​

graph TD
    A[Flag at 100%] --> B{> 2 weeks?}
    B -->|No| C[Wait]
    B -->|Yes| D[Remove old code]
    D --> E[Merge PR]
    E --> F[Archive flag]
    F --> G{Month passed?}
    G -->|Yes| H[Delete flag]
    G -->|No| F

Flag Cleanup Checklist ​

  1. Flag at 100% for at least 2 weeks
  2. Metrics are stable — no regressions
  3. Old code not needed — no rollback planned
  4. Remove if (flag): keep only the new code, delete old code
  5. Remove SDK import/dependency if this was the last flag
  6. Archive the flag in the dashboard
  7. Document the removal in the flag description: date, reason

Architecture Patterns ​

Patterns for Organizing Flags in Code ​

PatternCodeWhen to Use
Inlineif (client.isEnabled("flag", ctx)) { ... }Single flags, quick start
Feature WrapperfeatureService.ifEnabled("flag", ctx, () -> newCode())Many flags in one service — eliminates repeated if
Context FactoryMozhnoContextFactory.forUser(user)Same attribute set passed to dozens of calls
MiddlewareHTTP/gRPC interceptor adding attributes to contextAttributes from request headers (userId, tenantId, country)
ContextProviderclient auto-injects context via MozhnoContextProviderSpring apps — no need to pass context to every isEnabled()

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

// Usage:
var result = featureService.ifEnabled("new-checkout", ctx,
    () -> processNew(order),
    () -> processOld(order)
);

Example: Context Factory (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();
    }
}

Example: MozhnoContextProvider (Java) ​

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

// Context is automatically injected:
boolean enabled = client.isEnabled("new-checkout");

Testing with Feature Flags ​

Unit Testing ​

Test both code paths — with and without the flag:

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

Tip: Mock the SDK client in tests, not the можно. server. Tests should be fast and network-independent.

Anti-patterns ​

Anti-patternWhy It's BadHow to Fix
Flag on flagif (flagA && flagB) — impossible to debugMerge into a segment or single flag
Flags in loopsChecking flag on every iteration — overheadCheck flag before the loop
Flag as configif (flag) timeout = 30 else timeout = 60Use actual config, not a feature flag
Eternal flagsFlag exists 6+ monthsSchedule removal or mark as permanent
Ownerless flagsNo one is responsible for cleanupAssign an owner in the flag description
Context copy-pasteDuplicated MozhnoContext.builder()...Extract to factory method or middleware

Next Steps ​

Released under the BSL 1.1 License.