Feature Flags
The featureflag package is a thin wrapper around the OpenFeature Go SDK — the CNCF standard for feature-flag evaluation. Your application code calls one method (IsEnabled), and the actual flag storage + rule engine lives in a provider that you register at startup. Swap providers (in-memory for dev, LaunchDarkly for production, go-feature-flag for self-hosted, or a custom implementation) by changing a single line.
Import
import "github.com/gofastadev/gofasta/pkg/featureflag"Key Types
FeatureFlagService
type FeatureFlagService struct {
// unexported fields
}A thin wrapper around an OpenFeature *openfeature.Client. Every evaluation delegates to whichever provider the application has registered globally via openfeature.SetProvider.
Key Functions
| Function | Signature | Description |
|---|---|---|
NewFeatureFlagService | func NewFeatureFlagService(logger *slog.Logger) *FeatureFlagService | Returns a service bound to whichever OpenFeature provider is currently registered. Falls back to the SDK’s NoopProvider if none has been set — flag checks then resolve to the caller-supplied default. |
NewInMemoryService | func NewInMemoryService(flags map[string]memprovider.InMemoryFlag, logger *slog.Logger) (*FeatureFlagService, error) | Bootstraps the SDK with an in-memory provider populated from flags and returns a service wired against it. Convenience for local development, tests, and small apps that don’t need a rule engine. |
IsEnabled | func (s *FeatureFlagService) IsEnabled(ctx context.Context, flagKey, userID string, attributes map[string]any) bool | Evaluates a boolean flag for the given user context. Returns false on evaluation error (logged at debug level) — flag checks must never take the app down. |
Close | func (s *FeatureFlagService) Close() | Calls openfeature.Shutdown to release any provider-held resources. Safe to call without a registered provider. |
The Provider Model
OpenFeature is the interface contract. Providers are adapters that plug any flag system behind that contract. Think of the relationship the same way you think about database/sql + a driver:
your app → openfeature.Client → Provider (adapter) → flag source- SDK — stable public API (
openfeature.Client.BooleanValue(...)), never changes regardless of backend. - Provider — the swappable plugin that actually resolves flag values. Registered once at startup via
openfeature.SetProvider(someProvider).
Usage
Zero-Config: No Provider Registered
With no provider registered, the SDK’s default NoopProvider answers every evaluation with the caller-supplied default. This is safe — your application runs without a feature-flag backend; every flag simply takes its default branch.
svc := featureflag.NewFeatureFlagService(logger)
if svc.IsEnabled(ctx, "dark-mode", userID, nil) {
// never true — NoopProvider always returns the default (false)
}Useful for first-run scaffolds, test fixtures, or services where feature flagging is not yet wired up.
In-Memory Provider (recommended default)
For local development, CI, and small applications, the in-memory provider is the lowest-friction choice — flags are defined in Go code, evaluated instantly, zero network traffic:
import "github.com/open-feature/go-sdk/openfeature/memprovider"
flags := map[string]memprovider.InMemoryFlag{
"dark-mode": {
Key: "dark-mode",
State: memprovider.Enabled,
DefaultVariant: "on",
Variants: map[string]any{"on": true, "off": false},
},
"new-checkout": {
Key: "new-checkout",
State: memprovider.Enabled,
DefaultVariant: "off",
Variants: map[string]any{"on": true, "off": false},
},
}
svc, err := featureflag.NewInMemoryService(flags, logger)
if err != nil {
log.Fatalf("failed to init feature flags: %v", err)
}
defer svc.Close()Swapping to a Production Provider
For file-based rules + AB testing + usage tracking, register the go-feature-flag provider before constructing the service:
import (
"github.com/open-feature/go-sdk/openfeature"
gff "github.com/thomaspoignant/go-feature-flag/providers/go-feature-flag-in-process-provider"
"github.com/thomaspoignant/go-feature-flag/retriever/fileretriever"
)
provider, err := gff.NewProvider(gff.ProviderOptions{
Retrievers: []retriever.Retriever{
&fileretriever.Retriever{Path: "configs/features.yaml"},
},
PollingInterval: 60 * time.Second,
})
if err != nil {
log.Fatalf("init go-feature-flag provider: %v", err)
}
if err := openfeature.SetProviderAndWait(provider); err != nil {
log.Fatalf("register provider: %v", err)
}
svc := featureflag.NewFeatureFlagService(logger)For a commercial SaaS like LaunchDarkly , swap the provider constructor — the rest of the code stays identical:
import ldprovider "github.com/launchdarkly/openfeature-go-server-sdk-provider"
ldClient, _ := ldclient.MakeClient("sdk-key", 5*time.Second)
openfeature.SetProvider(ldprovider.NewProvider(ldClient))
svc := featureflag.NewFeatureFlagService(logger)For Flagd (CNCF, gRPC-based):
import "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg"
openfeature.SetProvider(flagd.NewProvider())
svc := featureflag.NewFeatureFlagService(logger)In every case, application code is identical. Only the provider registration line changes.
Evaluating Flags
// Simple check — no user context
if svc.IsEnabled(ctx, "dark-mode", userID, nil) {
// serve dark mode UI
}
// With evaluation-context attributes for targeting rules
attrs := map[string]any{
"plan": "premium",
"country": "US",
}
if svc.IsEnabled(ctx, "new-dashboard", userID, attrs) {
// show new dashboard
}The userID becomes the OpenFeature targeting key. Providers that support segment/percentage rollout use it (together with attributes) to decide which variant to return.
Using in Controllers
func (c *DashboardController) Index(w http.ResponseWriter, r *http.Request) error {
claims := auth.ClaimsFromContext(r.Context())
if c.ffService.IsEnabled(r.Context(), "new-dashboard", claims.UserID, nil) {
return httputil.OK(w, newDashboardData)
}
return httputil.OK(w, legacyDashboardData)
}Configuration via config.yaml
feature_flag:
enabled: true
config_path: configs/features.yamlenabled is a master switch your application reads before constructing the service. config_path is provider-specific — if the provider you register is file-driven (like go-feature-flag), point it at this path; other providers can ignore the field.
Environment variables use the GOFASTA_ prefix (e.g., GOFASTA_FEATURE_FLAG_ENABLED, GOFASTA_FEATURE_FLAG_CONFIG_PATH).
Writing a Custom Provider
Any type that implements openfeature.FeatureProvider is a valid provider. The interface is small — a few resolution methods plus metadata — so wiring flags sourced from a Postgres table, a Consul KV store, or an internal config service is ~20 lines:
type myProvider struct{ /* ... */ }
func (p *myProvider) Metadata() openfeature.Metadata { /* ... */ }
func (p *myProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
// your logic here
}
// ... plus String, Float, Int, Object evaluation methods
openfeature.SetProvider(&myProvider{})Why OpenFeature
Gofasta wraps OpenFeature (not any one engine) to uphold its opt-out-default philosophy: the default provider is swappable without touching the application’s flag-checking code. The same interface binds in-memory dev flags, self-hosted rule engines, commercial SaaS, and your own custom backends — you pick whichever fits this deployment without rewriting controllers.
Related Pages
- Config — Feature flag configuration
- Middleware — Inject flag service into request context
- Observability — Track flag evaluation metrics