Skip to content

Feature Flags vs Entitlements ​

Cocoar.Configuration has built-in support for two related but distinct concepts: feature flags and entitlements.

What They Are ​

Feature flags and entitlements are computed values, not stored values. They don't exist as their own entries in a config file — they are derived from configuration at runtime.

A flag or entitlement is a function that reads one or more configuration values and returns a result. Sometimes this is a simple pass-through (a single boolean from config), sometimes it's a computation combining multiple config sources:

csharp
// Simple: passes through a single config value
public FeatureFlag<bool> NewDashboard => () => Config.UseNewDashboard;

// Computed: combines multiple values into a decision
public FeatureFlag<UserContext, bool> BetaCheckout => user =>
    Config.BetaEnabled && Config.BetaRegions.Contains(user.Region);

The key insight: you cannot set a flag directly. You change the underlying configuration values, and the flag recomputes. This keeps the config layer as the single source of truth.

Feature Flags ​

Feature flags answer: "Does this code run?"

They represent temporary, operational toggles — rollouts, A/B tests, kill switches. They are owned by engineering/ops and must have an explicit expiration date.

csharp
public partial class BillingFlags : IFeatureFlags<BillingConfig>
{
    public DateTimeOffset ExpiresAt => new(2026, 6, 1, 0, 0, 0, TimeSpan.Zero);

    /// <summary>Enables the redesigned billing dashboard.</summary>
    public FeatureFlag<bool> NewDashboard => () => Config.UseNewDashboard;

    /// <summary>Gates beta checkout for specific users.</summary>
    public FeatureFlag<UserContext, bool> BetaCheckout => user => Config.BetaEnabled && user.IsBeta;
}

The partial class implements IFeatureFlags<TConfig>, and the source generator produces the constructor and Config property. Config reads IReactiveConfig<TConfig>.CurrentValue, so it always reflects the latest configuration.

After the expiration date, the flags keep working — but the health API reports Degraded, signaling that cleanup is overdue.

Entitlements ​

Entitlements answer: "May this actor do this?"

They represent permanent business logic — plan tiers, feature availability, permission limits. They are owned by product/business and have no expiration date.

csharp
public partial class PlanEntitlements : IEntitlements<PlanConfig>
{
    /// <summary>Maximum allowed team members.</summary>
    public Entitlement<int> MaxUsers => () => Config.UserLimit;

    /// <summary>Whether this plan can export data.</summary>
    public Entitlement<bool> CanExport => () => Config.Tier != "free";
}

The Litmus Test ​

A feature flag without an expiration date is an entitlement in disguise.

If you're unsure which to use, ask: will this toggle ever be removed? If yes, it's a feature flag. If it's permanent product logic, it's an entitlement.

How They Work ​

Both are pure functions over configuration state:

  1. Each property is a delegate that reads from Config (the source-generated property backed by IReactiveConfig<T>.CurrentValue)
  2. When configuration changes, the next invocation returns the new result
  3. No per-request state, no caching — just a function call
  4. No constructor needed — the source generator handles wiring

Why Delegates? ​

Flag and entitlement properties use delegate types (FeatureFlag<T>, Entitlement<T>) instead of plain properties or methods. This is a deliberate architectural constraint:

Enforced simplicity. A FeatureFlag<TResult> takes zero parameters. A FeatureFlag<TContext, TResult> takes exactly one. You cannot accidentally add extra parameters — the type system prevents it.

REST API compatibility. The REST evaluation pipeline maps directly to delegates:

  • FeatureFlag<TResult> → GET /flags/{Class}/{Property} — no input needed
  • FeatureFlag<TContext, TResult> → POST /flags/{Class}/{Property} — request body is deserialized, passed through a Context Resolver, and the resolved context becomes the single delegate argument

Pure functions. Flags must not inject services or call databases. All they receive is configuration (via Config) and optionally a context object. This keeps evaluation fast, deterministic, and debuggable — no hidden I/O, no async, no side effects.

Flags are pure — side effects belong in resolvers

If a flag needs data from a database or external service, that logic belongs in a Context Resolver. The resolver hydrates a rich context object; the flag makes a decision based on it.

csharp
// ✗ WRONG — DB call in a flag
BetaCheckout = async user => await db.Users.IsBeta(user.Id);

// ✓ RIGHT — Resolver fetches data, flag decides
// Resolver:
public async Task<UserContext> ResolveAsync(UserIdRequest req)
    => new UserContext(await db.Users.FindAsync(req.UserId));

// Flag: pure function
BetaCheckout = user => Config.BetaEnabled && user.IsBeta;

With Context ​

Both support contextual evaluation — flags and entitlements that depend on runtime context (current user, tenant, request):

csharp
/// <summary>Gates beta checkout for specific users.</summary>
public FeatureFlag<UserContext, bool> BetaCheckout => user => Config.BetaEnabled && user.IsBeta;

The context is resolved via Context Resolvers — a bridge between HTTP request data and your domain model.

When to Use Cocoar Flags vs. Dedicated Flag Services ​

Cocoar flags and dedicated feature flag services (LaunchDarkly, Unleash, Flagsmith, etc.) solve different problems. Understanding the trade-offs helps you pick the right tool — or use both.

Cocoar flags are a good fit when:

  • Flags are derived from your own configuration — plan tiers, tenant settings, deployment environment. The flag is a pure function over config you already manage.
  • You need tight integration with your config layer — flags recompute automatically when configuration changes, share the same health monitoring, and follow the same lifecycle.
  • You're an ISV controlling feature rollout across your own instances — each instance has its own config, and flags reflect that instance's state.
  • You want type-safe, compile-time validated flags — the source generator catches missing flags, wrong return types, and expired flags at build time.

Dedicated flag services are a better fit when:

  • You need a management UI for non-developers — product managers toggling flags without code changes or deployments.
  • You need percentage rollouts, A/B testing, or experimentation infrastructure — statistical analysis, cohort management, and gradual rollout are their core competency.
  • You need cross-platform flag evaluation — the same flag evaluated consistently across mobile apps, web frontends, and backend services with server-side SDKs.

They can coexist. Use Cocoar flags for config-derived decisions (plan limits, tenant features, deployment-specific toggles) and a dedicated service for user-targeting and experimentation. They solve different problems and don't conflict — a Cocoar entitlement might gate whether a feature is available on a plan, while a LaunchDarkly flag controls whether that feature's new UI is shown to 10% of users.

Comparison ​

Feature FlagsEntitlements
PurposeTemporary operational togglesPermanent business logic
ExpiryRequired (ExpiresAt)None
Health signalDegraded when expiredNone
InterfaceIFeatureFlags<TConfig>IEntitlements<TConfig>
Property typeFeatureFlag<TResult> or FeatureFlag<TContext, TResult>Entitlement<TResult> or Entitlement<TContext, TResult>
Owned byEngineering / OpsProduct / Business
LifecycleCreate → roll out → expire → removeCreate → keep forever

Released under the Apache-2.0 License.