Skip to content
Use feature flags in your service

Use feature flags in your service

This guide walks through the full lifecycle of a feature flag in a Theseus service: defining the flag, evaluating it in your Go code, toggling it in production, and removing it once the rollout is complete. It uses the feature-flags service — Flipt running in production, serving an OpenFeature-compatible evaluation API — and the LabKit v2 featureflag client. The go-service-template exemplar implements everything below, so you can read this guide against working code.

For when to reach for a flag at all — and when configuration is the right tool instead — see the feature flag strategy.

The feature-flags service is available for production use. For questions or support, reach out via the feature-flags repository.

How it flows

Flags are plain YAML in the feature-flags repository. The running service polls the repository, so a merged change is live within about 30 seconds — no deploy of the flag service, and no deploy of yours:

    flowchart LR
    yaml["feature-flags repo<br/>flags/&lt;namespace&gt;/features.yaml"] -->|"git poll (~30s)"| flipt["feature-flags service<br/>(Flipt)"]
    flipt -->|"HTTP evaluate"| svc["Your service<br/>LabKit featureflag client"]
    svc -->|"flag on"| new["New behaviour"]
    svc -->|"flag off / evaluation fails"| old["Existing behaviour<br/>(fail closed)"]
  

Define the flag

Each application has its own namespace, named after the application. Add your flag to your namespace’s YAML file in the feature-flags repository and open a merge request. New flags start disabled:

# flags/my-service/features.yaml
version: "1.5"
namespace:
  key: my-service
  name: My Service
flags:
  - key: enable-new-api
    name: Enable New API
    type: BOOLEAN_FLAG_TYPE
    enabled: false

If your application does not have a namespace yet, add one in the same merge request — see managing-flags.md in the feature-flags repository for the details.

Configure your service

The client needs two values: the evaluation endpoint and your namespace. Deliver them like any other application configuration — the exemplar carries them in its Fairway appConfig tree:

# fairway.yaml
spec:
  values:
    appConfig:
      data:
        feature_flags:
          namespace: my-service
          endpoint: http://flipt:8080

Environments that deliver configuration through environment variables instead (gitlab-shell’s production deployment, for example) set FEATURE_FLAG_ENDPOINT and FEATURE_FLAG_NAMESPACE directly.

Leave the endpoint unset in environments that have no feature-flags service — which is most of them, including virtually all self-managed installations. Your service must start and run correctly without it; the client degrades gracefully rather than failing at startup.

Initialise the client

Create the client once at application startup, in your composition root, and inject it into the domain layer like any other dependency:

import "gitlab.com/gitlab-org/labkit/featureflag"

ffClient, err := featureflag.NewWithConfig(ctx, &featureflag.Config{
	Namespace: cfg.FeatureFlags.Namespace,
})
if err != nil {
	return fmt.Errorf("initialise feature flag client: %w", err)
}

The constructor does not validate connectivity — it succeeds even when the endpoint is unreachable, and evaluation calls fail gracefully instead. Do not treat an unreachable flag service as a startup error.

Evaluate the flag

Evaluate at the decision point, passing the flag key, a default value, and an evaluation context:

import "github.com/open-feature/go-sdk/openfeature"

details, err := ffClient.BooleanValueDetails(
	ctx,
	"enable-new-api",
	false,
	openfeature.NewEvaluationContext(userID, nil),
)
if err != nil {
	logger.WarnContext(ctx, "feature flag evaluation failed",
		slog.String("flag", "enable-new-api"),
		labkitlog.Error(err),
	)
}

if err == nil && details.Value {
	// new behaviour
} else {
	// existing behaviour
}

Always default to false. The second argument is what evaluation returns when the service is unreachable, the flag is missing, or anything else goes wrong. Defaulting to off means a degraded or absent flag service has zero impact on your users — they get the existing behaviour, never an untested one. Log the failure at warning level and move on; do not fail the request.

See consuming-flags.md in the feature-flags repository for the full client reference, including HTTP and gRPC usage.

Toggle the flag

Enabling the feature is a one-line merge request against the feature-flags repository:

  - key: enable-new-api
    name: Enable New API
    type: BOOLEAN_FLAG_TYPE
    enabled: true

Merge it and the change is live within about 30 seconds. Rolling back is the same merge request in reverse — no pipeline, no redeploy, no revert of your service.

Clean up

A flag is scaffolding around a rollout, not a permanent switch. Once the new behaviour has been fully enabled and is stable:

  1. Remove the evaluation call and the old code path from your service.
  2. Remove the flag entry from the feature-flags repository.

If you find yourself wanting to keep the flag long-term, that is a sign the value belongs in application configuration instead — see Configuration vs Feature Flags.

Related reading

Last updated on