Feature Flag-Driven Development: Trunk-Based Development, Flag Lifecycle, and Cleanup
Implement feature flag-driven development: trunk-based development with flags, flag lifecycle management, targeting rules, automated cleanup, and preventing flag debt from accumulating.
Feature Flag-Driven Development: Trunk-Based Development, Flag Lifecycle, and Cleanup
Feature flags enable continuous delivery without big-bang releases. Instead of maintaining long-lived feature branches that diverge from main and require painful merges, you merge to main daily โ with new code behind a flag that's off for everyone. When you're ready to ship, you flip the flag. When the feature is stable, you remove the flag. Simple in principle; messy in practice.
The mess comes from flag debt: flags that stay in the codebase forever, flags with overlapping rules nobody understands, and flags that control behavior in production that engineers are afraid to touch. This post covers the patterns that keep flags manageable.
Why Trunk-Based Development
โ Feature Branch Model (common but painful):
main โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบ main
โโโ feature/checkout โโโโโโโโโโโโโโโโ (3 weeks, painful merge)
โโโ feature/payments โโโโโโโโโโโ (2 weeks, conflicts with checkout)
โ
Trunk-Based Development with Flags:
main โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบ main
(everyone merges here daily; flags gate incomplete features)
Benefits of trunk-based:
- Continuous integration: merging daily means small diffs, no merge conflicts
- Fast feedback: code in production (behind a flag) sooner
- Simpler branching: one long-lived branch (main), short-lived branches (<1 day)
- Safer rollouts: flip a flag rather than deploy to roll back
Flag Types and Their Lifespans
Not all flags are created equal. Mixing types causes confusion:
| Type | Purpose | Lifespan | Example |
|---|---|---|---|
| Release flag | Gate incomplete feature | Daysโweeks | new-checkout-flow |
| Experiment flag | A/B test | Daysโweeks | checkout-button-color |
| Ops flag | Kill switch / circuit breaker | Long-lived | enable-email-sends |
| Permission flag | Feature access by plan/role | Long-lived | advanced-analytics |
Rule: Release and experiment flags should have an expiry date set at creation. Ops and permission flags are permanent (documented as such).
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Flag Implementation Patterns
Server-Side Flag Evaluation (TypeScript)
// src/lib/flags.ts
// Using LaunchDarkly SDK (same pattern works for Unleash, Flagsmith, OpenFeature)
import { LDClient, init as initLD } from '@launchdarkly/node-server-sdk';
let client: LDClient;
export async function initFlags(): Promise<void> {
client = initLD(process.env.LAUNCHDARKLY_SDK_KEY!);
await client.waitForInitialization({ timeout: 5 });
}
// User context for targeting
export interface UserContext {
userId: string;
email: string;
plan: 'free' | 'pro' | 'enterprise';
country: string;
betaTester?: boolean;
}
// Type-safe flag evaluation
export async function isEnabled(
flagKey: string,
user: UserContext,
defaultValue = false,
): Promise<boolean> {
try {
return await client.variation(flagKey, {
kind: 'user',
key: user.userId,
email: user.email,
custom: {
plan: user.plan,
country: user.country,
betaTester: user.betaTester ?? false,
},
}, defaultValue);
} catch {
// Flag evaluation failure โ safe default
return defaultValue;
}
}
// String variant (for A/B tests with multiple variants)
export async function getVariant<T extends string>(
flagKey: string,
user: UserContext,
defaultValue: T,
): Promise<T> {
return client.variation(flagKey, { kind: 'user', key: user.userId }, defaultValue) as Promise<T>;
}
Using Flags in API Handlers
// src/api/checkout.ts
import { isEnabled } from '@/lib/flags';
export async function handleCheckout(req: FastifyRequest, reply: FastifyReply) {
const user = req.user!;
// โ
Good: flag wraps specific behavior, not entire handler
const useNewTaxEngine = await isEnabled('new-tax-engine', user);
const tax = useNewTaxEngine
? await calculateTaxV2(req.body.items, user.country)
: await calculateTaxV1(req.body.items, user.country);
// โ
Good: flag evaluation at the edge of behavior change
const order = await createOrder({
...req.body,
tax,
});
// โ
Good: logging flag state for debugging
req.log.info({ flagState: { newTaxEngine: useNewTaxEngine } }, 'Checkout completed');
return { orderId: order.id, total: order.total };
}
Client-Side Flags in React
// src/hooks/useFlag.ts
import { useLDClient } from 'launchdarkly-react-client-sdk';
import { useCallback } from 'react';
export function useFlag(flagKey: string, defaultValue = false): boolean {
const client = useLDClient();
if (!client) return defaultValue;
return client.variation(flagKey, defaultValue);
}
// Provider setup in App.tsx
import { LDProvider } from 'launchdarkly-react-client-sdk';
export function App() {
const user = useAuth();
return (
<LDProvider
clientSideID={process.env.NEXT_PUBLIC_LD_CLIENT_ID!}
context={{
kind: 'user',
key: user.id,
email: user.email,
custom: { plan: user.plan, betaTester: user.betaTester },
}}
>
<Router />
</LDProvider>
);
}
// Usage in component
function CheckoutButton() {
const useNewUI = useFlag('new-checkout-ui');
if (useNewUI) {
return <NewCheckoutButton />;
}
return <LegacyCheckoutButton />;
}
Targeting Rules: Common Patterns
// Targeting rule patterns (configured in LaunchDarkly UI or Unleash)
// 1. Beta opt-in
// Rule: betaTester = true โ ON
// 2. Gradual percentage rollout
// Rule: 10% of users โ ON (deterministic by userId hash)
// Then: 25% โ 50% โ 100% over days
// 3. Plan-based gating
// Rule: plan IN [pro, enterprise] โ ON
// Else โ OFF (feature is paid-only)
// 4. Internal dogfood
// Rule: email matches @mycompany.com โ ON
// Else โ OFF (test with your own team first)
// 5. Geography
// Rule: country NOT IN [EU] โ ON
// (While waiting for GDPR compliance work)
// 6. Canary deployment (combine with server targeting)
// Rule: serverId = "canary-pod-1" โ ON (target specific pod)
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Flag Lifecycle: Creation to Removal
## Flag Lifecycle Process
### 1. Creation
- Name format: `verb-noun` or `noun-adjective` (e.g., `enable-new-checkout`, `checkout-v2`)
- Add to flag registry (below)
- Set type: release | experiment | ops | permission
- Set expiry date for release/experiment flags (when to remove code)
- Set owner (team or person responsible for cleanup)
- Default: OFF in production
### 2. Development
- Code behind flag merged to main daily
- Integration tests run both flag=ON and flag=OFF paths
- Flag evaluated once per request boundary (not deep in business logic)
### 3. Rollout
- Internal team first (dogfood rule)
- Beta testers (opt-in rule)
- 10% โ 25% โ 50% โ 100% (scheduled or manual)
- Monitor error rates, latency, and business metrics per variant
### 4. Graduation (flag = 100% ON, stable)
- Create ticket: "Remove flag: [flag-name]"
- Remove flag evaluation from code
- Remove the old code path (not just the flag check)
- Delete flag from flag management platform
- Update flag registry
### 5. Rollback (if needed)
- Flip flag to OFF instantly โ no deployment needed
- Investigate root cause
- Fix in code, re-roll out
Flag Registry: Preventing Flag Debt
// src/flags/registry.ts
// Single source of truth for all flags in the codebase
export type FlagType = 'release' | 'experiment' | 'ops' | 'permission';
interface FlagDefinition {
key: string;
type: FlagType;
description: string;
owner: string; // Team or GitHub username
createdAt: string; // ISO date
expiresAt?: string; // ISO date โ required for release/experiment
defaultValue: boolean;
rolloutStatus?: string; // "10%" | "beta" | "100%" | "cleanup"
}
// โ
Every flag in the codebase is registered here
export const FLAGS = {
NEW_CHECKOUT_UI: {
key: 'new-checkout-ui',
type: 'release',
description: 'Redesigned checkout flow with single-page layout',
owner: 'team-payments',
createdAt: '2026-07-15',
expiresAt: '2026-09-01', // Remove by this date
defaultValue: false,
rolloutStatus: '25%',
},
ADVANCED_ANALYTICS: {
key: 'advanced-analytics',
type: 'permission',
description: 'Advanced analytics dashboard โ Pro and Enterprise plans only',
owner: 'team-growth',
createdAt: '2026-01-10',
// No expiresAt โ permanent permission flag
defaultValue: false,
rolloutStatus: 'plan-based',
},
DISABLE_EMAIL_SENDS: {
key: 'disable-email-sends',
type: 'ops',
description: 'Kill switch: disables all transactional email sends',
owner: 'team-infra',
createdAt: '2025-11-01',
defaultValue: false, // OFF = emails enabled (inverted for kill switch)
rolloutStatus: 'ops',
},
} as const satisfies Record<string, FlagDefinition>;
export type FlagKey = typeof FLAGS[keyof typeof FLAGS]['key'];
Automated Staleness Detection
// scripts/check-stale-flags.ts
import { FLAGS } from '../src/flags/registry';
const today = new Date();
const WARNING_DAYS = 14; // Warn 2 weeks before expiry
for (const [name, flag] of Object.entries(FLAGS)) {
if (!flag.expiresAt) continue; // Permanent flags skip
const expiry = new Date(flag.expiresAt);
const daysUntilExpiry = Math.floor((expiry.getTime() - today.getTime()) / 86400000);
if (daysUntilExpiry < 0) {
console.error(`๐จ EXPIRED: ${flag.key} (owner: ${flag.owner}) expired ${-daysUntilExpiry} days ago`);
process.exitCode = 1;
} else if (daysUntilExpiry <= WARNING_DAYS) {
console.warn(`โ ๏ธ EXPIRING SOON: ${flag.key} expires in ${daysUntilExpiry} days (owner: ${flag.owner})`);
}
}
# .github/workflows/flag-staleness.yml
name: Flag Staleness Check
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9am
push:
paths: ['src/flags/registry.ts']
jobs:
check-flags:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npm ci
- run: npx ts-node scripts/check-stale-flags.ts
Testing Both Flag Paths
// src/api/__tests__/checkout.test.ts
import { isEnabled } from '@/lib/flags';
jest.mock('@/lib/flags');
const mockIsEnabled = jest.mocked(isEnabled);
describe('Checkout handler', () => {
describe('with new-tax-engine flag OFF (default)', () => {
beforeEach(() => {
mockIsEnabled.mockResolvedValue(false);
});
it('uses v1 tax calculation', async () => {
// test with old behavior
});
});
describe('with new-tax-engine flag ON', () => {
beforeEach(() => {
mockIsEnabled.mockResolvedValue(true);
});
it('uses v2 tax calculation', async () => {
// test with new behavior
});
});
});
Working With Viprasol
We implement feature flag-driven development workflows โ from flag infrastructure setup through registry design, lifecycle automation, and test patterns.
What we deliver:
- LaunchDarkly or Unleash setup and SDK integration
- Flag registry design with staleness detection CI job
- Trunk-based development workflow setup (branch protection + flag conventions)
- TypeScript type-safe flag evaluation patterns
- Testing patterns for flag-gated code paths
โ Discuss your continuous delivery practices โ DevOps and platform engineering services
See Also
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.