Skip to content

Implement Server-Side Feature Flags

⏱ 30 minutes intermediate

Client-side flags execute in the browser, where they are visible, slow to load, and easy to circumvent. Server-side flags evaluate on your infrastructure, keeping business logic hidden and decisions fast. Use them for backend features, pricing rules, API behavior changes, and any logic that should not be exposed to end users.

Install the Optimizely SDK
bash
npm install @optimizely/optimizely-sdk
bash
pip install optimizely-sdk
bash
dotnet add package Optimizely.SDK

Create a single SDK instance at application startup. The SDK downloads the datafile (your project configuration) and keeps it in memory for fast evaluations. Never create a new instance per request.

Initialize the Optimizely client
javascript
import { createInstance } from '@optimizely/optimizely-sdk';

// Create once at application startup
const optimizely = createInstance({
  sdkKey: process.env.OPTIMIZELY_SDK_KEY,
  datafileOptions: {
    autoUpdate: true,
    updateInterval: 60_000, // Poll for changes every 60 seconds
  },
});

// Wait for the datafile to download before serving traffic
await optimizely.onReady();
console.log('Optimizely SDK initialized');
python
import os
from optimizely import optimizely
from optimizely.config_manager import PollingConfigManager

# Create once at application startup
config_manager = PollingConfigManager(
    sdk_key=os.environ['OPTIMIZELY_SDK_KEY'],
    update_interval=60,  # Poll for changes every 60 seconds
)

client = optimizely.Optimizely(config_manager=config_manager)
print('Optimizely SDK initialized')
csharp
using OptimizelySDK;

// In Startup.cs or Program.cs — create once at application startup
var optimizely = OptimizelyFactory.NewDefaultInstance(
    Environment.GetEnvironmentVariable("OPTIMIZELY_SDK_KEY")
);

// Register as a singleton in your DI container
builder.Services.AddSingleton(optimizely);

Key points:

  • One instance per application — The SDK caches the datafile in memory. Multiple instances waste resources and can cause inconsistent evaluations.
  • Auto-update — Enable polling so flag changes propagate without redeployment.
  • SDK key — Found in the Optimizely application under SettingsEnvironments. Use the correct key for each environment (development, staging, production).

A user context represents the person (or entity) making a request. Pass a unique identifier and any attributes needed for audience targeting.

Create a user context per request
javascript
// In your request handler (Express, Fastify, etc.)
app.get('/api/checkout', (req, res) => {
  const user = optimizely.createUserContext(req.userId, {
    country: req.geoCountry,
    plan: req.user.plan,
    device_type: req.headers['x-device-type'] || 'desktop',
    is_employee: req.user.isEmployee,
  });

  const decision = user.decide('new_checkout_flow');
  // ... handle decision
});
python
# In your request handler (Flask, Django, FastAPI, etc.)
@app.route('/api/checkout')
def checkout(request):
    user = client.create_user_context(request.user_id, {
        'country': request.geo_country,
        'plan': request.user.plan,
        'device_type': request.headers.get('X-Device-Type', 'desktop'),
        'is_employee': request.user.is_employee,
    })

    decision = user.decide('new_checkout_flow')
    # ... handle decision
csharp
// In your controller or request handler
[HttpGet("/api/checkout")]
public IActionResult Checkout()
{
    var user = _optimizely.CreateUserContext(HttpContext.User.GetUserId(), new UserAttributes
    {
        { "country", Request.Headers["X-Geo-Country"].ToString() },
        { "plan", HttpContext.User.GetPlan() },
        { "device_type", Request.Headers["X-Device-Type"].ToString() },
        { "is_employee", HttpContext.User.IsEmployee() },
    });

    var decision = user.Decide("new_checkout_flow");
    // ... handle decision
}

User ID guidance:

  • Use a stable identifier — logged-in user ID, account ID, or device ID
  • The same user ID always gets the same variation (sticky bucketing)
  • For anonymous users, generate a UUID and store it in a cookie or session

The decide method returns a decision object containing the flag state, variation key, and variable values.

Evaluate a flag and use variables
javascript
const decision = user.decide('search_algorithm');

if (decision.enabled) {
  // Flag is on for this user
  const algorithm = decision.variables.algorithm_name;   // e.g., 'semantic_v2'
  const maxResults = decision.variables.max_results;     // e.g., 50
  const results = search(query, { algorithm, maxResults });
} else {
  // Flag is off — use defaults
  const results = search(query, { algorithm: 'keyword_v1', maxResults: 20 });
}

// Log which variation was served (for debugging)
console.log(`Flag: search_algorithm | Variation: ${decision.variationKey}`);
python
decision = user.decide('search_algorithm')

if decision.enabled:
    # Flag is on for this user
    algorithm = decision.variables['algorithm_name']   # e.g., 'semantic_v2'
    max_results = decision.variables['max_results']    # e.g., 50
    results = search(query, algorithm=algorithm, max_results=max_results)
else:
    # Flag is off — use defaults
    results = search(query, algorithm='keyword_v1', max_results=20)

# Log which variation was served (for debugging)
print(f'Flag: search_algorithm | Variation: {decision.variation_key}')
csharp
var decision = user.Decide("search_algorithm");

if (decision.Enabled)
{
    // Flag is on for this user
    var algorithm = decision.Variables.GetValue<string>("algorithm_name");
    var maxResults = decision.Variables.GetValue<int>("max_results");
    var results = _searchService.Search(query, algorithm, maxResults);
}
else
{
    // Flag is off — use defaults
    var results = _searchService.Search(query, "keyword_v1", 20);
}

// Log which variation was served (for debugging)
_logger.LogInformation("Flag: search_algorithm | Variation: {Variation}", decision.VariationKey);

Send conversion events when users complete meaningful actions. Events are matched to running experiments for results analysis.

Track conversion events
javascript
// After a successful search interaction
user.trackEvent('search_click');

// After a purchase — include revenue in cents
user.trackEvent('purchase', {
  revenue: 2499,
  quantity: 1,
});
python
# After a successful search interaction
user.track_event('search_click')

# After a purchase — include revenue in cents
user.track_event('purchase', {
    'revenue': 2499,
    'quantity': 1,
})
csharp
// After a successful search interaction
user.TrackEvent("search_click");

// After a purchase — include revenue in cents
user.TrackEvent("purchase", new EventTags
{
    { "revenue", 2499 },
    { "quantity", 1 },
});

Flag evaluations should never break your application. The SDK is designed to fail gracefully — if the datafile is unavailable or an error occurs, decide returns a decision with enabled = false. Build your code to handle this.

Defensive flag evaluation
javascript
function getSearchConfig(user) {
  try {
    const decision = user.decide('search_algorithm');

    if (decision.enabled && decision.variables.algorithm_name) {
      return {
        algorithm: decision.variables.algorithm_name,
        maxResults: decision.variables.max_results ?? 20,
      };
    }
  } catch (error) {
    console.error('Flag evaluation failed:', error);
  }

  // Default values — always have a fallback
  return { algorithm: 'keyword_v1', maxResults: 20 };
}
python
def get_search_config(user):
    try:
        decision = user.decide('search_algorithm')

        if decision.enabled and decision.variables.get('algorithm_name'):
            return {
                'algorithm': decision.variables['algorithm_name'],
                'max_results': decision.variables.get('max_results', 20),
            }
    except Exception as e:
        print(f'Flag evaluation failed: {e}')

    # Default values — always have a fallback
    return {'algorithm': 'keyword_v1', 'max_results': 20}
csharp
public SearchConfig GetSearchConfig(OptimizelyUserContext user)
{
    try
    {
        var decision = user.Decide("search_algorithm");

        if (decision.Enabled)
        {
            return new SearchConfig
            {
                Algorithm = decision.Variables.GetValue<string>("algorithm_name") ?? "keyword_v1",
                MaxResults = decision.Variables.GetValue<int?>("max_results") ?? 20,
            };
        }
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Flag evaluation failed");
    }

    // Default values — always have a fallback
    return new SearchConfig { Algorithm = "keyword_v1", MaxResults = 20 };
}
  • Evaluations are local — The SDK evaluates flags in memory (microseconds). No network calls occur at decision time.
  • Datafile polling is background — Updates download asynchronously without blocking requests.
  • Batch events — The SDK batches track calls and sends them in bulk to reduce network overhead.
  • Minimize attributes — Only pass attributes you actually use in audience conditions. Passing dozens of unused attributes wastes memory.
IssueCauseFix
decide always returns enabled: falseSDK not initialized or wrong SDK keyVerify the SDK key and check onReady() resolved
Same user gets different variationsDifferent user IDs across requestsUse a stable, consistent user ID
Flag changes not reflectedDatafile not pollingConfirm autoUpdate is enabled and the interval is reasonable
Events not appearing in resultsEvent name mismatch or event batching delayVerify event names match and wait 5-10 minutes for batched events
High memory usageCreating new SDK instances per requestUse a single shared instance (singleton pattern)