June 5, 2026

Feature flags without a vendor: a minimal in-house approach

backend
Feature flags cover graphic for erkshitiz.com.np

We wanted to ship a rewritten checkout flow behind a flag so we could roll it out to a slice of users first. The obvious move was to sign up for LaunchDarkly or Split, but when I actually looked at what we needed, it was four flags total, all toggled by engineers, none of them needing scheduled rollouts or A/B experiment reporting. Paying a monthly fee and wiring up an SDK for that felt backwards.

So we built the smallest thing that could work: a table and a function.

The table is nothing fancy. Just a name, whether it is on, and an optional rollout percentage for gradual releases:

CREATE TABLE feature_flags (
    key TEXT PRIMARY KEY,
    enabled BOOLEAN NOT NULL DEFAULT false,
    rollout_percent SMALLINT NOT NULL DEFAULT 0 CHECK (rollout_percent BETWEEN 0 AND 100),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

enabled is the on/off switch for everyone. rollout_percent is for the in-between case, when we want the flag on for some fraction of users rather than all or nothing. If enabled is true and rollout_percent is 100, that flag is fully live. If it is somewhere in between, we need a consistent way to decide which users are in and which are out.

That is where hashing the user ID comes in. We hash the ID, mod it by 100, and compare against the rollout percentage. The same user always lands on the same side of that line, so nobody flips in and out of the new checkout flow mid-session:

func IsEnabled(flag Flag, userID string) bool {
	if !flag.Enabled {
		return false
	}
	if flag.RolloutPercent >= 100 {
		return true
	}
	if flag.RolloutPercent <= 0 {
		return false
	}

	h := fnv.New32a()
	h.Write([]byte(flag.Key + ":" + userID))
	bucket := h.Sum32() % 100

	return bucket < uint32(flag.RolloutPercent)
}

Salting the hash with the flag key matters more than it looks like it should. Without it, a user who lands in the bottom 10% for one flag lands in the bottom 10% for every flag, which means your rollouts are correlated when they should be independent. With the key mixed in, each flag gets its own hash space, so being an early adopter of one feature says nothing about whether you’ll get another.

We load the whole table into memory on service startup and refresh it every thirty seconds with a background goroutine, so checking a flag is just a map lookup plus a hash, no database round trip on the request path. For a service handling a modest amount of traffic, that refresh interval is more than fast enough, flag changes just take up to thirty seconds to fully propagate, which has never mattered in practice.

I want to be upfront about what this setup does not give you. There is no UI, so toggling a flag means running a SQL update or, if you are slightly fancier, a tiny internal admin page. There is no audit log, so if someone asks who flipped a flag and when, the honest answer is “check the updated_at column and hope that’s enough.” There is no targeting by attributes like account plan or region, only the user ID hash. And there is no experiment framework bolted on, no automatic statistical significance testing if you are trying to measure the effect of a flag.

For us, none of that mattered enough to justify a vendor. But I’d draw the line somewhere. If you get past a dozen or so active flags, if non-engineers need to be able to toggle things without asking someone to run a query, or if you need targeting rules more complex than “some percentage of users,” that is the point where a real vendor starts paying for itself. The value in those tools isn’t the flag storage, it’s the UI, the audit trail, and the targeting engine built on top, and those get harder to justify hand-rolling as the number of flags and the number of people touching them grows.

The practical takeaway: don’t reach for a feature-flag vendor just because you need feature flags. If your actual requirement is “turn this on for everyone, or for a percentage of users, and let an engineer flip the switch,” a table with a boolean and a percentage column, paired with a hash-based lookup function, covers that completely for free. Save the vendor evaluation for the day your flag count or your non-engineer toggling needs actually outgrow it.