Courses/Feature Flags & A/B Testing
Standalone Course~30 min4 challenges

Feature Flags & A/B Testing

Grit ships an in-memory feature-flag engine with sticky bucketing, percentage rollouts, allow/blocklists, and realtime push when an admin toggles a flag. It lets you decouple deploy from release — merge code whenever, turn it on when you're ready.

Your codeFlag EngineStore & clientsreadloadpushengine.Enabled()your checkEnginein-memory mapflags tablesourceAdmin togglerefresh nowRealtimeflag.updated
Memory readsRealtime push
Reads hit an in-memory map; admin toggles refresh instantly and push to clients

Reference docs: Feature Flags →

Flags vs deploys

Ship code dark, then turn it on for whoever you choose — no redeploy.

Percentage rollouts

Release to 1%, then 10%, then everyone, watching Pulse as you go.

Sticky bucketing

A user always lands in the same variant, so the experience stays consistent.

Realtime push

Toggle a flag in the admin and connected clients update without a refresh.

Why feature flags?

A feature flag is a runtime switch that decides whether a piece of functionality is active. Wrap new code in a flag and you can merge it to main immediately, keep it off in production, then enable it for internal users, then 5% of traffic, then everyone — all without shipping new code.

Decoupling deploy from release: Deploy = your code is running on the server. Release = users can actually see the feature. Flags split these two events so a risky launch becomes a config change you can reverse in seconds, not a rollback.

Checking a flag

On the server, gate logic behind flags.IsEnabled. The request context carries the current user, which the engine uses for sticky bucketing.

internal/handlers/checkout.go
func (h *CheckoutHandler) Create(c *gin.Context) {
    if flags.IsEnabled(c, "new_checkout") {
        h.newCheckout(c)
        return
    }
    h.legacyCheckout(c)
}

On the client, the generated hook reads the same flags and re-renders when an admin pushes a change over the realtime hub.

apps/web/app/checkout/page.tsx
const newCheckout = useFlag('new_checkout')
return newCheckout ? <CheckoutV2 /> : <CheckoutV1 />

Rollouts & targeting

Sticky bucketing: A user is hashed to a stable number in [0,100). A 10% rollout enables the flag for everyone whose number is below 10 — and because the hash is stable, the same user stays in the same bucket across requests and sessions. No flag-flapping experience.
Allow / blocklist: Explicit overrides that win over the percentage. Add your QA team to the allowlist to always see a flag, or blocklist an account that must never get an experiment.
ControlEffect
Enabled toggleMaster on/off for the flag
PercentageShare of users who get it (sticky)
AllowlistAlways-on for these users
BlocklistAlways-off for these users
For a clean A/B test, hold the percentage steady and compare a metric between the two buckets in Pulse. Resist the urge to change the split mid-experiment — it pollutes the comparison.

Practice

1

Challenge: Gate a feature

Wrap any handler branch behind flags.IsEnabled(c, "my_feature"). Create the flag in the admin with the toggle off. Confirm the old path runs, then flip it on and confirm the new path runs.

2

Challenge: Roll out to 10%

Set the flag to a 10% rollout. Hit the endpoint as several different users. Roughly what fraction get the new path? Does the same user always get the same result?

3

Challenge: Allowlist yourself

With the percentage at 0%, add your own account to the allowlist. Do you see the feature while everyone else doesn't? Which wins — the allowlist or the percentage?

4

Challenge: Watch the realtime push

Open the client page that uses useFlag. With the page open, toggle the flag in the admin. Does the UI change without a manual refresh? What mechanism makes that happen?