Choosing Between Custom Metadata Types and Custom Settings for Runtime Configuration in Salesforce
Deciding between Custom Metadata Types and Custom Settings in Salesforce can be tricky. This guide compares the two options, highlights trade‑offs, and provides concrete implementation examples for feature flags, runtime configs, and packaging scenarios.
11 Jun 2026, 14:20 UTC

Problem Statement
When building a Salesforce org that needs runtime configuration—such as feature flags, environment‑specific endpoints, or user‑level preferences—you must decide whether to store that data in a Custom Metadata Type (CMT) or a Custom Setting. Both offer metadata‑driven configuration, but they differ in deployment, query behavior, packaging, and runtime capabilities. This guide helps you pick the right option for your use case.
Decision Guide
Use the following table to compare the core differences. The decision comes down to three constraints: deployment flow, runtime access pattern, and packaging needs.
| Feature | Custom Metadata Type | Custom Setting (List) | Custom Setting (Hierarchy) |
|---|---|---|---|
| Deployment | Deploys as metadata via Change Sets, Packages, Metadata API. Records migrate with the metadata. | Data only; records must be migrated manually or via post‑deploy scripts. | Same as List. |
| Runtime DML | No DML at runtime (read‑only). getInstance() or SOQL only. | Full DML support; can create/update/delete in Apex. | Full DML support; can override per profile/user. |
| Query Cost | Counts against SOQL limit (100 per transaction). Cached after first access. | No SOQL cost; getAll() or getValues() are free. | No SOQL cost; getInstance(profileId) is free. |
| Field Types | Text, Number, Checkbox, Date, DateTime, Picklist, Long Text Area, URL, Email, Phone, etc. | Text, Number, Checkbox, Date, DateTime, Email, Phone. No Picklist or Long Text Area. | Same as List. |
| Relationships | Supports lookup to other CMTs and standard objects via EntityDefinition. | No relationships. | No relationships. |
| Packaging | Can be protected or public. Protected records are hidden from subscribers. | Can be protected; subscriber records are always editable unless protected. | Same as List. |
| Hierarchy Overrides | Not applicable. | None. | Supports profile/user overrides. |
| Use in Formulas/Validation Rules/Flows | Fully supported. | Only in Apex via getAll() or getValues(). | Only in Apex. |
Trade‑Off Analysis
- Feature Flags & Picklist‑Driven Logic – CMT is ideal because it supports picklists, relationships, and can be referenced in formulas, validation rules, and flows. It also ensures that changes propagate with metadata deployments.
- Runtime‑Toggleable Limits – List Custom Settings are preferable. They avoid SOQL limits, allow DML for dynamic updates, and are cheap to read in bulk via
getAll(). - User‑Specific Preferences – Hierarchy Custom Settings provide per‑profile or per‑user overrides without additional code.
- Packaging Constraints – If you need to ship configuration that cannot be edited by subscribers, use a protected CMT. For settings that subscribers should be able to adjust, use a public CMT or List Custom Setting.
- Large Datasets – Avoid storing >10k rows in List Custom Settings because
getAll()returns all records and can hit heap limits. - Test Isolation – CMT records are not visible in test methods unless you use
SeeAllData=trueor load data viaTest.loadData(). List Custom Settings can be created in test context but may affect governor limits.
Concrete Implementation Examples
Example 1: Feature Flag with Custom Metadata Type
// Create a CMT called Feature_Flag__mdt with fields:
// Name (Text) – API name
// Is_Enabled__c (Checkbox)
// Description__c (Long Text Area)
// Deploy via Metadata API or Change Set.
Access in Apex (cached per transaction):
public with sharing class FeatureFlagUtil {
private static Map flagCache;
public static Boolean isEnabled(String flagName) {
if (flagCache == null) {
flagCache = new Map();
for (Feature_Flag__mdt f : [SELECT Name, Is_Enabled__c FROM Feature_Flag__mdt]) {
flagCache.put(f.Name, f);
}
}
Feature_Flag__mdt f = flagCache.get(flagName);
return f != null && f.Is_Enabled__c;
}
}
Validation Rule example referencing the flag (if the flag is true, a field must be populated):
AND(ISPICKVAL(Feature_Flag__mdt.Is_Enabled__c, true), ISBLANK(Text_Field__c))
Unit test using Test.loadData() to avoid SeeAllData=true:
@IsTest
private class FeatureFlagUtilTest {
@IsTest static void testIsEnabled() {
Test.loadData(Feature_Flag__mdt.SObjectType, new List<String>{
'Feature_Flag__mdt',
'Name,Is_Enabled__c,Description__c',
'NewFeature,true,Description'
});
System.assertEquals(true, FeatureFlagUtil.isEnabled('NewFeature'));
}
}
Example 2: Runtime Configuration with List Custom Setting
// Create a List Custom Setting called Runtime_Config__c with fields:
// Name (Text)
// Value__c (Text)
// Is_Active__c (Checkbox)
Access in Apex without SOQL:
public with sharing class RuntimeConfig {
private static Map cache;
public static String getValue(String key) {
if (cache == null) {
cache = Runtime_Config__c.getAll();
}
Runtime_Config__c cfg = cache.get(key);
return (cfg != null && cfg.Is_Active__c) ? cfg.Value__c : null;
}
}
Example usage in a trigger (bulkified, single cache load):
trigger AccountTrigger on Account (before insert) {
for (Account a : Trigger.new) {
String endpoint = RuntimeConfig.getValue('External_API_Endpoint');
if (endpoint != null) {
a.External_Endpoint__c = endpoint;
}
}
}
Unit test creating records in test context:
@IsTest
private class RuntimeConfigTest {
@IsTest static void testGetValue() {
Runtime_Config__c cfg = new Runtime_Config__c(Name='External_API_Endpoint', Value__c='https://api.example.com', Is_Active__c=true);
insert cfg;
System.assertEquals('https://api.example.com', RuntimeConfig.getValue('External_API_Endpoint'));
}
}
Validation & Verification Checklist
- Deploy the chosen configuration object via Change Set or Metadata API and confirm records appear in Setup.
- Run an Apex script that counts queries:
System.debug(Limits.getQueries());. Verify that CMT access consumes a query, while List Custom Setting access does not. - In a managed package, mark a CMT as protected and attempt a SOQL query from a subscriber org; you should receive an insufficient privilege error.
- Use Flow Builder to add a Decision element that references a CMT picklist field; ensure it appears in the picklist editor.
- For List Custom Settings, insert >10k records in a sandbox and run
Runtime_Config__c.getAll()in a test method; monitor CPU and heap limits to confirm thresholds.
Limitations & Caveats
- CMT records are immutable at runtime; any change requires a deployment or Setup UI edit.
- List Custom Settings cannot store picklists or long text; use CMT if you need those types.
- Hierarchy Custom Settings do not respect Permission Sets; use a CMT with a custom permission for finer control.
- When deploying a package that includes a protected CMT, subscribers cannot query it via SOQL; expose values through @AuraEnabled methods if needed.
- Always bulkify CMT access in triggers or batch jobs; a single query per transaction is recommended to stay within the 100 query limit.
By applying this decision guide, you can confidently choose between Custom Metadata Types and Custom Settings, ensuring that your configuration data aligns with deployment workflows, runtime performance, and packaging requirements.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.