FeatureFlag
A tiny, cached gateway for turning Apex behavior on and off at runtime. It reads boolean toggles from Feature_Flag__mdt custom metadata, fails closed when a flag is missing, and lets tests override any flag in memory — no DML required.
Overview
FeatureFlag answers one question: “is this feature turned on right now?” You give it a flag name, it returns a Boolean. That single check lets you ship code that stays dormant until you flip a switch — useful for gradual rollouts, kill switches, or guarding risky behavior.
Flag values live in Feature_Flag__mdt custom metadata, so toggling a feature is a configuration change, not a code change. Deploy or edit a metadata record in the target org and you’re done. This is the classic Feature Toggle pattern: separate the decision to deploy code from the decision to release it.
Use FeatureFlag when you want a runtime on/off switch that admins or release engineers can control without redeploying Apex — gradual rollouts, kill switches, environment-specific behavior, or guarding work-in-progress code.
Reach for a custom setting or your own custom metadata type instead when you need more than a boolean. FeatureFlag has no percentage rollouts, no per-user or per-profile targeting, and no values beyond true/false. If you need configurable values (a threshold, a URL, a limit), model those directly rather than stretching a boolean toggle.
Quickstart
Wrap new or risky behavior in an isEnabled check. The feature stays off until a Feature_Flag__mdt record with that name exists and its Is_Enabled__c checkbox is checked.
if (FeatureFlag.isEnabled('bedrock.feature.x')) {
runNewBehavior();
} else {
runLegacyBehavior();
}
That is the entire production API. Nothing to instantiate, no configuration object to pass.
Examples
Guard a feature with a simple branch
The everyday use: wrap new or risky behavior so it only runs when the flag is on.
public void processOrder(Order__c order) {
if (FeatureFlag.isEnabled('bedrock.feature.newPricing')) {
applyNewPricing(order);
} else {
applyLegacyPricing(order);
}
}
Use a flag as a kill switch
Because a missing or disabled flag returns false, you can wrap an entire risky code path and turn it off instantly by unchecking Is_Enabled__c in the org — no deployment required.
if (FeatureFlag.isEnabled('bedrock.feature.externalSync')) {
callExternalSystem(); // disable in seconds if the integration misbehaves
}
Check the same flag repeatedly without query cost
Thanks to the per-transaction cache, checking a flag inside a loop is safe. Only the first check queries; the rest are served from memory.
for (Account account : accounts) {
if (FeatureFlag.isEnabled('bedrock.feature.enrichment')) { // 1 query total
enrich(account);
}
}
If you prefer, hoist the check above the loop into a local
Boolean. Both patterns cost one query — the cache makes them equivalent. A local variable does have one advantage: it documents that the value does not change mid-loop.
Where Flag Values Come From
Flag values come from a single source: the Feature_Flag__mdt custom metadata type in force-app/bedrock/lib/feature-flag/objects/. The class queries two fields:
| Field label | API name | Type | Role |
|---|---|---|---|
| Flag Key | Name__c | Text (255, required, unique) | The string you pass to isEnabled. The query matches on this field — not the record’s DeveloperName or Label. |
| Is Enabled | Is_Enabled__c | Checkbox (default false) | The on/off value returned by isEnabled. |
// The lookup the class performs (from FeatureFlag.cls)
List<Feature_Flag__mdt> flags = [
SELECT Name__c, Is_Enabled__c
FROM Feature_Flag__mdt
WHERE Name__c = :name
LIMIT 1
];
A few consequences worth internalizing:
- The match is on
Name__c, the custom “Flag Key” field. Whatever string you store inName__cis the exact string you must pass toisEnabled. The examples in the tests use a dotted convention likebedrock.feature.newCheckout, which is a readable way to namespace flags. - There is no hierarchy and no per-user or per-profile override. Custom metadata is org-wide configuration; unlike hierarchy custom settings, there is no user or profile layering here. A flag is the same value for everyone in the org.
Is_Enabled__cdefaults tofalse. A newly created flag record is off until someone checks the box.- To change a flag in a real org, deploy or edit the corresponding
Feature_Flag__mdtrecord. No Apex redeploy is needed to flip a feature.
Testing
Here is the problem FeatureFlag.set solves. Custom metadata records deployed to an org are visible to test methods, but you usually cannot rely on org data in a unit test — it makes tests environment-dependent. You also cannot insert new Feature_Flag__mdt records with DML the way you would a normal SObject. FeatureFlag.set(name, enabled) writes a synthetic value straight into the in-memory cache. The very next isEnabled(name) call returns whatever you set — no DML, no SOQL, no dependency on what is deployed.
Turn a flag on for a test
Even if no Feature_Flag__mdt record exists for the name, set(name, true) makes isEnabled return true.
FeatureFlag.set('bedrock.feature.newPricing', true);
Boolean enabled = FeatureFlag.isEnabled('bedrock.feature.newPricing');
Assert.areEqual(true, enabled, 'Expected set to enable a missing feature flag.');
Turn a flag off for a test
Pass false to explicitly disable a flag — useful for asserting the legacy or “flag off” branch of your code.
FeatureFlag.set('bedrock.feature.newPricing', false);
Boolean enabled = FeatureFlag.isEnabled('bedrock.feature.newPricing');
Assert.areEqual(false, enabled, 'Expected set to explicitly disable a flag.');
Test both branches of flag-guarded code
Seed the flag, exercise the unit, re-seed with the opposite value, and exercise again — all in memory.
@istest static void testProcessOrder_respectsFlagBranches() {
OrderProcessor processor = new OrderProcessor();
Order__c order = new Order__c();
FeatureFlag.set('bedrock.feature.newPricing', true);
processor.processOrder(order);
Assert.areEqual('new', order.Pricing_Path__c, 'Expected the new pricing path when the flag is enabled.');
FeatureFlag.set('bedrock.feature.newPricing', false);
processor.processOrder(order);
Assert.areEqual('legacy', order.Pricing_Path__c, 'Expected the legacy path when the flag is disabled.');
}
Pass null to revert to the disabled default
set(name, null) stores a cache entry whose Is_Enabled__c is null. Because isEnabled evaluates flag != null && flag.Is_Enabled__c, a null Is_Enabled__c reads as false. A null value behaves like “off,” not “unknown.” The entry still exists in the cache — it is not removed.
FeatureFlag.set('bedrock.feature.override', true);
FeatureFlag.set('bedrock.feature.override', null); // reverts to disabled
Boolean enabled = FeatureFlag.isEnabled('bedrock.feature.override');
Assert.areEqual(false, enabled, 'Expected a null override to restore the default false.');
Clear the cache to force a re-query
clearCache() is @TestVisible, so tests can call it to empty the entire cache. It discards both overrides set via set and any SOQL results cached during the test. The next isEnabled for any name then performs a fresh SOQL lookup.
FeatureFlag.set('bedrock.feature.cached', true);
Boolean overridden = FeatureFlag.isEnabled('bedrock.feature.cached'); // true (override)
FeatureFlag.clearCache();
Boolean restored = FeatureFlag.isEnabled('bedrock.feature.cached'); // false (re-queried, none found)
Assert.areEqual(true, overridden, 'Override was applied while cached.');
Assert.areEqual(false, restored, 'clearCache restores the default disabled state.');
How It Works
Three ideas explain everything FeatureFlag does.
1. It is a static gateway with one public read
There is nothing to instantiate. You call FeatureFlag.isEnabled(name) directly. Internally it looks up a Feature_Flag__mdt record whose Name__c matches the flag name and returns that record’s Is_Enabled__c value.
2. It fails closed
A flag is only “on” when a matching metadata record exists and its Is_Enabled__c checkbox is true. Anything else returns false:
- The flag name is blank (
null,'', or whitespace) →false. - No
Feature_Flag__mdtrecord matches the name →false. - A record matches but
Is_Enabled__cisfalse→false.
This behavior is deliberate and safe: if a flag is misspelled or its metadata was never deployed, the guarded feature stays off rather than silently turning on.
3. It caches per transaction
The class keeps a private static Map<String, Feature_Flag__mdt> named flagsByName. The first time you ask about a flag, it runs one SOQL query and stores the result — including a null entry when no record was found. Every later read of the same name returns the cached value with no further query.
static state in Apex lives for the duration of a single transaction, so the cache is naturally scoped to one execution context and starts empty in the next one.
Why caching a
nullmatters: when a flag has no metadata record,FeatureFlagstill writes anullentry for that name. A missing flag costs one query per transaction — not one per check. Check an undefined flag a thousand times in a loop and you still make a single SOQL call.
Public API
FeatureFlag is a public inherited sharing class. Two methods are public. Production code should call isEnabled; set is public so tests can seed the cache, but it is a test seam by convention rather than durable configuration.
A note on access modifiers: in Apex, a member with no access modifier is
private. Two methods in this class —get(String name)andclearCache()— have nopublicmodifier, so they are private.clearCacheis annotated@TestVisible, which exposes it to test classes only. It remains private to all other callers.
| Member | Signature | Returns | Description |
|---|---|---|---|
isEnabled | public static Boolean isEnabled(String name) | Boolean | The one method production code should call. Returns true only when a Feature_Flag__mdt record with Name__c = name exists and its Is_Enabled__c is true. Returns false for blank names and missing flags (fail closed). |
set | @TestVisible public static void set(String name, Boolean enabled) | void | Public cache override for tests. Writes a flag value directly into the in-memory cache, overriding or pre-seeding what isEnabled will return for name in the current transaction. See Testing. |
Public properties
There are none. FeatureFlag exposes no public properties or public fields. Its only state is the private static cache flagsByName, which you interact with exclusively through the methods above.
Private members (for context, not for calling)
| Member | Visibility | Role |
|---|---|---|
flagsByName | private static | The per-transaction cache of Name__c → Feature_Flag__mdt (or null). |
get(String name) | private static | Cache-aware lookup: returns the cached entry if present, otherwise runs the SOQL query and caches the result (including null when no record is found). |
clearCache() | private static, @TestVisible | Empties the entire cache. Reachable from test code only; lets a test discard all overrides and cached SOQL results so the next read re-queries. |
Notes & Edge Cases
-
No access modifier means private.
isEnabledandsetare public.getandclearCacheare private;clearCacheis reachable only from test code via@TestVisible. Treatsetas a test helper, not production configuration. -
Match on
Name__c, notDeveloperName. The query filters on the custom “Flag Key” field. The string inName__cmust exactly equal the string you pass toisEnabled— case, dots, and all. -
Fail closed is a feature, not a bug. A misspelled name, a flag whose metadata was never deployed, and a blank name all return
false. Guarded features stay off until a real, enabled record exists. If a flag refuses to turn on, spelling is the first thing to check. -
The cache is per transaction. Values are stored in static state, which lives for one Apex transaction. A
Feature_Flag__mdtrecord changed mid-transaction — or deployed at that exact moment — is not visible until the next transaction starts. In tests, useclearCache()to force a fresh read. -
set(name, null)reads as disabled, not absent. It stores aFeature_Flag__mdtwithIs_Enabled__c = nullin the cache. The entry is not removed, so subsequent calls do not re-query.isEnabledevaluatesflag != null && flag.Is_Enabled__c, so a nullIs_Enabled__creturnsfalse. -
setonly affects the current transaction’s memory. It does not insert metadata and nothing persists. It is a test seam for the in-memory cache — do not call it from production code expecting durable configuration. -
Seed flags in tests; do not rely on org data. Use
FeatureFlag.setto put the flag into a known state so your test is deterministic regardless of whichFeature_Flag__mdtrecords happen to be deployed in the running org. -
It is a boolean toggle only. No percentages, no per-user or per-profile targeting, no non-boolean values. If you outgrow on/off, model the need with a custom setting or a richer custom metadata type rather than stretching
FeatureFlag.