Feature Flags in Mobile Applications
Feature flags (also known as feature toggles) enable teams to deploy code changes without immediately exposing them to users. This approach allows for gradual rollouts, A/B testing, and quick feature rollbacks without requiring app store deployments.
Feature Flag Architecture
Flag Types
- Kill Switches: Emergency disable features
- Release Flags: Gradual feature rollouts
- Permission Flags: Role-based feature access
- Experiment Flags: A/B testing variations
- Operational Flags: Performance toggles
Implementation Strategies
1. Configuration-Based Flags
typescript
// Feature flag configuration
interface FeatureFlag {
key: string;
enabled: boolean;
rolloutPercentage: number;
targetAudience: string[];
conditions: FlagCondition[];
metadata: Record<string, any>;
}
interface FlagCondition {
type: 'user_id' | 'app_version' | 'device_type' | 'location';
operator: 'equals' | 'contains' | 'greater_than' | 'in_list';
value: any;
}
2. Runtime Evaluation Engine
typescript
class FeatureFlagEngine {
private flags: Map<string, FeatureFlag> = new Map();
private userContext: UserContext;
constructor(userContext: UserContext) {
this.userContext = userContext;
}
isEnabled(flagKey: string): boolean {
const flag = this.flags.get(flagKey);
if (!flag) return false;
return this.evaluateFlag(flag);
}
private evaluateFlag(flag: FeatureFlag): boolean {
// Base enabled check
if (!flag.enabled) return false;
// Rollout percentage check
if (!this.isInRollout(flag.rolloutPercentage)) return false;
// Condition evaluation
return this.evaluateConditions(flag.conditions);
}
private isInRollout(percentage: number): boolean {
const userId = this.userContext.userId;
const hash = this.hashUserId(userId);
return (hash % 100) < percentage;
}
private evaluateConditions(conditions: FlagCondition[]): boolean {
return conditions.every(condition => {
switch (condition.type) {
case 'user_id':
return this.evaluateUserCondition(condition);
case 'app_version':
return this.evaluateVersionCondition(condition);
case 'device_type':
return this.evaluateDeviceCondition(condition);
case 'location':
return this.evaluateLocationCondition(condition);
default:
return false;
}
});
}
}
Platform-Specific Implementations
Android Implementation (Kotlin)
kotlin
// Feature Flag Manager
class FeatureFlagManager private constructor(
private val context: Context,
private val remoteConfigService: RemoteConfigService
) {
companion object {
@Volatile
private var INSTANCE: FeatureFlagManager? = null
fun getInstance(context: Context, remoteConfigService: RemoteConfigService): FeatureFlagManager {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: FeatureFlagManager(context, remoteConfigService).also { INSTANCE = it }
}
}
}
private val localCache = FeatureFlagCache(context)
private val evaluationEngine = FlagEvaluationEngine()
suspend fun isFeatureEnabled(flagKey: String): Boolean {
return try {
val flag = getFlag(flagKey)
evaluationEngine.evaluate(flag, getUserContext())
} catch (e: Exception) {
Log.e("FeatureFlags", "Error evaluating flag $flagKey", e)
getDefaultValue(flagKey)
}
}
suspend fun getFeatureVariant(flagKey: String): String {
val flag = getFlag(flagKey)
return evaluationEngine.getVariant(flag, getUserContext())
}
private suspend fun getFlag(flagKey: String): FeatureFlag {
// Try local cache first
localCache.getFlag(flagKey)?.let { return it }
// Fetch from remote
val flag = remoteConfigService.getFlag(flagKey)
localCache.saveFlag(flag)
return flag
}
private fun getUserContext(): UserContext {
return UserContext(
userId = getCurrentUserId(),
deviceId = getDeviceId(),
appVersion = getAppVersion(),
deviceType = getDeviceType(),
location = getCurrentLocation()
)
}
}
// Usage in Activity/Fragment
class MainActivity : AppCompatActivity() {
private lateinit var featureFlagManager: FeatureFlagManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
featureFlagManager = FeatureFlagManager.getInstance(this, remoteConfigService)
lifecycleScope.launch {
if (featureFlagManager.isFeatureEnabled("new_ui_design")) {
enableNewUIDesign()
}
when (featureFlagManager.getFeatureVariant("checkout_flow")) {
"variant_a" -> showCheckoutVariantA()
"variant_b" -> showCheckoutVariantB()
else -> showDefaultCheckout()
}
}
}
}
iOS Implementation (Swift)
swift
// Feature Flag Manager
class FeatureFlagManager {
static let shared = FeatureFlagManager()
private let remoteConfigService: RemoteConfigService
private let localCache: FeatureFlagCache
private let evaluationEngine: FlagEvaluationEngine
private init() {
self.remoteConfigService = RemoteConfigService()
self.localCache = FeatureFlagCache()
self.evaluationEngine = FlagEvaluationEngine()
}
func isFeatureEnabled(_ flagKey: String) async -> Bool {
do {
let flag = try await getFlag(flagKey)
let userContext = getUserContext()
return evaluationEngine.evaluate(flag, context: userContext)
} catch {
print("Error evaluating flag \(flagKey): \(error)")
return getDefaultValue(flagKey)
}
}
func getFeatureVariant(_ flagKey: String) async -> String {
do {
let flag = try await getFlag(flagKey)
let userContext = getUserContext()
return evaluationEngine.getVariant(flag, context: userContext)
} catch {
return "default"
}
}
private func getFlag(_ flagKey: String) async throws -> FeatureFlag {
// Try local cache first
if let cachedFlag = localCache.getFlag(flagKey) {
return cachedFlag
}
// Fetch from remote
let flag = try await remoteConfigService.getFlag(flagKey)
localCache.saveFlag(flag)
return flag
}
private func getUserContext() -> UserContext {
return UserContext(
userId: getCurrentUserId(),
deviceId: getDeviceId(),
appVersion: getAppVersion(),
deviceType: getDeviceType(),
location: getCurrentLocation()
)
}
}
// Usage in View Controller
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Task {
if await FeatureFlagManager.shared.isFeatureEnabled("new_ui_design") {
enableNewUIDesign()
}
let checkoutVariant = await FeatureFlagManager.shared.getFeatureVariant("checkout_flow")
switch checkoutVariant {
case "variant_a":
showCheckoutVariantA()
case "variant_b":
showCheckoutVariantB()
default:
showDefaultCheckout()
}
}
}
}
React Native Implementation
typescript
// Feature Flag Hook
import { useState, useEffect, useContext } from 'react';
import { FeatureFlagManager } from './FeatureFlagManager';
export const useFeatureFlag = (flagKey: string): boolean => {
const [isEnabled, setIsEnabled] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(true);
useEffect(() => {
const checkFlag = async () => {
try {
const enabled = await FeatureFlagManager.isFeatureEnabled(flagKey);
setIsEnabled(enabled);
} catch (error) {
console.error(`Error checking feature flag ${flagKey}:`, error);
setIsEnabled(false);
} finally {
setIsLoading(false);
}
};
checkFlag();
}, [flagKey]);
return isEnabled;
};
export const useFeatureVariant = (flagKey: string): string => {
const [variant, setVariant] = useState<string>('default');
useEffect(() => {
const getVariant = async () => {
try {
const flagVariant = await FeatureFlagManager.getFeatureVariant(flagKey);
setVariant(flagVariant);
} catch (error) {
console.error(`Error getting feature variant ${flagKey}:`, error);
setVariant('default');
}
};
getVariant();
}, [flagKey]);
return variant;
};
// Feature Flag Component
export const FeatureFlag: React.FC<{
flagKey: string;
children: React.ReactNode;
fallback?: React.ReactNode;
}> = ({ flagKey, children, fallback = null }) => {
const isEnabled = useFeatureFlag(flagKey);
return isEnabled ? <>{children}</> : <>{fallback}</>;
};
// Usage in Components
const CheckoutScreen: React.FC = () => {
const checkoutVariant = useFeatureVariant('checkout_flow');
return (
<View>
<FeatureFlag flagKey="new_ui_design" fallback={<OldHeader />}>
<NewHeader />
</FeatureFlag>
{checkoutVariant === 'variant_a' && <CheckoutVariantA />}
{checkoutVariant === 'variant_b' && <CheckoutVariantB />}
{checkoutVariant === 'default' && <DefaultCheckout />}
</View>
);
};
Flutter Implementation (Dart)
dart
// Feature Flag Service
class FeatureFlagService {
static final FeatureFlagService _instance = FeatureFlagService._internal();
factory FeatureFlagService() => _instance;
FeatureFlagService._internal();
final RemoteConfigService _remoteConfigService = RemoteConfigService();
final FeatureFlagCache _localCache = FeatureFlagCache();
final FlagEvaluationEngine _evaluationEngine = FlagEvaluationEngine();
Future<bool> isFeatureEnabled(String flagKey) async {
try {
final flag = await _getFlag(flagKey);
final userContext = await _getUserContext();
return _evaluationEngine.evaluate(flag, userContext);
} catch (e) {
print('Error evaluating flag $flagKey: $e');
return _getDefaultValue(flagKey);
}
}
Future<String> getFeatureVariant(String flagKey) async {
try {
final flag = await _getFlag(flagKey);
final userContext = await _getUserContext();
return _evaluationEngine.getVariant(flag, userContext);
} catch (e) {
return 'default';
}
}
Future<FeatureFlag> _getFlag(String flagKey) async {
// Try local cache first
final cachedFlag = await _localCache.getFlag(flagKey);
if (cachedFlag != null) return cachedFlag;
// Fetch from remote
final flag = await _remoteConfigService.getFlag(flagKey);
await _localCache.saveFlag(flag);
return flag;
}
Future<UserContext> _getUserContext() async {
return UserContext(
userId: await _getCurrentUserId(),
deviceId: await _getDeviceId(),
appVersion: await _getAppVersion(),
deviceType: await _getDeviceType(),
location: await _getCurrentLocation(),
);
}
}
// Feature Flag Widget
class FeatureFlagWidget extends StatefulWidget {
final String flagKey;
final Widget child;
final Widget? fallback;
const FeatureFlagWidget({
Key? key,
required this.flagKey,
required this.child,
this.fallback,
}) : super(key: key);
@override
_FeatureFlagWidgetState createState() => _FeatureFlagWidgetState();
}
class _FeatureFlagWidgetState extends State<FeatureFlagWidget> {
final FeatureFlagService _featureFlagService = FeatureFlagService();
bool _isEnabled = false;
bool _isLoading = true;
@override
void initState() {
super.initState();
_checkFeatureFlag();
}
Future<void> _checkFeatureFlag() async {
final isEnabled = await _featureFlagService.isFeatureEnabled(widget.flagKey);
setState(() {
_isEnabled = isEnabled;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const CircularProgressIndicator();
}
return _isEnabled ? widget.child : (widget.fallback ?? const SizedBox.shrink());
}
}
// Usage in Widgets
class CheckoutScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: FeatureFlagWidget(
flagKey: 'new_ui_design',
child: const NewAppBarTitle(),
fallback: const OldAppBarTitle(),
),
),
body: FutureBuilder<String>(
future: FeatureFlagService().getFeatureVariant('checkout_flow'),
builder: (context, snapshot) {
final variant = snapshot.data ?? 'default';
switch (variant) {
case 'variant_a':
return const CheckoutVariantA();
case 'variant_b':
return const CheckoutVariantB();
default:
return const DefaultCheckout();
}
},
),
);
}
}
Backend Services
Feature Flag Service (Node.js)
typescript
// Feature Flag Service
import express from 'express';
import mongoose from 'mongoose';
import Redis from 'ioredis';
interface FeatureFlag {
key: string;
name: string;
description: string;
enabled: boolean;
rolloutPercentage: number;
conditions: FlagCondition[];
variants: FlagVariant[];
environment: string;
createdAt: Date;
updatedAt: Date;
}
interface FlagCondition {
type: 'user_id' | 'app_version' | 'device_type' | 'location' | 'custom';
operator: 'equals' | 'contains' | 'greater_than' | 'less_than' | 'in_list';
value: any;
}
interface FlagVariant {
key: string;
name: string;
weight: number;
payload: Record<string, any>;
}
const FeatureFlagModel = mongoose.model('FeatureFlag', new mongoose.Schema({
key: { type: String, required: true, unique: true },
name: { type: String, required: true },
description: String,
enabled: { type: Boolean, default: false },
rolloutPercentage: { type: Number, default: 0, min: 0, max: 100 },
conditions: [{
type: { type: String, required: true },
operator: { type: String, required: true },
value: mongoose.Schema.Types.Mixed
}],
variants: [{
key: { type: String, required: true },
name: { type: String, required: true },
weight: { type: Number, required: true },
payload: mongoose.Schema.Types.Mixed
}],
environment: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
}));
class FeatureFlagService {
private redis: Redis;
constructor() {
this.redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
}
async getFlag(flagKey: string, environment: string = 'production'): Promise<FeatureFlag | null> {
// Try cache first
const cached = await this.redis.get(`flag:${environment}:${flagKey}`);
if (cached) {
return JSON.parse(cached);
}
// Fetch from database
const flag = await FeatureFlagModel.findOne({ key: flagKey, environment });
if (flag) {
// Cache for 5 minutes
await this.redis.setex(`flag:${environment}:${flagKey}`, 300, JSON.stringify(flag));
}
return flag;
}
async evaluateFlag(flagKey: string, userContext: UserContext, environment: string = 'production'): Promise<{
enabled: boolean;
variant?: string;
payload?: Record<string, any>;
}> {
const flag = await this.getFlag(flagKey, environment);
if (!flag) {
return { enabled: false };
}
// Base enabled check
if (!flag.enabled) {
return { enabled: false };
}
// Rollout percentage check
if (!this.isInRollout(userContext.userId, flag.rolloutPercentage)) {
return { enabled: false };
}
// Condition evaluation
if (!this.evaluateConditions(flag.conditions, userContext)) {
return { enabled: false };
}
// Variant selection
const variant = this.selectVariant(flag.variants, userContext.userId);
return {
enabled: true,
variant: variant?.key,
payload: variant?.payload
};
}
async getAllFlags(environment: string = 'production'): Promise<FeatureFlag[]> {
return await FeatureFlagModel.find({ environment });
}
async createFlag(flag: Partial<FeatureFlag>): Promise<FeatureFlag> {
const newFlag = new FeatureFlagModel(flag);
await newFlag.save();
// Invalidate cache
await this.redis.del(`flag:${flag.environment}:${flag.key}`);
return newFlag;
}
async updateFlag(flagKey: string, updates: Partial<FeatureFlag>, environment: string = 'production'): Promise<FeatureFlag | null> {
const flag = await FeatureFlagModel.findOneAndUpdate(
{ key: flagKey, environment },
{ ...updates, updatedAt: new Date() },
{ new: true }
);
if (flag) {
// Invalidate cache
await this.redis.del(`flag:${environment}:${flagKey}`);
// Publish update event
await this.redis.publish('flag-updates', JSON.stringify({
flagKey,
environment,
action: 'updated',
timestamp: new Date().toISOString()
}));
}
return flag;
}
private isInRollout(userId: string, percentage: number): boolean {
if (percentage === 100) return true;
if (percentage === 0) return false;
const hash = this.hashString(userId);
return (hash % 100) < percentage;
}
private evaluateConditions(conditions: FlagCondition[], userContext: UserContext): boolean {
return conditions.every(condition => {
const contextValue = this.getContextValue(condition.type, userContext);
return this.evaluateCondition(condition, contextValue);
});
}
private selectVariant(variants: FlagVariant[], userId: string): FlagVariant | null {
if (variants.length === 0) return null;
const hash = this.hashString(userId);
const totalWeight = variants.reduce((sum, variant) => sum + variant.weight, 0);
const target = hash % totalWeight;
let currentWeight = 0;
for (const variant of variants) {
currentWeight += variant.weight;
if (target < currentWeight) {
return variant;
}
}
return variants[0];
}
private hashString(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash);
}
}
// API Routes
const app = express();
const featureFlagService = new FeatureFlagService();
app.get('/api/flags/:flagKey/evaluate', async (req, res) => {
try {
const { flagKey } = req.params;
const { environment = 'production' } = req.query;
const userContext = req.body;
const result = await featureFlagService.evaluateFlag(flagKey, userContext, environment as string);
res.json(result);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
app.get('/api/flags', async (req, res) => {
try {
const { environment = 'production' } = req.query;
const flags = await featureFlagService.getAllFlags(environment as string);
res.json(flags);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/api/flags', async (req, res) => {
try {
const flag = await featureFlagService.createFlag(req.body);
res.status(201).json(flag);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
Feature Flag Dashboard (React)
tsx
import React, { useState, useEffect } from 'react';
import { Switch, Slider, Button, Modal, Form, Input, Select, Table, Space, Tag, Card, Statistic } from 'antd';
interface FeatureFlag {
key: string;
name: string;
description: string;
enabled: boolean;
rolloutPercentage: number;
conditions: FlagCondition[];
variants: FlagVariant[];
environment: string;
stats?: {
totalEvaluations: number;
enabledEvaluations: number;
variantDistribution: { [key: string]: number };
};
}
const FeatureFlagDashboard: React.FC = () => {
const [flags, setFlags] = useState<FeatureFlag[]>([]);
const [loading, setLoading] = useState(true);
const [environment, setEnvironment] = useState('production');
const [editingFlag, setEditingFlag] = useState<FeatureFlag | null>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
useEffect(() => {
fetchFlags();
}, [environment]);
const fetchFlags = async () => {
try {
setLoading(true);
const response = await fetch(`/api/flags?environment=${environment}`);
const data = await response.json();
setFlags(data);
} catch (error) {
console.error('Error fetching flags:', error);
} finally {
setLoading(false);
}
};
const toggleFlag = async (flagKey: string, enabled: boolean) => {
try {
await fetch(`/api/flags/${flagKey}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled, environment })
});
fetchFlags();
} catch (error) {
console.error('Error toggling flag:', error);
}
};
const updateRollout = async (flagKey: string, rolloutPercentage: number) => {
try {
await fetch(`/api/flags/${flagKey}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rolloutPercentage, environment })
});
fetchFlags();
} catch (error) {
console.error('Error updating rollout:', error);
}
};
const columns = [
{
title: 'Flag',
dataIndex: 'name',
key: 'name',
render: (text: string, record: FeatureFlag) => (
<div>
<div style={{ fontWeight: 'bold' }}>{text}</div>
<div style={{ fontSize: '12px', color: '#666' }}>{record.key}</div>
<div style={{ fontSize: '12px', color: '#999' }}>{record.description}</div>
</div>
),
},
{
title: 'Status',
dataIndex: 'enabled',
key: 'enabled',
render: (enabled: boolean, record: FeatureFlag) => (
<Switch
checked={enabled}
onChange={(checked) => toggleFlag(record.key, checked)}
/>
),
},
{
title: 'Rollout',
dataIndex: 'rolloutPercentage',
key: 'rolloutPercentage',
render: (percentage: number, record: FeatureFlag) => (
<div style={{ width: '150px' }}>
<Slider
value={percentage}
onChange={(value) => updateRollout(record.key, value)}
disabled={!record.enabled}
tooltip={{ formatter: (value) => `${value}%` }}
/>
<div style={{ textAlign: 'center', fontSize: '12px' }}>{percentage}%</div>
</div>
),
},
{
title: 'Conditions',
dataIndex: 'conditions',
key: 'conditions',
render: (conditions: FlagCondition[]) => (
<div>
{conditions.map((condition, index) => (
<Tag key={index} color="blue">
{condition.type} {condition.operator} {JSON.stringify(condition.value)}
</Tag>
))}
</div>
),
},
{
title: 'Variants',
dataIndex: 'variants',
key: 'variants',
render: (variants: FlagVariant[]) => (
<div>
{variants.map((variant, index) => (
<Tag key={index} color="green">
{variant.name} ({variant.weight}%)
</Tag>
))}
</div>
),
},
{
title: 'Stats',
key: 'stats',
render: (record: FeatureFlag) => (
<div>
{record.stats && (
<Space direction="vertical" size="small">
<Statistic
title="Evaluations"
value={record.stats.totalEvaluations}
suffix={`(${((record.stats.enabledEvaluations / record.stats.totalEvaluations) * 100).toFixed(1)}% enabled)`}
/>
</Space>
)}
</div>
),
},
{
title: 'Actions',
key: 'actions',
render: (record: FeatureFlag) => (
<Space>
<Button
type="primary"
size="small"
onClick={() => {
setEditingFlag(record);
setIsModalVisible(true);
}}
>
Edit
</Button>
<Button
type="link"
size="small"
onClick={() => viewAnalytics(record.key)}
>
Analytics
</Button>
</Space>
),
},
];
const viewAnalytics = (flagKey: string) => {
// Navigate to analytics page
window.open(`/analytics/flags/${flagKey}`, '_blank');
};
return (
<div style={{ padding: '24px' }}>
<div style={{ marginBottom: '24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h1>Feature Flags Dashboard</h1>
<Space>
<Select
value={environment}
onChange={setEnvironment}
style={{ width: 120 }}
>
<Select.Option value="development">Development</Select.Option>
<Select.Option value="staging">Staging</Select.Option>
<Select.Option value="production">Production</Select.Option>
</Select>
<Button
type="primary"
onClick={() => {
setEditingFlag(null);
setIsModalVisible(true);
}}
>
Create Flag
</Button>
</Space>
</div>
<Table
columns={columns}
dataSource={flags}
loading={loading}
rowKey="key"
pagination={{ pageSize: 20 }}
/>
<FlagEditModal
flag={editingFlag}
visible={isModalVisible}
onCancel={() => setIsModalVisible(false)}
onSuccess={() => {
setIsModalVisible(false);
fetchFlags();
}}
environment={environment}
/>
</div>
);
};
const FlagEditModal: React.FC<{
flag: FeatureFlag | null;
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
environment: string;
}> = ({ flag, visible, onCancel, onSuccess, environment }) => {
const [form] = Form.useForm();
useEffect(() => {
if (flag) {
form.setFieldsValue(flag);
} else {
form.resetFields();
}
}, [flag, form]);
const handleSubmit = async (values: any) => {
try {
const url = flag ? `/api/flags/${flag.key}` : '/api/flags';
const method = flag ? 'PUT' : 'POST';
await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...values, environment })
});
onSuccess();
} catch (error) {
console.error('Error saving flag:', error);
}
};
return (
<Modal
title={flag ? 'Edit Feature Flag' : 'Create Feature Flag'}
visible={visible}
onCancel={onCancel}
onOk={() => form.submit()}
width={800}
>
<Form form={form} onFinish={handleSubmit} layout="vertical">
<Form.Item name="key" label="Key" rules={[{ required: true }]}>
<Input disabled={!!flag} />
</Form.Item>
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="description" label="Description">
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item name="enabled" label="Enabled" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="rolloutPercentage" label="Rollout Percentage">
<Slider marks={{ 0: '0%', 50: '50%', 100: '100%' }} />
</Form.Item>
</Form>
</Modal>
);
};
export default FeatureFlagDashboard;
Advanced Features
1. Real-time Flag Updates
typescript
// WebSocket-based real-time updates
class RealtimeFeatureFlagClient {
private ws: WebSocket;
private listeners: Map<string, (enabled: boolean) => void> = new Map();
constructor(wsUrl: string) {
this.ws = new WebSocket(wsUrl);
this.setupEventHandlers();
}
private setupEventHandlers() {
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'flag_update') {
const { flagKey, enabled } = data;
const listener = this.listeners.get(flagKey);
if (listener) {
listener(enabled);
}
}
};
}
subscribe(flagKey: string, callback: (enabled: boolean) => void) {
this.listeners.set(flagKey, callback);
// Send subscription message
this.ws.send(JSON.stringify({
type: 'subscribe',
flagKey
}));
}
unsubscribe(flagKey: string) {
this.listeners.delete(flagKey);
this.ws.send(JSON.stringify({
type: 'unsubscribe',
flagKey
}));
}
}
2. Flag Analytics and Metrics
typescript
// Analytics tracking
class FeatureFlagAnalytics {
private analyticsService: AnalyticsService;
constructor(analyticsService: AnalyticsService) {
this.analyticsService = analyticsService;
}
trackFlagEvaluation(
flagKey: string,
enabled: boolean,
variant?: string,
userContext?: UserContext
) {
this.analyticsService.track('feature_flag_evaluated', {
flag_key: flagKey,
enabled,
variant,
user_id: userContext?.userId,
device_type: userContext?.deviceType,
app_version: userContext?.appVersion,
timestamp: new Date().toISOString()
});
}
async getFlagStats(flagKey: string, timeRange: string): Promise<FlagStats> {
return await this.analyticsService.query(`
SELECT
COUNT(*) as total_evaluations,
COUNT(CASE WHEN enabled = true THEN 1 END) as enabled_evaluations,
variant,
COUNT(*) as variant_count
FROM feature_flag_events
WHERE flag_key = ? AND timestamp >= ?
GROUP BY variant
`, [flagKey, timeRange]);
}
}
3. Gradual Rollout Automation
typescript
// Automated gradual rollout
class GradualRolloutManager {
private featureFlagService: FeatureFlagService;
private metricsService: MetricsService;
async startGradualRollout(flagKey: string, config: RolloutConfig) {
const rolloutPlan = this.createRolloutPlan(config);
for (const phase of rolloutPlan.phases) {
await this.executePhase(flagKey, phase);
// Monitor metrics
const success = await this.monitorPhase(flagKey, phase);
if (!success) {
await this.rollback(flagKey);
throw new Error(`Rollout failed at ${phase.percentage}%`);
}
// Wait before next phase
await this.delay(phase.duration);
}
}
private async executePhase(flagKey: string, phase: RolloutPhase) {
await this.featureFlagService.updateFlag(flagKey, {
rolloutPercentage: phase.percentage
});
}
private async monitorPhase(flagKey: string, phase: RolloutPhase): Promise<boolean> {
const metrics = await this.metricsService.getMetrics(flagKey, phase.duration);
// Check success criteria
return (
metrics.errorRate < phase.maxErrorRate &&
metrics.performanceRegression < phase.maxPerformanceRegression
);
}
}
Best Practices
1. Flag Lifecycle Management
- Short-lived flags: Remove flags after feature is fully rolled out
- Flag naming: Use consistent naming conventions
- Documentation: Document flag purpose and cleanup timeline
- Monitoring: Track flag usage and performance impact
2. Safety Measures
typescript
// Flag safety wrapper
class SafeFeatureFlag {
private static readonly TIMEOUT_MS = 100;
static async isEnabled(flagKey: string, defaultValue: boolean = false): Promise<boolean> {
try {
const result = await Promise.race([
FeatureFlagManager.isFeatureEnabled(flagKey),
new Promise<boolean>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), this.TIMEOUT_MS)
)
]);
return result;
} catch (error) {
console.warn(`Feature flag ${flagKey} evaluation failed, using default:`, error);
return defaultValue;
}
}
}
3. Testing Strategies
typescript
// Feature flag testing utilities
class FeatureFlagTestUtils {
static mockFlag(flagKey: string, enabled: boolean, variant?: string) {
jest.spyOn(FeatureFlagManager, 'isFeatureEnabled')
.mockResolvedValue(enabled);
if (variant) {
jest.spyOn(FeatureFlagManager, 'getFeatureVariant')
.mockResolvedValue(variant);
}
}
static clearMocks() {
jest.clearAllMocks();
}
}
// Usage in tests
describe('CheckoutFlow', () => {
beforeEach(() => {
FeatureFlagTestUtils.clearMocks();
});
it('should show new checkout when flag is enabled', async () => {
FeatureFlagTestUtils.mockFlag('new_checkout', true);
const { getByTestId } = render(<CheckoutScreen />);
await waitFor(() => {
expect(getByTestId('new-checkout')).toBeInTheDocument();
});
});
});
4. Performance Optimization
- Local caching: Cache flag evaluations to reduce network calls
- Batch evaluation: Evaluate multiple flags in single request
- CDN distribution: Use CDN for flag configuration distribution
- Lazy loading: Load flags only when needed
5. Security Considerations
- Sensitive flags: Don't expose sensitive business logic in client-side flags
- Authentication: Secure flag management APIs
- Audit logging: Log all flag changes and evaluations
- Environment isolation: Separate flags by environment
Feature flags provide powerful capabilities for mobile applications, enabling safer deployments, A/B testing, and gradual feature rollouts. The key to success is implementing robust evaluation logic, comprehensive monitoring, and following best practices for flag lifecycle management.