mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-05 17:53:12 +02:00
Update formatting
This commit is contained in:
parent
66b51a4fce
commit
f6df88f74f
@ -1,493 +1,517 @@
|
|||||||
---
|
---
|
||||||
title: Managing feature flags in your codebase
|
title: Managing feature flags in your codebase
|
||||||
---
|
---
|
||||||
|
|
||||||
import Tabs from '@theme/Tabs';
|
import Tabs from '@theme/Tabs';
|
||||||
import TabItem from '@theme/TabItem';
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
The choices you make when organizinga and implementing feature flags in your codebase significantly impact your application's performance, testability, and long-term maintainability. In this guide, we explore key considerations for flag management in code, offering detailed recommendations, and code examples to help you build reliable and flexible systems.
|
The choices you make when implementing and organizing feature flags in your codebase significantly impact your application's performance, testability, and long-term maintainability.
|
||||||
|
When not managed with discipline, flags can introduce technical debt, making code harder to understand and riskier to change.
|
||||||
|
|
||||||
Good general software design practices, like modularity and clear separation of concerns, also make integrating and managing feature flags easier.
|
This guide explores key considerations for managing flags in your code. We offer detailed recommendations and practical examples to help you build reliable, scalable, and flexible systems.
|
||||||
|
|
||||||
Before diving into specifics, let's look at some high-level ... that help you work with feature flags in code:
|
Good software design practices, like modularity and a clear separation of concerns, make integrating feature flags easier. Before diving into specifics, let's establish some guiding principles:
|
||||||
|
|
||||||
- **Clarity**: Feature flag-related logic should be easy to understand, locate, and reason about. Developers should quickly grasp what a flag does and how it affects behavior.
|
- **Clarity**: Feature flag logic should be easy to locate, understand, and reason about. Any developer should be able to quickly grasp what a flag does and how it affects system behavior.
|
||||||
- **Maintainability**: Adding new flags, modifying, and especially removing flags when they are no longer needed should be straightforward and low-risk.
|
- **Maintainability**: Adding new flags, modifying existing ones, and—most importantly—removing flags when they are no longer needed should be a straightforward and low-risk process.
|
||||||
- **Testability**: Code controlled by feature flags must be easily testables.
|
- **Testability**: Code controlled by feature flags must be easily and reliably testable, ideally without a combinatorial explosion of test cases.
|
||||||
- **Scalability**: Your approach must accommodate a growing number of feature flags and a large number of developers interacting with them, without leading to a tangled or unmanageable codebase.
|
- **Scalability**: Your approach must accommodate a growing number of flags and developers without leading to a tangled, unmanageable codebase.
|
||||||
|
|
||||||
.. now with some of the high-level principles, let's dive into feature flag specifics.
|
-----
|
||||||
|
|
||||||
## Defining and storing flag names
|
## Defining and storing flag names
|
||||||
|
|
||||||
The first step in managing flags in code is deciding how to represent and store the flag names. These are the identifiers that link your code to the flag configurations in the Unleash Admin UI.
|
The first step is to decide how to represent and store flag names. These identifiers link your code to the flag configurations in the Unleash Admin UI. A disorganized approach here can quickly lead to typos, inconsistencies, and difficulty in tracking down where a flag is used.
|
||||||
|
|
||||||
Unleash already tells you that flag names must be unique.. but what else to consider here???
|
**We recommend centralizing your flag name definitions using constants or enums.**
|
||||||
|
|
||||||
When it comes to defining flag names in your code, we recommend a statically-defined and centrally-configured approach. If possible, use your language's type system to provide strong type safety and discoverability for flag names.
|
This approach establishes a single source of truth for all flag names in your application.
|
||||||
|
|
||||||
> **Centralize your flag name definitions. Use constants or enums within your codebase.**
|
**Why centralize definitions?**
|
||||||
|
|
||||||
Why?
|
* **Avoids errors**: Using constants or enums prevents typos and inconsistencies that arise from scattering string literals (`"my-new-feature"`) throughout the application. Your compiler or linter can catch errors for you.
|
||||||
- **Single source of truth**: Avoids scattering string literals throughout your application, which can lead to typos and inconsistencies.
|
* **Improves discoverability**: A central file acts as a manifest of all flags used in the application, making it easy for developers to see what's available and how flags are named.
|
||||||
- **Type safety**: Enums or strongly-typed constants provide compile-time checks, catching errors early.
|
* **Simplifies refactoring**: If you need to change a flag's name in your code (for example, to fix a typo), you only need to update it in one place.
|
||||||
- **Refactoring**: Although flag names are immutable in Unleash, you may need to update it in your application (for example, to fix a typo). With a centralized place for flag definitions, you only need to update it in one place in your code.
|
|
||||||
- **Discoverability**: It's easier to see all available flags and their intended use.
|
|
||||||
|
|
||||||
This pattern centers on the pre-definition of known flag identifiers using TypeScript's type system, alongside establishing default configurations for these flags.
|
For languages with strong type systems like TypeScript, you can create even safer and more expressive definitions. This pattern, used within the Unleash codebase itself, combines union types and mapped types for excellent compile-time checking.
|
||||||
|
|
||||||
### Structured flag definitions with type safety
|
```typescript
|
||||||
|
// src/feature-flags.ts
|
||||||
|
|
||||||
|
// 1. Define all possible flag keys as a type-safe union.
|
||||||
When managing a collection of flags that might include variants or complex states beyond simple booleans, consider patterns that leverage your language's type system.
|
|
||||||
In TypeScript, for example, a common and effective pattern (also used within the [Unleash codebase](https://github.com/Unleash/unleash/blob/main/src/lib/types/experimental.ts).)) involves using union types for keys and mapped types for collections.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Pattern summary:
|
|
||||||
|
|
||||||
Define flag keys: Create a union type of string literals for all your feature flag names.
|
|
||||||
|
|
||||||
Define flag state structure: Create a type (often a mapped type based on the union type of keys) that describes the shape of an object holding all flags. The value for each flag key can be a boolean or a more complex FlagVariant object.
|
|
||||||
|
|
||||||
Provide initial/default values: Define an object conforming to this type to hold default states for your flags.
|
|
||||||
|
|
||||||
Code example (typescript: typed keys and collection):
|
|
||||||
|
|
||||||
// src/feature-flag-definitions.ts
|
|
||||||
|
|
||||||
// 1. Define all possible flag keys as a union type
|
|
||||||
export type AppFeatureKey =
|
export type AppFeatureKey =
|
||||||
| 'newUserProfilePage'
|
| 'newUserProfilePage'
|
||||||
| 'darkModeTheme'
|
| 'darkModeTheme'
|
||||||
| 'experimentalAnalyticsDashboard';
|
| 'advancedReportingEngine';
|
||||||
|
|
||||||
// Represents the structure of a flag that might have variants
|
// 2. (Optional) Define a more complex structure for flags with variants.
|
||||||
export interface AppFlagVariant {
|
export interface AppFlagVariant {
|
||||||
name: string; // Corresponds to variant name in Unleash
|
name: string; // Corresponds to the variant name in Unleash
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
payload?: {
|
payload?: {
|
||||||
type: string; // e.g., 'string', 'json', 'csv'
|
type: string; // e.g., 'string', 'json'
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Define the type for the object holding all flags
|
// 3. Define a type for the object that holds all flag states.
|
||||||
// Flags can be simple booleans or more complex variants
|
// This allows flags to be simple booleans or more complex objects.
|
||||||
export type AppFeatures = Partial<{
|
export type AppFeatures = Partial<{
|
||||||
[key in AppFeatureKey]: boolean | AppFlagVariant;
|
[key in AppFeatureKey]: boolean | AppFlagVariant;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
// 3. Provide default/initial values for flags
|
// 4. (Optional) Provide default values.
|
||||||
export const initialAppFeatureFlags: AppFeatures = {
|
export const initialAppFeatureFlags: AppFeatures = {
|
||||||
newUserProfilePage: false, // Default to off
|
newUserProfilePage: false,
|
||||||
darkModeTheme: {
|
darkModeTheme: { name: 'disabled', enabled: false },
|
||||||
name: 'uiTheme', // This could be the flag name if it's a variant flag
|
|
||||||
enabled: false,
|
|
||||||
payload: {
|
|
||||||
type: 'string',
|
|
||||||
value: 'light', // Default payload value
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// experimentalAnalyticsDashboard might default to undefined,
|
|
||||||
// relying on the Unleash server or a default in the evaluation logic.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Usage with a feature service (conceptual)
|
|
||||||
// if (featureService.isEnabled(AppFeatureKey.newUserProfilePage, context)) { ... }
|
|
||||||
// const darkModeVariant = featureService.getVariant(AppFeatureKey.darkModeTheme, context);
|
|
||||||
// if (darkModeVariant?.enabled && darkModeVariant.payload?.value === 'dark') { ... }
|
|
||||||
|
|
||||||
Things to consider (for typed keys and collections pattern):
|
|
||||||
|
|
||||||
Enhanced type safety: This pattern provides excellent compile-time checking for flag names and their potential structures, reducing runtime errors. It's particularly beneficial when flags have variants with payloads.
|
|
||||||
|
|
||||||
Clarity for complex states: Clearly defines the shape of each flag, whether it's a simple toggle or has multiple variants.
|
|
||||||
|
|
||||||
Verbosity: Can be more verbose for projects with only a few simple boolean flags compared to just using string constants or a basic enum.
|
|
||||||
|
|
||||||
Language specific implementation: While the principle of strong typing is general, the exact implementation shown (union types, mapped types) is specific to TypeScript. Other statically-typed languages may offer different mechanisms to achieve similar levels of type safety for flag definitions.
|
|
||||||
|
|
||||||
Large number of flags: for hundreds of flags, consider organizing definitions by feature area, maintaining a clear project-wide convention.
|
|
||||||
|
|
||||||
Dynamic flag names: avoid constructing flag names dynamically (e.g., "feature_" + sectionName) as it hinders static analysis and discovery.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Strategic flag evaluation: where, when, and how often
|
|
||||||
How you implement feature flags in your codebase significantly affects its structure and behavior. Thoughtful evaluation strategies are key to maintainable and predictable systems.
|
|
||||||
|
|
||||||
### Define flags at the highest level of abstraction
|
|
||||||
Place feature flag evaluations as high in the application stack as is practical for the change they control. For instance, when toggling a new UI theme or a different checkout flow, evaluate the flag in the controller, presenter, or top-level component that orchestrates that part of the user experience.
|
|
||||||
|
|
||||||
Benefits:
|
|
||||||
|
|
||||||
- Simplified code: by controlling feature behavior from a single, high-level point, the rest of your system often doesn't need to be aware of the flag itself.
|
|
||||||
|
|
||||||
- Easier testing: isolating the decision point makes it easier to unit and integration test the feature.
|
|
||||||
|
|
||||||
- Faster cleanup: flags defined and evaluated in one primary location are easier to find and remove.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// src/controllers/checkoutController.js
|
|
||||||
// import { featureService } from '../services/featureService';
|
|
||||||
// import { FeatureFlags } from '../feature-flags';
|
|
||||||
|
|
||||||
export function handleCheckoutRequest(req, res) {
|
|
||||||
const userContext = { userId: req.user.id, /* ... other context */ };
|
|
||||||
const useNewCheckout = featureService.isEnabled(FeatureFlags.NEW_CHECKOUT_PROCESS, userContext);
|
|
||||||
|
|
||||||
if (useNewCheckout) {
|
|
||||||
renderNewCheckoutPage(req, res, userContext);
|
|
||||||
} else {
|
|
||||||
renderOldCheckoutPage(req, res, userContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Additional things to consider:
|
**Avoid dynamic flag names.** Constructing flag names at runtime (e.g., `"feature_" + sectionName`) prevents static analysis, making it nearly impossible to find all references to a flag automatically.
|
||||||
|
|
||||||
- Performance: passing evaluated flag state down many layers can be cumbersome. If a decision is only relevant deep in a call stack, evaluate closer to the point of use, but weigh this against centralization benefits and the "evaluate once" principle.
|
|
||||||
|
|
||||||
- Granularity: for a high-level module controlling many distinct, independently flagged sub-features, evaluating all flags at the top might be overly complex. Delegate flag evaluation for sub-features to their respective sub-modules, while still adhering to "evaluate once" for each flag.
|
## Architecting flag evaluation
|
||||||
|
|
||||||
- Backend logic: evaluate close to the affected module
|
How and where you check a flag's state is one of the most important architectural decisions you'll make. A well-designed evaluation strategy keeps your code clean and your system's behavior predictable.
|
||||||
For changes mainly affecting backend logic (e.g., new database interaction, optimized algorithm), evaluate the flag close to the specific module responsible.
|
|
||||||
|
|
||||||
Benefits:
|
### Use an abstraction layer
|
||||||
|
|
||||||
Focused testing: simplifies unit and integration testing of the module with the flag enabled/disabled.
|
Directly calling the Unleash SDK (e.g., `unleash.isEnabled()`) throughout your codebase tightly couples your application to the specific SDK implementation. This can create problems down the line.
|
||||||
|
|
||||||
Isolation: reduces the risk of unintended side effects in unrelated backend code.
|
**We recommend implementing an abstraction layer, often called a "Feature Service" or "wrapper," to encapsulate all interactions with the Unleash SDK.**
|
||||||
|
|
||||||
Ensuring testability for backend modules:
|
This service becomes the single entry point for all feature flag checks in your application.
|
||||||
|
|
||||||
Inject flag result: evaluate the flag outside the module's core logic and inject the boolean result.
|
```typescript
|
||||||
|
|
||||||
Separate implementations: use patterns like strategy where the flag decision determines which implementation is used.
|
|
||||||
|
|
||||||
Code example (conceptual python service module):
|
|
||||||
|
|
||||||
```
|
|
||||||
# services/data_processor.py
|
|
||||||
# from ..feature_service import feature_service
|
|
||||||
# from .. import feature_flags
|
|
||||||
|
|
||||||
class DataProcessor:
|
|
||||||
def __init__(self, use_new_algorithm: bool):
|
|
||||||
self._use_new_algorithm = use_new_algorithm
|
|
||||||
|
|
||||||
def process(self, data):
|
|
||||||
if self._use_new_algorithm:
|
|
||||||
return self._process_with_new_algorithm(data)
|
|
||||||
else:
|
|
||||||
return self._process_with_old_algorithm(data)
|
|
||||||
|
|
||||||
def _process_with_old_algorithm(self, data): return f"Processed with old: {data}"
|
|
||||||
def _process_with_new_algorithm(self, data): return f"Processed with NEW: {data}"
|
|
||||||
|
|
||||||
# Somewhere higher in the stack:
|
|
||||||
# def get_data_processor(user_context):
|
|
||||||
# use_new_algo_flag_enabled = feature_service.is_enabled(
|
|
||||||
# feature_flags.OPTIMIZED_DATA_PROCESSING, user_context
|
|
||||||
# )
|
|
||||||
# return DataProcessor(use_new_algorithm=use_new_algo_flag_enabled)
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Things to consider:
|
|
||||||
|
|
||||||
This complements "evaluate once per request." The result of a flag evaluation can be passed to the backend module.
|
|
||||||
|
|
||||||
Avoid individual backend modules re-evaluating the same global flag multiple times for the same operation.
|
|
||||||
|
|
||||||
Evaluate feature flags once per user request
|
|
||||||
Evaluate a feature flag once per user request, typically at the entry point. Propagate the outcome downstream.
|
|
||||||
|
|
||||||
Benefits:
|
|
||||||
|
|
||||||
Consistent user experience: guarantees the user experiences the same feature state throughout their interaction.
|
|
||||||
|
|
||||||
Predictable behavior: simplifies reasoning about system behavior.
|
|
||||||
|
|
||||||
Cleaner code: reduces redundant flag evaluations.
|
|
||||||
|
|
||||||
Resilience: minimizes the window for experiencing varying flag states during rare inconsistencies.
|
|
||||||
|
|
||||||
How to propagate the flag's outcome:
|
|
||||||
|
|
||||||
Request headers: for distributed systems (e.g., X-Feature-NewCheckout: true).
|
|
||||||
|
|
||||||
Request context/object: within a monolith or single service.
|
|
||||||
|
|
||||||
Configuration object: derived from flags at the start of the request.
|
|
||||||
|
|
||||||
```
|
|
||||||
// --- API Gateway (e.g., Express.js middleware) ---
|
|
||||||
// import { featureService } from './services/featureService';
|
|
||||||
// import { FeatureFlags } from './feature-flags';
|
|
||||||
// import axios from 'axios';
|
|
||||||
|
|
||||||
gateway.use(async (req, res, next) => {
|
|
||||||
const userContext = { userId: req.user?.id };
|
|
||||||
req.featureFlags = {
|
|
||||||
useNewPaymentMethod: featureService.isEnabled(FeatureFlags.NEW_PAYMENT_METHOD, userContext)
|
|
||||||
};
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
gateway.post('/api/orders', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const orderServiceResponse = await axios.post('http://order-service/create-order',
|
|
||||||
req.body, { headers: { 'X-Feature-UseNewPayment': req.featureFlags.useNewPaymentMethod } }
|
|
||||||
);
|
|
||||||
res.json(orderServiceResponse.data);
|
|
||||||
} catch (error) { /* ... */ }
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Things to consider:
|
|
||||||
|
|
||||||
Overhead: propagating flag state adds minor overhead.
|
|
||||||
|
|
||||||
Long-lived processes: for background jobs (not typical user requests), you might need to re-evaluate flags periodically if their state can change and influence ongoing behavior.
|
|
||||||
|
|
||||||
Sdk caching: while unleash sdks have efficient caching, "evaluate once per request" is primarily about logical consistency for a user interaction.
|
|
||||||
|
|
||||||
## Accessing and evaluating flags: the abstraction layer advantage
|
|
||||||
Directly calling unleash.isEnabled() throughout your codebase can lead to tight coupling.
|
|
||||||
|
|
||||||
Recommendation: implement an abstraction layer or a dedicated service (e.g., FeatureService) wrapping unleash sdk calls.
|
|
||||||
|
|
||||||
Why: decoupling, centralized logic (defaults, context, logging), simplified usage, enhanced testability.
|
|
||||||
|
|
||||||
Code example (typescript/node.js FeatureService):
|
|
||||||
|
|
||||||
```
|
|
||||||
// src/services/feature-service.ts
|
// src/services/feature-service.ts
|
||||||
import { Unleash, Context as UnleashContext } from 'unleash-client';
|
import { Unleash, Context as UnleashContext } from 'unleash-client';
|
||||||
|
import { AppFeatureKey } from '../feature-flags'; // Import from your central definitions
|
||||||
|
|
||||||
export interface AppUserContext { /* ... as previously defined ... */ }
|
// Define your application's context structure
|
||||||
|
export interface AppUserContext {
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
remoteAddress?: string;
|
||||||
|
properties?: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class FeatureService {
|
class FeatureService {
|
||||||
private unleash: Unleash;
|
private unleash: Unleash;
|
||||||
constructor(unleashInstance: Unleash) { this.unleash = unleashInstance; }
|
|
||||||
|
|
||||||
private buildUnleashContext(appContext?: AppUserContext): UnleashContext { /* ... */ return { ...appContext }; }
|
constructor(unleashInstance: Unleash) {
|
||||||
|
this.unleash = unleashInstance;
|
||||||
|
}
|
||||||
|
|
||||||
public isEnabled(flagName: string, appContext?: AppUserContext, defaultValue: boolean = false): boolean {
|
private buildUnleashContext(appContext?: AppUserContext): UnleashContext {
|
||||||
|
if (!appContext) return {};
|
||||||
|
return { ...appContext };
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEnabled(flagName: AppFeatureKey, appContext?: AppUserContext): boolean {
|
||||||
|
// Always provide a safe, default value (usually `false`)
|
||||||
|
const defaultValue = false;
|
||||||
try {
|
try {
|
||||||
const unleashContext = this.buildUnleashContext(appContext);
|
const unleashContext = this.buildUnleashContext(appContext);
|
||||||
// See [Unleash SDK isEnabled Documentation](placeholder_link_to_sdk_isenabled_doc)
|
|
||||||
return this.unleash.isEnabled(flagName, unleashContext, defaultValue);
|
return this.unleash.isEnabled(flagName, unleashContext, defaultValue);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error evaluating flag ${flagName}:`, error);
|
// Log the error for observability
|
||||||
|
console.error(`Error evaluating flag "${flagName}":`, error);
|
||||||
|
// Fallback to the safe default
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example of a more semantic, business-language method
|
// You can also create more semantic, business-language methods
|
||||||
public canUserAccessNewDashboard(userContext?: AppUserContext): boolean {
|
public canUserSeeNewProfilePage(userContext?: AppUserContext): boolean {
|
||||||
// Assuming FeatureFlags.BETA_USER_DASHBOARD is defined
|
return this.isEnabled('newUserProfilePage', userContext);
|
||||||
return this.isEnabled(FeatureFlags.BETA_USER_DASHBOARD, userContext, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Initialize and export:
|
|
||||||
// const unleash = initializeUnleashClient();
|
// Initialize and export a singleton instance for your app to use
|
||||||
|
// const unleash = initializeUnleashClient(); // Your Unleash setup
|
||||||
// export const featureService = new FeatureService(unleash);
|
// export const featureService = new FeatureService(unleash);
|
||||||
```
|
```
|
||||||
|
|
||||||
Key considerations for your abstraction layer:
|
**Benefits of an abstraction layer:**
|
||||||
|
|
||||||
Context provision: define how relevant context is passed for targeted evaluations.
|
- **Vendor abstraction**: If you ever switch feature flagging providers, you only need to update the Feature Service instead of hunting for SDK calls across the entire codebase.
|
||||||
|
- **Centralized control**: It provides a single place to implement cross-cutting concerns like logging, performance monitoring, and consistent error handling for all flag evaluations.
|
||||||
|
- **Simplified cleanup**: To find all usages of a flag, you can search for its name within your centralized definitions file, which is far more reliable than searching for a string literal.
|
||||||
|
- **Improved readability**: Methods with business-friendly names (`canUserSeeNewProfilePage()`) make the code's intent clearer than a generic `isEnabled("newUserProfilePage")`.
|
||||||
|
|
||||||
Default values & backward compatibility: always provide a safe default value (typically the "off" or pre-existing behavior). This is crucial for backward compatibility, ensuring that if a flag is turned off or the sdk fails, the application gracefully reverts to known, stable functionality without errors.
|
### Evaluate flags at the right level and time
|
||||||
|
|
||||||
Error handling: implement robust error handling around sdk calls.
|
**For a given user request, evaluate a feature flag once at the highest practical level of your application stack.** Propagate the *result* (the decision) of that evaluation downstream to other components or functions.
|
||||||
|
|
||||||
Refer to the unleash sdk documentation.
|
For example, when toggling a new checkout flow, evaluate the flag in the controller or top-level component that orchestrates that user experience. That component then decides whether to render the `NewCheckoutFlow` or the `OldCheckoutFlow`. The child components within the flow don't need to know the flag exists; they just receive props and do their job.
|
||||||
|
|
||||||
Things to consider:
|
```javascript
|
||||||
|
// src/controllers/checkoutController.js
|
||||||
|
// import { featureService } from '../services/featureService';
|
||||||
|
|
||||||
Over-abstraction: for very small applications, a dedicated service might seem like overkill, but it often pays off as complexity grows.
|
export function handleCheckoutRequest(req, res) {
|
||||||
|
const userContext = { userId: req.user.id };
|
||||||
|
|
||||||
Semantic methods: encapsulating flag checks within methods that use business language (e.g., user.canPerformAction() instead of featureService.isEnabled("actionFlag")) can significantly improve code readability and align it closer to domain concepts. This can be part of your FeatureService or even methods on domain objects themselves if they have access to flag evaluation capabilities.
|
// 1. Evaluate ONCE at the highest level.
|
||||||
|
const useNewCheckout = featureService.isEnabled('new-checkout-process', userContext);
|
||||||
|
|
||||||
Structuring conditional logic: beyond simple if/else
|
// 2. Propagate the DECISION, not the flag check.
|
||||||
Overusing if/else for complex features can lead to code that's hard to read, test, and clean up.
|
if (useNewCheckout) {
|
||||||
|
renderNewCheckoutPage(req, res); // This component tree uses the new logic
|
||||||
Recommendation: for complex features, consider design patterns like strategy or encapsulate logic into separate modules/classes.
|
} else {
|
||||||
|
renderOldCheckoutPage(req, res); // This component tree uses the old logic
|
||||||
Why: cleaner code, improved testability, easier cleanup (delete a class/module instead of surgical else block removal).
|
|
||||||
|
|
||||||
Code example (strategy pattern - conceptual typescript):
|
|
||||||
|
|
||||||
```
|
|
||||||
// src/feature-flags.ts
|
|
||||||
export enum FeatureFlags { ADVANCED_REPORTING_ENGINE = "advancedReportingEngineEnabled" }
|
|
||||||
|
|
||||||
interface ReportData { /* ... */ }
|
|
||||||
interface ReportGenerationStrategy { generateReport(data: ReportData, context?: AppUserContext): Promise<string>; }
|
|
||||||
class StandardReportStrategy implements ReportGenerationStrategy { /* ... */ }
|
|
||||||
class AdvancedReportStrategy implements ReportGenerationStrategy { /* ... */ }
|
|
||||||
|
|
||||||
class ReportService {
|
|
||||||
private standardStrategy: ReportGenerationStrategy;
|
|
||||||
private advancedStrategy: ReportGenerationStrategy;
|
|
||||||
private featureService: /* type of featureService */ any;
|
|
||||||
|
|
||||||
constructor(fsInstance: any) { /* ... initialize strategies and fsInstance ... */ }
|
|
||||||
|
|
||||||
public async createReport(data: ReportData, userContext?: AppUserContext): Promise<string> {
|
|
||||||
let strategy = this.featureService.isEnabled(FeatureFlags.ADVANCED_REPORTING_ENGINE, userContext)
|
|
||||||
? this.advancedStrategy
|
|
||||||
: this.standardStrategy;
|
|
||||||
return strategy.generateReport(data, userContext);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Isolating feature-specific code: contain new feature code within new modules/classes for clarity and easier removal.
|
**Why evaluate once?**
|
||||||
|
|
||||||
Things to consider:
|
- **Consistency**: It ensures a user sees the same feature state throughout their interaction. Evaluating the same flag multiple times during a single request could yield different results if the flag's configuration is changed mid-request, leading to a broken or confusing user experience.
|
||||||
|
- **Performance**: It minimizes redundant calls to the evaluation logic.
|
||||||
|
- **Simplicity**: It prevents "flag-aware" logic from spreading deep into your application's components, making them simpler and easier to test.
|
||||||
|
|
||||||
Complexity for simple toggles: using complex patterns for simple on/off toggles is usually unnecessary.
|
|
||||||
|
|
||||||
Upfront effort: patterns like strategy require more upfront design.
|
## Structuring conditional logic
|
||||||
|
|
||||||
Readability vs. dry (don't repeat yourself): when dealing with feature flags, the two code paths (flag on vs. flag off) are often intentionally different. Prioritize readability and ease of cleanup for each path over aggressively trying to dry the code between them. Sometimes, a little duplication is acceptable if it makes each path clearer and the eventual removal of the old path simpler.
|
As features become more complex than a simple on/off toggle, relying on scattered `if/else` statements can lead to code that is difficult to read, test, and clean up.
|
||||||
|
|
||||||
Minimizing code clutter: the art of flag hygiene
|
### The anti-pattern: Scattered `if/else` statements
|
||||||
Without discipline, your codebase can become cluttered with flag checks, leading to "flag debt."
|
|
||||||
|
|
||||||
Recommendations:
|
While intuitive for simple cases, this pattern becomes a major source of technical debt at scale.
|
||||||
|
|
||||||
Clear comments: every flag check should explain its purpose, task reference (jira id), expected lifecycle, and owner.
|
```java
|
||||||
|
// Anti-Pattern: Scattered conditional logic
|
||||||
|
public void processPayment(PaymentDetails details, UserContext user) {
|
||||||
|
// This logic gets duplicated wherever a payment is processed.
|
||||||
|
if (featureService.isNewPaymentGatewayEnabled(user)) {
|
||||||
|
newPaymentService.charge(details);
|
||||||
|
} else {
|
||||||
|
legacyPaymentService.charge(details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
// JIRA-123: Enable new dashboard. Remove after Q1 (Team Phoenix)
|
Cleaning this up requires hunting down every `if` block, carefully removing the `else` branch, and then removing the conditional itself. This is tedious, error-prone, and a key reason why "temporary" flags become permanent fixtures in the code.
|
||||||
if (featureService.isEnabled(FeatureFlags.BETA_USER_DASHBOARD, userContext)) { /* ... */ }
|
|
||||||
|
|
||||||
Associate metadata: reflect metadata from unleash ui (tags, descriptions) in code comments.
|
### The solution: The strategy design pattern
|
||||||
|
|
||||||
Minimize or group changes behind a single flag: instead of many granular flags for different facets of one larger feature, consider grouping related changes under a single, more encompassing flag. This reduces the number of flags to manage, simplifies understanding what a flag controls, and lowers the risk of conflicting flag interactions. For example, a "newSettingsPageV1" flag might control the display of several new ui elements and backend endpoints related to that page.
|
For managing complex behavioral changes, the **Strategy pattern** is a superior approach. Instead of using a flag to select a *code path* inside a method, you use the flag to select a concrete *implementation* of a shared interface at runtime.
|
||||||
|
|
||||||
Regular reviews: periodically review active flags in code.
|
This encapsulates the different behaviors into distinct, interchangeable classes. The core application logic remains clean and agnostic of the feature flag itself.
|
||||||
|
|
||||||
Use unleash tags for organization in the unleash platform.
|
The primary benefit of this pattern is the **radical simplification of cleanup**. When the feature is fully rolled out, you simply delete the old strategy's file and update the factory to only provide the new one. This is a safe, atomic operation.
|
||||||
|
|
||||||
Things to consider:
|
#### Java example (with Spring)
|
||||||
|
|
||||||
Comment verbosity: balance detail with conciseness in comments.
|
Spring's dependency injection and conditional properties make implementing this pattern elegant.
|
||||||
|
|
||||||
Granularity balance: while grouping flags simplifies management, ensure flags are not so broad that they lose their utility for fine-grained control or targeted rollouts when needed.
|
**1. Define the strategy interface**
|
||||||
|
|
||||||
## Flag lifecycle management in code: cleanup and removal
|
```java
|
||||||
All flags (except perhaps permanent operational toggles) have a lifecycle. Removing them from code is crucial.
|
public interface PaymentProcessor {
|
||||||
|
PaymentResult process(Payment payment);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
When to remove flags: feature fully rolled out and stable; a/b test concluded; temporary flag no longer needed.
|
**2. Create concrete strategy implementations**
|
||||||
|
|
||||||
Process for safe removal from code:
|
Each class implements the behavior for one branch of the feature flag. `@ConditionalOnProperty` tells Spring which bean to create.
|
||||||
|
|
||||||
Verify in unleash: confirm status (e.g., 100% rollout, disabled).
|
```java
|
||||||
|
// Old implementation
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty(name = "features.new-payment-gateway.enabled", havingValue = "false", matchIfMissing = true)
|
||||||
|
public class LegacyPaymentProcessor implements PaymentProcessor {
|
||||||
|
@Override
|
||||||
|
public PaymentResult process(Payment payment) {
|
||||||
|
// ... logic for the old payment gateway
|
||||||
|
return new PaymentResult("SUCCESS_LEGACY");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Set permanent state (optional safeguard): configure flag in unleash to its permanent state; monitor.
|
// New implementation
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty(name = "features.new-payment-gateway.enabled", havingValue = "true")
|
||||||
|
public class StripePaymentProcessor implements PaymentProcessor {
|
||||||
|
@Override
|
||||||
|
public PaymentResult process(Payment payment) {
|
||||||
|
// ... logic for the new Stripe gateway
|
||||||
|
return new PaymentResult("SUCCESS_STRIPE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Remove conditional logic: delete if/else blocks.
|
**3. Inject the strategy into your service**
|
||||||
|
|
||||||
Delete old code path: critical step to avoid dead code.
|
The `CheckoutService` has no idea which implementation it will receive. It is decoupled from the flagging logic.
|
||||||
|
|
||||||
Delete flag definition: remove from constants/enums.
|
```java
|
||||||
|
@Service
|
||||||
|
public class CheckoutService {
|
||||||
|
private final PaymentProcessor paymentProcessor;
|
||||||
|
|
||||||
Clean up abstraction layer: remove specific helper methods.
|
@Autowired
|
||||||
|
public CheckoutService(PaymentProcessor paymentProcessor) {
|
||||||
|
this.paymentProcessor = paymentProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
Archive/delete flag in unleash ui: see managing flags in unleash.
|
public void finalizePurchase(Order order) {
|
||||||
|
// The service doesn't contain any if/else logic for the flag.
|
||||||
|
PaymentResult result = this.paymentProcessor.process(order.getPayment());
|
||||||
|
// ... handle result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Test thoroughly: ensure expected behavior.
|
**Cleanup:**
|
||||||
|
|
||||||
Code review: for removal changes.
|
1. Set `features.new-payment-gateway.enabled=true` permanently.
|
||||||
|
2. Delete the `LegacyPaymentProcessor.java` file.
|
||||||
|
3. Remove the `@ConditionalOnProperty` annotations.
|
||||||
|
|
||||||
Failure to remove old code paths is a primary cause of technical debt.
|
#### Python example
|
||||||
|
|
||||||
Things to consider:
|
In Python, the same pattern can be achieved with classes or, more idiomatically, with first-class functions.
|
||||||
|
|
||||||
"Zombie" flags: flags 100% rolled out but never removed. Implement diligent cleanup policies.
|
**1. Define the strategies and a factory**
|
||||||
|
|
||||||
Fear of removal: clear documentation and robust testing mitigate this.
|
```python
|
||||||
|
# In payment_strategies.py
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
Feature flags in monolithic applications: specific considerations
|
class PaymentStrategy(ABC):
|
||||||
Monoliths present unique nuances: shared codebase complexity, deployment cadence (entire monolith often deployed), vast testing surface area, and potentially complex refactoring for cleanup. In monoliths, a robust FeatureService and disciplined cleanup are paramount.
|
@abstractmethod
|
||||||
|
def process(self, amount: float) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
Things to consider:
|
class LegacyGatewayStrategy(PaymentStrategy):
|
||||||
|
def process(self, amount: float) -> str:
|
||||||
|
print(f"Processing ${amount} through LEGACY gateway.")
|
||||||
|
return "success_legacy"
|
||||||
|
|
||||||
Strangler fig pattern: flags are useful for gradual monolith refactoring.
|
class NewGatewayStrategy(PaymentStrategy):
|
||||||
|
def process(self, amount: float) -> str:
|
||||||
|
print(f"Processing ${amount} through NEW gateway.")
|
||||||
|
return "success_new"
|
||||||
|
|
||||||
Performance of flag evaluation: in high-throughput monolith sections, ensure flag evaluations aren't a bottleneck.
|
# The Factory encapsulates the decision logic.
|
||||||
|
def get_payment_strategy(feature_service, user_context) -> PaymentStrategy:
|
||||||
|
"""Selects the appropriate payment strategy based on a feature flag."""
|
||||||
|
if feature_service.is_enabled("new-payment-gateway", user_context):
|
||||||
|
return NewGatewayStrategy()
|
||||||
|
else:
|
||||||
|
return LegacyGatewayStrategy()
|
||||||
|
```
|
||||||
|
|
||||||
Beyond the code: supporting best practices for in-code success
|
**2. Use the strategy in your service**
|
||||||
Effective in-code flag management is significantly influenced by broader team practices:
|
|
||||||
|
|
||||||
Consistent naming conventions: establish clear conventions for flag names used in unleash and code.
|
```python
|
||||||
|
# In checkout_service.py
|
||||||
|
from payment_strategies import get_payment_strategy
|
||||||
|
|
||||||
Access control (rbac): utilize unleash's role-based access control. See unleash rbac documentation.
|
class CheckoutService:
|
||||||
|
def __init__(self, feature_service):
|
||||||
|
self._feature_service = feature_service
|
||||||
|
|
||||||
Flag lifespan planning: differentiate between short-lived and long-lived flags. Plan for removal.
|
def finalize_purchase(self, amount: float, user_context):
|
||||||
|
# The factory provides the correct strategy object.
|
||||||
|
strategy = get_payment_strategy(self._feature_service, user_context)
|
||||||
|
|
||||||
Clear documentation & communication: document flag purpose, scope, owner, and lifecycle.
|
# The service is clean of conditional logic.
|
||||||
|
result = strategy.process(amount)
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
Regular audits & dedicated cleanup time: schedule flag audits and allocate time for cleanup.
|
#### TypeScript/React example
|
||||||
|
|
||||||
Beware of dependent flags (parent-child hierarchies): while it might seem useful to group flags (e.g., a parent flag controlling several child flags), this can add significant complexity, especially if both parent and child flags have their own targeting rules. Misconfigurations can easily occur, and understanding rollout percentages becomes difficult (e.g., if parent is 50% and child is 50%, is it 25% overall?). If you use such structures, keep targeting rules simple, document hierarchies meticulously, and consider if flags can be designed to be managed independently to reduce this complexity.
|
In a frontend framework like React, you can apply the Strategy pattern to conditionally render different components.
|
||||||
|
|
||||||
Avoid defining core business logic in flags: feature flags are excellent for experimenting with business logic changes or controlling rollout, but they should not become the long-term definition of fundamental business rules. Core logic should reside in your main codebase or dedicated entitlement services. Relying on flags for permanent business rules can lead to:
|
**1. Create components for each strategy**
|
||||||
|
|
||||||
Dependency on external services: critical business functions shouldn't solely depend on a flagging service's availability.
|
```tsx
|
||||||
|
// src/components/OldUserProfile.tsx
|
||||||
|
const OldUserProfile = () => <div>Legacy Profile View</div>;
|
||||||
|
export default OldUserProfile;
|
||||||
|
|
||||||
Increased complexity & maintainability issues: business rules become scattered and entangled.
|
// src/components/NewUserProfile.tsx
|
||||||
|
const NewUserProfile = () => <div>New Redesigned Profile View</div>;
|
||||||
|
export default NewUserProfile;
|
||||||
|
```
|
||||||
|
|
||||||
Performance implications: complex evaluations for business logic can slow down flag checks.
|
**2. Use a "selector" component**
|
||||||
|
|
||||||
Security risks: accidental toggling could expose or alter critical business operations.
|
This component uses the flag to decide which strategy (component) to render.
|
||||||
Use flags to test and validate business logic changes, then integrate successful changes into your core system and remove the flag.
|
|
||||||
|
|
||||||
These practices create an environment where managing flags in code is a smoother, more predictable process.
|
```tsx
|
||||||
|
// src/components/UserProfile.tsx
|
||||||
|
import { useFeature } from '../hooks/useFeature'; // Your custom hook wrapping the Feature Service
|
||||||
|
import OldUserProfile from './OldUserProfile';
|
||||||
|
import NewUserProfile from './NewUserProfile';
|
||||||
|
|
||||||
|
const UserProfile = () => {
|
||||||
|
const isNewProfileEnabled = useFeature('newUserProfilePage');
|
||||||
|
|
||||||
|
// The strategy pattern selects which component to use.
|
||||||
|
const ProfileComponent = isNewProfileEnabled ? NewUserProfile : OldUserProfile;
|
||||||
|
|
||||||
|
return <ProfileComponent />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserProfile;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cleanup:**
|
||||||
|
|
||||||
|
1. Once the `newUserProfilePage` flag is fully rolled out, change the `UserProfile` component to render `NewUserProfile` directly.
|
||||||
|
2. Delete the `OldUserProfile.tsx` file.
|
||||||
|
|
||||||
|
|
||||||
|
## Managing flags in microservices
|
||||||
|
|
||||||
|
In a microservices environment, a single user action can trigger calls across multiple services. This introduces a significant challenge: **consistency**.
|
||||||
|
|
||||||
|
Imagine a `new-pricing-model` flag. A user requests a product page.
|
||||||
|
|
||||||
|
1. The `edge-service` receives the request, sees the flag is **on**, and passes the call to the `product-service`.
|
||||||
|
2. The `product-service` also sees the flag is **on** and prepares to show a promotional banner. It calls the `pricing-service`.
|
||||||
|
3. In the milliseconds between these calls, an operator disables the flag due to an issue.
|
||||||
|
4. The `pricing-service` now evaluates the flag, sees it as **off**, and returns the standard price.
|
||||||
|
|
||||||
|
The user is now in an inconsistent state: they see a promotion but get the old price.
|
||||||
|
|
||||||
|
### The principle: Evaluate once, pass the decision
|
||||||
|
|
||||||
|
The solution is to evaluate a feature flag's state **exactly one time** at the "edge" of your system—typically in the API Gateway or the first service that receives the external request. The *result* of that evaluation (e.g., `true` or `false`), not the flag itself, must then be propagated downstream to all other services in the call chain.
|
||||||
|
|
||||||
|
### Propagating context and decisions
|
||||||
|
|
||||||
|
To make this work, downstream services need the initial flag decisions and the user context (ID, location, etc.) used to make them. The standard, most robust way to achieve this is with **OpenTelemetry Baggage**.
|
||||||
|
|
||||||
|
While OpenTelemetry is known for distributed tracing, its Baggage specification is purpose-built to carry application-defined key-value pairs across process boundaries. It's the ideal mechanism for this use case.
|
||||||
|
|
||||||
|
Here's how it works:
|
||||||
|
|
||||||
|
1. The **edge service** receives a request, authenticates the user, and evaluates all necessary flags.
|
||||||
|
2. It uses the OpenTelemetry SDK to add the user context and the flag *decisions* to the current baggage.
|
||||||
|
3. When the edge service makes an HTTP call to a downstream service, the OpenTelemetry instrumentation automatically serializes the baggage into the `baggage` HTTP header and sends it.
|
||||||
|
4. The downstream service's instrumentation automatically receives this header, deserializes it, and makes the baggage available to your application code.
|
||||||
|
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Example in Java (Edge Service) using OpenTelemetry SDK
|
||||||
|
Baggage.current()
|
||||||
|
.toBuilder()
|
||||||
|
.put("user.id", "user-123")
|
||||||
|
.put("user.tier", "premium")
|
||||||
|
// Propagate the DECISION, not the flag name.
|
||||||
|
.put("decision.new-checkout.enabled", "true")
|
||||||
|
.build()
|
||||||
|
.makeCurrent(); // This context is now active and will be propagated.
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Example in Python (Downstream Service)
|
||||||
|
from opentelemetry import baggage
|
||||||
|
|
||||||
|
def handle_request():
|
||||||
|
# Retrieve the propagated context.
|
||||||
|
all_baggage = baggage.get_all()
|
||||||
|
user_id = all_baggage.get('user.id')
|
||||||
|
new_checkout_is_enabled = all_baggage.get('decision.new-checkout.enabled') == 'true'
|
||||||
|
|
||||||
|
# Use the consistent, propagated decision.
|
||||||
|
if new_checkout_is_enabled:
|
||||||
|
# ...
|
||||||
|
else:
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Adopting OpenTelemetry Baggage solves the context propagation problem at a platform level, providing a consistent mechanism for flags, tracing, A/B testing, and more.
|
||||||
|
|
||||||
|
## Minimizing code clutter and managing the flag lifecycle
|
||||||
|
|
||||||
|
Without discipline, your codebase will accumulate "flag debt"—a backlog of stale, forgotten, and risky flags. A rigorous governance and lifecycle management process is the only way to conquer this debt.
|
||||||
|
|
||||||
|
### Start with clear naming and metadata
|
||||||
|
|
||||||
|
The first line of defense is clarity. A flag named `temp_fix_v2` is a mystery waiting to happen.
|
||||||
|
**Enforce a strict naming convention** that encodes key information. A highly effective pattern is: `[team-owner]_[flag-type]_[description]`.
|
||||||
|
|
||||||
|
- `checkout_release_multistep-payment-flow`
|
||||||
|
- `search_experiment_new-ranking-algorithm`
|
||||||
|
- `platform_ops_database-failover-enabled`
|
||||||
|
|
||||||
|
Furthermore, **every flag check in the code should have a comment** containing essential metadata:
|
||||||
|
|
||||||
|
- **Ticket ID**: A link to the Jira, Asana, or GitHub issue.
|
||||||
|
- **Owner**: The team responsible for the flag's lifecycle.
|
||||||
|
- **Lifecycle**: The flag's purpose (e.g., Release, Experiment) and its expected removal date.
|
||||||
|
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// JIRA-451: Enable new dashboard for beta users.
|
||||||
|
// Owner: Team Phoenix
|
||||||
|
// Type: Release Flag. To be removed by end of Q3 2025.
|
||||||
|
if (featureService.isEnabled('phoenix_release_new-user-dashboard', userContext)) {
|
||||||
|
// ... new dashboard logic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### The process for safe removal
|
||||||
|
|
||||||
|
All temporary flags must eventually be removed. Follow a clear, repeatable process:
|
||||||
|
|
||||||
|
1. **Verify Status in Unleash**: Confirm the flag is either 100% rolled out and stable or permanently disabled.
|
||||||
|
2. **Solidify the Winning Path**: Remove the conditional logic (`if/else`) and all code associated with the "losing" path. This is the most critical step for avoiding dead code.
|
||||||
|
3. **Delete Flag Definition**: Remove the flag's name from your central constants or enums file. This will cause a compile-time or linter error if any references still exist.
|
||||||
|
4. **Clean Up the Abstraction Layer**: If you created a specific semantic method (e.g., `canUserSeeNewDashboard()`), remove it.
|
||||||
|
5. **Archive the Flag in Unleash**: Archive (don't delete) the flag in the Unleash UI. This preserves its history for auditing purposes.
|
||||||
|
6. **Test and Deploy**: Thoroughly test that the application behaves as expected with the flag's logic removed and deploy the changes.
|
||||||
|
|
||||||
|
### Automate cleanup to conquer flag debt
|
||||||
|
|
||||||
|
Relying on manual discipline for cleanup is a strategy destined for failure. **Automate your governance process.**
|
||||||
|
|
||||||
|
- **Automated Ticketing**: Use webhooks or integrations to automatically create a "Remove Flag" ticket in your project management tool when a flag has been at 100% rollout for a set period (e.g., two weeks).
|
||||||
|
- **Stale Flag Detection**: Use tools that can scan your codebase and the Unleash API to find "zombie flags"—flags that exist in Unleash but have no references in your code.
|
||||||
|
- **Scheduled Reviews**: Institute a regular, recurring meeting (e.g., "Flag Friday") where teams must review their active flags and justify their existence or schedule them for removal.
|
||||||
|
- **Update "Definition of Done"**: A feature isn't "done" until its associated feature flag has been removed from the code and archived in Unleash.
|
||||||
|
|
||||||
|
|
||||||
|
## Testing with feature flags
|
||||||
|
|
||||||
|
A common fear is the "combinatorial explosion" of tests—if you have 10 flags, you have 2¹⁰ = 1024 possible combinations. This fear is a myth. You don't need to test every permutation. A pragmatic and effective strategy focuses on a few critical states:
|
||||||
|
|
||||||
|
1. **The Production Default State**: A test suite that runs with all *new* feature flags turned **off**. This is your most important suite, as it verifies that adding new, dormant code has not caused regressions in existing functionality.
|
||||||
|
2. **The New Feature State**: For each new feature, a dedicated test run is executed with *that specific flag turned on* and all others off. This isolates the new feature and validates its functionality without interference.
|
||||||
|
3. **The Fallback State**: Test how your application behaves if the feature flagging service is unavailable. Your abstraction layer should handle this gracefully, falling back to safe default values.
|
||||||
|
|
||||||
|
### Testing in production safely
|
||||||
|
|
||||||
|
The most powerful testing practice enabled by feature flags is **testing in production**. This doesn't mean exposing bugs to your customers. It means using targeting rules to enable a feature *only for your internal teams* in the live production environment.
|
||||||
|
|
||||||
|
For example, you can configure a flag to be "on" only for users with an `@your-company.com` email address. This allows your QA team and developers to interact with the new feature on the actual production infrastructure, connected to real production services—a context that is impossible to replicate perfectly in a staging environment. If a bug is found, it has zero impact on real users and can be fixed before a public release.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
## Frequently asked questions (FAQs)
|
## Frequently asked questions (FAQs)
|
||||||
Q1: where should i define my feature flag names in the code?
|
|
||||||
A: centralize them in a dedicated file using constants or enums for a single source of truth.
|
|
||||||
|
|
||||||
Q2: should i call the unleash sdk directly everywhere, or build a helper service?
|
**Q: Where should I define my feature flag names in the code?**
|
||||||
A: build a helper service (abstraction layer) to decouple your app, centralize logic, and improve testability.
|
A: Centralize them in a dedicated file using constants or enums. This creates a single source of truth and prevents typos.
|
||||||
|
|
||||||
Q3: how do i handle code for complex features controlled by flags cleanly?
|
**Q: Should I call the Unleash SDK directly everywhere or build a helper service?**
|
||||||
A: use design patterns like strategy or encapsulate logic in separate modules/classes. Prioritize readability for each code path, even if it means not strictly adhering to dry.
|
A: Always build a helper service (an abstraction layer). It decouples your app from the SDK, centralizes logic like error handling, and dramatically simplifies maintenance and future migrations.
|
||||||
|
|
||||||
Q4: how do we avoid "flag debt" (messy, old flag code)?
|
**Q: How do I cleanly handle code for complex features controlled by flags?**
|
||||||
A: document flags clearly, group related changes under single flags where appropriate, conduct regular audits, and prioritize removing obsolete flags and their old code.
|
A: Use design patterns like the Strategy pattern to encapsulate the different behaviors in separate classes or modules. This avoids messy `if/else` blocks and makes cleanup trivial.
|
||||||
|
|
||||||
Q5: when and how should i remove a feature flag from the code?
|
**Q: How do we avoid "flag debt" (a messy codebase full of old flags)?**
|
||||||
A: remove a flag once fully rolled out and stable, or an experiment concludes. Delete conditional logic, the old code path, the flag definition, and archive it in unleash. Test thoroughly.
|
A: Enforce strict naming conventions, document flags with metadata in code comments, conduct regular audits, and automate the cleanup process as much as possible.
|
||||||
|
|
||||||
Q6: what's the best way to evaluate a flag: once per request or multiple times?
|
**Q: When and how should I remove a feature flag from the code?**
|
||||||
A: evaluate once per user request at the entry point and propagate its outcome for consistency.
|
A: Remove a flag once it is fully rolled out and stable, or an experiment has concluded. The process involves removing the conditional logic, deleting the old code path, removing the flag's definition, and finally archiving it in the Unleash UI.
|
||||||
|
|
||||||
Q7: how high up in my code should i check a feature flag?
|
**Q: What's the best way to evaluate a a feature flag in code?**
|
||||||
A: evaluate at the highest practical level of abstraction. For backend logic, evaluate closer to the affected module, based on a flag state determined once per request.
|
A: Evaluate it **once per user request** at the highest practical entry point of your application. Propagate the *decision* (the boolean result) to downstream components to ensure a consistent experience.
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
Effectively managing feature flags within your codebase is as crucial as managing them in the unleash platform. By establishing clear definitions, leveraging abstraction layers, adopting strategic evaluation points, structuring conditional logic thoughtfully, and committing to regular cleanup, you can effectively use unleash. These in-code practices, supported by broader team discipline, will ensure that feature flags remain a valuable tool for software development, not a source of technical debt.
|
|
||||||
|
|
||||||
For more information on getting started with unleash, visit the Unleash documentation home.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
2. Call your flag in as few places as possible
|
|
||||||
It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another.
|
|
||||||
|
|
||||||
If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example:
|
|
||||||
|
|
||||||
JavaScript
|
|
||||||
|
|
||||||
function useBetaFeature() {
|
|
||||||
return unleash.isFeatureEnabled('beta-feature')
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user