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.
Recommended Pattern
<domain>_<feature>_<detail>| Component | Description | Example |
|---|---|---|
domain | Service or business area | checkout, auth, search, billing |
feature | The feature being gated | redesign, upsell, darkmode |
detail | Variant or version (optional) | v2, experiment_a, beta |
Examples:
| Good | Poor | Why |
|---|---|---|
checkout-one-click-v2 | flag_42 | Descriptive vs. opaque |
auth-sso-saml | new_auth | Specific vs. vague |
search-ranking-ml-v3 | search_v3 | Includes purpose |
billing-tax-eu-vat | tax_stuff | Scoped 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-01Tip: The
temporaryandexpirestags are community conventions. There is no automatic expiry in можно — use these tags to identify flags for manual cleanup.
When to Archive vs Delete
| Criterion | Archive | Delete |
|---|---|---|
| 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 ADMIN → DEVELOPER → VIEWER (each role inherits the permissions of those below it). Assign the minimum permissions necessary for each user.
Roles
| Role | View | Manage flags/segments/strategies | Manage keys, users, environments | Typical Assignee |
|---|---|---|---|---|
| Viewer | ✅ | ❌ | ❌ | Support, analysts, read-only dashboards |
| Developer | ✅ | ✅ | ❌ | Developers, QA engineers |
| Admin | ✅ | ✅ | ✅ | Team leads, platform engineers |
API Key Scopes
Create separate API keys with limited scopes for different environments and services:
| Key Name | Environment | Permissions | Used By |
|---|---|---|---|
sdk-production | Production | flags:read, segments:read | Production application servers |
sdk-staging | Staging | flags:read, segments:read | Staging application servers |
ci-cd-bot | All | flags:read, flags:write | CI/CD pipelines |
monitoring-bot | Production | flags:read | Monitoring dashboards |
Tip: Never share API keys between environments. A compromised staging key should not affect production flags.
JWT vs API Keys
| Auth Method | Use Case | Example |
|---|---|---|
| JWT | Dashboard access (human users) | Web UI login sessions |
| API Key | SDK 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:
- Rollout at 100% for > 2 weeks? → Schedule code removal and archive the flag.
- Flag has not been evaluated in > 30 days? → Investigate. It may be dead code.
- Flag has an
expirestag in the past? → Archive or extend the expiry. - Flag is archived and > 60 days old? → Delete permanently if code removed.
- Flag description is empty or outdated? → Update it.
Automated Cleanup Script
Use the API to identify stale flags:
#!/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:
// 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
| Pattern | Example | Best For |
|---|---|---|
| Inline | if (client.isEnabled("flag", ctx)) { ... } | Single flags, quick start |
| Feature Wrapper | featureService.ifEnabled("flag", ctx, () -> newCode()) | Many flags in one service — eliminates repeated if statements |
| Context Factory | MozhnoContextFactory.forUser(user) | Same attributes passed to dozens of flag checks |
| Middleware | HTTP/gRPC interceptor adding attributes to context | Attributes from request headers (userId, tenantId, country) |
Feature Wrapper (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)
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:
@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:
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:
- 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 testTesting in Staging
Before enabling a flag in production:
- Enable the flag at 100% in staging.
- Run end-to-end tests against staging.
- Manually verify the feature with targeted rules (
userId equals "qa-user"). - 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:
# 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:
| Metric | Target | How to Measure |
|---|---|---|
| Total active flags | < 50 per service | Dashboard count |
| Flags at 100% rollout > 30 days | 0 | API query |
| Flags never evaluated in 60 days | 0 | API query (lastEvaluatedBefore) |
| Archived flags > 90 days | 0 | Dashboard filter by state |
| Average flag lifetime | < 90 days | Creation-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
expirestag 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
ownertag. 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
| Environment | Instance | Flag Behaviour |
|---|---|---|
| Local development | Local Docker (make dev) | Flags can be toggled freely |
| CI | Ephemeral | Use SDK test mode or override file |
| Staging | Shared staging instance | Mirror of production flags; test rollouts here first |
| Production | Production instance | Controlled changes only; require PR approval for flag modifications |
Tip: Use the
make web-devcommand to run the dashboard locally. The Swagger UI is available athttp://localhost:8080/swagger-ui.htmlfor API exploration.
Next Steps
- Learn about the Java SDK or JavaScript SDK for application integration.
- Read the API Overview for programmatic flag management.
- Set up Audit Log monitoring to track who makes changes.