Skip to content

Best Practices

This guide covers naming conventions, permission models, cleanup strategies, testing patterns, and flag debt management for teams using можно at scale.

Naming Conventions

A consistent naming scheme keeps flags discoverable and prevents collisions across teams and services.

<domain>_<feature>_<detail>
ComponentDescriptionExample
domainService or business areacheckout, auth, search, billing
featureThe feature being gatedredesign, upsell, darkmode
detailVariant or version (optional)v2, experiment_a, beta

Examples:

GoodPoorWhy
checkout-one-click-v2flag_42Descriptive vs. opaque
auth-sso-samlnew_authSpecific vs. vague
search-ranking-ml-v3search_v3Includes purpose
billing-tax-eu-vattax_stuffScoped and precise

Naming Rules

  • Use kebab-case (lowercase with hyphens).
  • Start with the service or domain name.
  • Include a version suffix (-v2, -v3) when iterating on a feature.
  • Use descriptive action words for kill switches: kill-payment-provider-x.

Tagging Strategy

Supplement keys with tags for cross-cutting organisation:

team:platform
owner:alice
type:kill-switch
env:production
service:api-gateway
temporary:true
expires:2026-09-01

Tip: The temporary and expires tags are community conventions. There is no automatic expiry in можно — use these tags to identify flags for manual cleanup.

When to Archive vs Delete

CriterionArchiveDelete
Flag still referenced in any codebase
Feature fully rolled out and stable
Need to preserve audit history
Possible future rollback needed
Flag was an aborted experiment✅ (if code removed)
Flag created in error (never used)
All code references have been removed
Flag key conflicts with a new flag

Default rule: Archive first. Delete only when you are certain the flag key will never be needed again.

Warning: Deleting a flag that is still referenced in application code will cause SDK evaluations to return an error for that key. Remove all code references before deletion.

Permission Model

можно uses a role-based access model with the hierarchy ADMINDEVELOPERVIEWER (each role inherits the permissions of those below it). Assign the minimum permissions necessary for each user.

Roles

RoleViewManage flags/segments/strategiesManage keys, users, environmentsTypical Assignee
ViewerSupport, analysts, read-only dashboards
DeveloperDevelopers, QA engineers
AdminTeam leads, platform engineers

API Key Scopes

Create separate API keys with limited scopes for different environments and services:

Key NameEnvironmentPermissionsUsed By
sdk-productionProductionflags:read, segments:readProduction application servers
sdk-stagingStagingflags:read, segments:readStaging application servers
ci-cd-botAllflags:read, flags:writeCI/CD pipelines
monitoring-botProductionflags:readMonitoring dashboards

Tip: Never share API keys between environments. A compromised staging key should not affect production flags.

JWT vs API Keys

Auth MethodUse CaseExample
JWTDashboard access (human users)Web UI login sessions
API KeySDK and API access (machine clients)Java SDK, CI/CD scripts

Cleanup Strategies

Feature flags accumulate over time. Without a cleanup process, you end up with flag debt: stale flags that clutter the dashboard, slow down evaluation, and confuse developers.

Flag Lifecycle Timeline

gantt
    title Flag Lifecycle
    dateFormat  YYYY-MM-DD
    section checkout_v2
    Create & develop     :2026-01-01, 14d
    Rollout (0→100%)     :2026-01-15, 30d
    Stabilize            :2026-02-15, 14d
    Remove code refs     :2026-03-01, 7d
    Archive flag         :milestone, 2026-03-08, 1d

Weekly Flag Review Checklist

Run through this checklist weekly for all flags you own:

  1. Rollout at 100% for > 2 weeks? → Schedule code removal and archive the flag.
  2. Flag has not been evaluated in > 30 days? → Investigate. It may be dead code.
  3. Flag has an expires tag in the past? → Archive or extend the expiry.
  4. Flag is archived and > 60 days old? → Delete permanently if code removed.
  5. Flag description is empty or outdated? → Update it.

Automated Cleanup Script

Use the API to identify stale flags:

bash
#!/bin/bash
# List flags not evaluated in the last 30 days
curl "https://your-instance/api/v1/flags?lastEvaluatedBefore=$(date -d '30 days ago' -u +%Y-%m-%dT%H:%M:%SZ)" \
  -H "Authorization: Bearer $JWT_TOKEN" \
  | jq '.items[] | {key: .key, lastEvaluated: .lastEvaluatedAt, state: .state}'

Code Removal Pattern

Before archiving, remove the flag from your application code:

java
// Before: flag-guarded code
if (client.isEnabled("checkout_v2", context)) {
    return newCheckoutFlow();
}
return oldCheckoutFlow();

// After: flag removed, new code is the default
return newCheckoutFlow();

Tip: When removing a flag, do it in a separate PR from the feature work. This makes it easy to audit and revert if needed.

Code Architecture Patterns

How to Organize Flags in Your Codebase

PatternExampleBest For
Inlineif (client.isEnabled("flag", ctx)) { ... }Single flags, quick start
Feature WrapperfeatureService.ifEnabled("flag", ctx, () -> newCode())Many flags in one service — eliminates repeated if statements
Context FactoryMozhnoContextFactory.forUser(user)Same attributes passed to dozens of flag checks
MiddlewareHTTP/gRPC interceptor adding attributes to contextAttributes from request headers (userId, tenantId, country)

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

Middleware (Express)

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

Testing with Feature Flags

Unit Testing

Mock the SDK client in unit tests to control flag values directly:

Java:

java
@Test
void testNewCheckoutFlow() {
    MozhnoClient mockClient = mock(MozhnoClient.class);
    when(mockClient.isEnabled(eq("checkout_v2"), any())).thenReturn(true);

    CheckoutService service = new CheckoutService(mockClient);
    Result result = service.checkout(cart);

    assertThat(result).isInstanceOf(NewCheckoutResult.class);
}

JavaScript:

js
import { MozhnoClient } from "@mozhno/client-js";

jest.mock("@mozhno/client-js");

test("shows new checkout when flag is enabled", async () => {
  MozhnoClient.mockImplementation(() => ({
    isEnabled: jest.fn().mockReturnValue(true),
  }));

  const result = await renderCheckout();
  expect(result.type).toBe("new_checkout");
});

Integration Testing

Test both flag states in your CI pipeline:

yaml
- name: Test with flag enabled
  run: MOZHNO_FLAG_overrides='{"checkout_v2":true}' npm test

- name: Test with flag disabled
  run: MOZHNO_FLAG_overrides='{"checkout_v2":false}' npm test

Testing in Staging

Before enabling a flag in production:

  1. Enable the flag at 100% in staging.
  2. Run end-to-end tests against staging.
  3. Manually verify the feature with targeted rules (userId equals "qa-user").
  4. Test edge cases: missing context attributes, connection failures (SDK should return defaults).

Testing Rollback

Regularly test that toggling a flag off correctly restores the old behaviour:

bash
# Test rollback in staging
curl -X PATCH "$MOZHNO_STAGING_URL/api/v1/flags/$FLAG_KEY" \
  -H "Authorization: Bearer $MOZHNO_JWT" \
  -H "Content-Type: application/json" \
  -d '{"archived": true}'

# Run smoke tests — should see old behaviour
npm run smoke-test

# Re-enable
curl -X PATCH "$MOZHNO_STAGING_URL/api/v1/flags/$FLAG_KEY" \
  -H "Authorization: Bearer $MOZHNO_JWT" \
  -H "Content-Type: application/json" \
  -d '{"state": "ACTIVE"}'

Flag Debt Management

Flag debt is the accumulated cost of maintaining stale or unnecessary feature flags. Left unchecked, it leads to:

  • Increased SDK evaluation overhead.
  • Dashboard clutter and confusion.
  • Risk of accidentally toggling a forgotten flag.
  • Bloated codebase with dead code paths.

Measuring Flag Debt

Track these metrics in your team's dashboard:

MetricTargetHow to Measure
Total active flags< 50 per serviceDashboard count
Flags at 100% rollout > 30 days0API query
Flags never evaluated in 60 days0API query (lastEvaluatedBefore)
Archived flags > 90 days0Dashboard filter by state
Average flag lifetime< 90 daysCreation-to-archive duration

Flag Debt Reduction Workflow

flowchart TD
    Review[Weekly flag review] --> Identify[Identify stale flags]
    Identify --> RemoveCode[Remove code references]
    RemoveCode --> Archive[Archive flag]
    Archive --> Wait[Wait 1 release cycle]
    Wait --> Verify{Any issues?}
    Verify -->|No| Delete[Delete permanently]
    Verify -->|Yes| Restore[Restore flag]

Preventing Flag Debt

  • Set an expires tag on every temporary flag during creation.
  • Create a ticket in your issue tracker for flag removal when the flag is created. Link the ticket to the flag key.
  • Enforce flag ownership: every flag must have an owner tag. The owner is responsible for its entire lifecycle.
  • Include flag removal in Definition of Done: a feature is not "done" until the flag is archived and code references removed.
  • Limit total active flags: agree on a team maximum and treat exceeding it as a blocker.

Environment Strategy

EnvironmentInstanceFlag Behaviour
Local developmentLocal Docker (make dev)Flags can be toggled freely
CIEphemeralUse SDK test mode or override file
StagingShared staging instanceMirror of production flags; test rollouts here first
ProductionProduction instanceControlled changes only; require PR approval for flag modifications

Tip: Use the make web-dev command to run the dashboard locally. The Swagger UI is available at http://localhost:8080/swagger-ui.html for API exploration.

Next Steps

Released under the AGPL v3.0 License.