Back to Blog

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.

Viprasol Tech Team
August 14, 2026
13 min read

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:

TypePurposeLifespanExample
Release flagGate incomplete featureDaysโ€“weeksnew-checkout-flow
Experiment flagA/B testDaysโ€“weekscheckout-button-color
Ops flagKill switch / circuit breakerLong-livedenable-email-sends
Permission flagFeature access by plan/roleLong-livedadvanced-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

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol ยท Web Development

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.