Uncategorized

How to Build a Feature Flag System That Doesn’t Become a Liability

Feature flags sound simple until you have 200 of them, three engineers who disagree on what's live, and a production incident caused by a flag nobody remembers creating.

Feature flags are one of those things that feel like a quick win and turn into a quiet disaster. You add one to ship a half-finished payment flow to beta users. Then another to hide the new dashboard from enterprise accounts while you fix a permissions bug. Six months later you have 40 flags, no naming convention, a shared boolean in your config that two engineers interpret differently, and a junior dev who turned something on in production on a Friday afternoon because they thought it was a test flag. We’ve seen this exact sequence more times than we’d like.

The point isn’t that feature flags are bad. They’re genuinely useful – probably the best tool you have for decoupling deployment from release, running controlled rollouts, and giving your product team the ability to move without waiting on engineering. The point is that a flag system needs to be treated as infrastructure from the start, not as a one-off config hack that you’ll “clean up later”. Later never comes.

What a Feature Flag System Is Actually Doing

At its core, a feature flag is a conditional in your code that points to an external decision. Instead of if (newCheckout) hardcoded in your logic, you’re asking a service: “should this user, in this context, see this behaviour?” That sounds trivial. It isn’t.

The moment the decision lives outside your code, you’ve introduced a runtime dependency. Your application behaviour is now determined by state that isn’t in your codebase, isn’t version-controlled alongside your deploy, and can be changed by someone who isn’t an engineer. That’s the whole point – and it’s also the source of every flag-related incident we’ve ever seen.

The architecture question isn’t “should we use flags?” It’s “where does the flag state live, who can change it, how does your app fetch it, and what happens when that fetch fails?”

Build vs Buy: The Honest Version

The three realistic options are a managed service like LaunchDarkly or Statsig, an open-source self-hosted tool like Unleash or Flagsmith, or rolling your own on top of your existing database. Each has a genuine case.

Managed services cost real money – LaunchDarkly starts around A$150-300/month for small teams and scales steeply. What you’re paying for is the SDK ecosystem, the evaluation latency (sub-millisecond via local caching), the audit log, the built-in targeting rules, and the A/B testing layer if you need it. If you’re running a SaaS with enterprise customers who ask about your feature management during security reviews, managed is usually worth it. The audit trail alone has closed deals for clients we’ve worked with.

Self-hosted Unleash or Flagsmith makes sense when you have compliance requirements that prevent sending evaluation data to a third party, or when the managed cost is genuinely prohibitive at your stage. Expect to spend a few days on the initial setup and ongoing maintenance. It’s not “free” – you’re trading cash for time.

Rolling your own is almost never the right call unless your requirements are genuinely minimal: a handful of flags, no targeting rules, no percentage rollouts, no analytics. A basic flags table in Postgres with a Redis cache in front of it takes half a day to build. It also takes half a day to outgrow. If you think you’ll want user-segment targeting in three months, skip this and use a real tool.

The Targeting Model You Need to Think Through Early

Simple on/off flags are fine until you need to roll something out to 10% of users, or enable a feature only for accounts on an enterprise plan, or show a new flow to users in NSW but not QLD because of a regulatory difference. Once you need any of that, you need a targeting model – and the shape of that model needs to match your data.

The most common mistake we see is flags that target on user ID when they should target on account ID. In a multi-tenant SaaS this is the difference between one person on an account seeing the new feature while their colleague on the same account doesn’t. That’s not a controlled rollout, that’s a support ticket waiting to happen.

Before you configure your first real flag, answer these questions:

  • Is the primary targeting unit a user, an account/organisation, or a session?
  • What attributes do you need to target on – plan tier, account age, geography, cohort?
  • Does your flag evaluation happen server-side (secure, slower to personalise) or client-side (faster, but attributes are exposed to the browser)?
  • Who in your team needs to be able to change flags – just engineers, or product managers too?
  • What’s your fallback behaviour when the flag service is unreachable?

That last one matters more than people think. A flag SDK that fails open (defaults to true) and a flag SDK that fails closed (defaults to false) behave very differently in an outage. You want explicit, documented defaults – not whatever the library does by accident.

Naming, Lifecycle, and the Graveyard Problem

The single biggest operational problem with flag systems isn’t the architecture, it’s the accumulation of dead flags that nobody is willing to delete. A fintech we worked with inherited a codebase with over 180 flags. About 30 were active. The rest were either permanently enabled (meaning the old code path was unreachable and could be deleted) or permanently disabled (meaning the new code was never shipped and was sitting there rotting). Nobody knew which was which without tracing through six months of git history.

A naming convention won’t fix this on its own, but it helps. We use a prefix system:

  • release_ – controls whether a feature is visible. Temporary by definition. Has an expected deletion date.
  • ops_ – controls operational behaviour like rate limits or cache TTLs. Often permanent.
  • experiment_ – tied to an A/B test. Deleted when the test concludes.
  • kill_ – a circuit breaker to disable a specific integration or feature under load. Permanent infrastructure.

The lifecycle rule is simple: every release_ flag gets a ticket in your backlog for removal when the rollout is complete. Not “someday”. A real ticket, assigned to someone, with a due date. Flags that have been 100% enabled for more than 30 days are dead code waiting to confuse the next engineer. Clean them up.

Performance: Where Flags Actually Cause Production Problems

Calling a flag evaluation service synchronously on every request is a straightforward way to add 50-200ms of latency and a new single point of failure to your application. Don’t do it.

Every serious flag SDK solves this with a local cache – the SDK polls or streams flag state from the server and stores it in memory, so evaluations are local. This means your flag evaluations are effectively free from a latency perspective, but it also means your flags are eventually consistent. A flag change might take 30-60 seconds to propagate to all your running instances. For most use cases that’s fine. For a kill switch you’re activating during an incident, you want to understand that delay exists.

If you’re building on a serverless architecture – Lambda, Vercel Edge Functions, that kind of thing – the local cache model breaks because you don’t have a persistent process. You’re either making a network call per invocation (expensive) or caching at the edge with a TTL (acceptable, but understand your consistency guarantees). Statsig and LaunchDarkly both have edge-compatible SDKs; the configuration is non-trivial and worth testing under realistic load before you go live.

When Your Flag System Needs to Talk to Your Analytics

If you’re using flags for anything more than simple on/off releases – percentage rollouts, experiments, plan-gated features – you need flag exposure events flowing into your analytics stack. Without them, you can’t answer “did the users who saw the new checkout actually convert better?” You just have a guess.

The architecture for this is straightforward but easy to skip: every time a flag is evaluated and returns a non-default value, emit an event (user_id, flag_key, variant, timestamp) to your event pipeline. That event needs to be queryable alongside your conversion events. Most managed flag tools have native integrations with Amplitude, Mixpanel, or Segment. If you’re self-hosting, you’re building this yourself – factor that into the build vs buy decision.

One thing worth being explicit about: flag exposure events can be high volume. On a SaaS with 10,000 active users and 20 active flags, you might be emitting 200,000 events per day just from flag evaluations. Make sure your analytics pipeline can handle that without inflating your event costs or polluting your session data.

If you’re building a SaaS and you’re trying to work out whether to invest in proper flag infrastructure before your next release cycle, talk to Amora about your build – it’s usually a one-day architectural decision that saves weeks of production pain later.

The teams that do this well treat their flag system the same way they treat their database schema: something you design with care, migrate deliberately, and never let accumulate entropy unchecked. The teams that do it badly treat it as a config file. You can usually tell which is which within about ten minutes of looking at the codebase.

Got something you want built?

Amora Digital is an Australian software and AI agency. We scope it, build it, and ship it – live in 28 days. No offshore teams. No surprises.

Book a discovery call

Ready to stop guessing and start growing?

Book a 30-minute strategy call. No pitch, no pressure — just a clear read on what's working, what isn't, and where the lift is.

Book your strategy call