Stop Hardcoding Thresholds: Custom Metadata Types as Apex Configuration
Hardcoded constants and Custom Settings both fail as Apex configuration. Custom Metadata Types deploy like code, query for free against governor limits, and let admins change behavior without a release.
13 Jul 2026, 01:54 UTC

Every Apex codebase eventually grows a class full of constants: a discount cap here, an integration endpoint there, a batch size somebody tuned two years ago. Then the business asks to change the discount cap, and suddenly a one-line tweak becomes a code review, a deployment, and a test run. Custom Metadata Types (CMDT) exist to break that cycle. They let you store application configuration as metadata — deployable, queryable from Apex without burning governor limits, and editable by admins without touching code.
The problem with the usual alternatives
Before CMDT, the two common options were hardcoded values and Custom Settings. Hardcoding is fast until it isn't: every change is a code change. Custom Settings are editable in the UI, but their records are data. That means when you deploy a new environment, your configuration doesn't come along — someone has to recreate records by hand or load them with a data tool. I've seen more than one sandbox refresh quietly wipe out the settings an integration depended on.
Custom Metadata Types sit in a useful middle ground. The records are metadata, so they travel through change sets, unlocked packages, and the Metadata API exactly like your Apex classes do. Spin up a scratch org or refresh a sandbox, deploy, and your configuration is already there.
Why Apex developers specifically should care
Two runtime behaviors make CMDT attractive in code, not just in deployment pipelines:
- SOQL queries against CMDT don't count against the per-transaction query limit. You can read configuration inside a loop or a trigger without the usual anxiety about the 100-query synchronous governor limit.
- Records are cached at the application level. Repeated reads are cheap compared with querying regular SObjects.
CMDT also supports custom fields and relationships between metadata types, so you can model layered configuration — say, a parent "Integration" record with child "Endpoint" records — rather than flattening everything into key-value strings.
A worked example: configurable discount rules
Suppose sales ops wants tiered discount caps that change quarterly. Create a Custom Metadata Type called Discount_Tier__mdt with fields Tier_Name__c (Text) and Max_Discount_Percent__c (Number). Add records: Gold → 30, Silver → 20, Bronze → 10.
In Apex, reading the configuration looks like any other query:
public class DiscountService {
public static Decimal getMaxDiscount(String tier) {
List<Discount_Tier__mdt> rows = [
SELECT Max_Discount_Percent__c
FROM Discount_Tier__mdt
WHERE Tier_Name__c = :tier
LIMIT 1
];
if (rows.isEmpty()) {
return 0; // fail closed: unknown tier gets no discount
}
return rows[0].Max_Discount_Percent__c;
}
}Run this in any trigger, service class, or anonymous Apex window — no special permissions beyond read access to the type. The query consumes zero of your SOQL governor limit allotment. When sales ops wants Gold raised to 35, an admin edits the record in Setup (or you deploy the updated record) and no code ships.
To verify the governor-limit behavior yourself, open the Developer Console in a Developer Edition org, run a loop of 150 queries against your CMDT in anonymous Apex, and check the debug log's limit usage section — the SOQL count should stay flat. Repeat against a standard object like Account and you'll hit the limit. Don't take my word for it; this is a two-minute experiment.
The trade-offs you need to design around
CMDT is not a general-purpose settings store, and the limitations shape how you use it:
- Records are read-only from Apex. DML on CMDT records fails at runtime. Updates happen through the UI, the Metadata API, or the
Metadata.Operationsclass (which deploys asynchronously and has its own quirks). If users need to edit values interactively from a Lightning page in real time, CMDT is the wrong tool — that's what Custom Settings or a custom object are for. - Changes are deployments. Editing a record in production is effectively a metadata change. In orgs with locked-down production change windows, "just update the config" still means following your release process.
- Heap limits still apply. Free SOQL doesn't mean free memory. Querying thousands of CMDT rows into a list consumes heap like any other query, so select only the fields you need and filter aggressively.
- No triggers or validation rules with full Apex logic. If a config value is wrong, nothing stops it at save time beyond field-level validation. Consider a defensive default in code, like the fail-closed
return 0above.
Where to start
Audit your codebase for the smell: static final constants that business users ask about, or Custom Settings records that must be manually recreated after every sandbox refresh. Those are your migration candidates. Move one — something low-risk like a feature flag list or a threshold table — into a Custom Metadata Type, deploy it through your normal pipeline, and confirm the records arrive in the target org without a data load. Once you've seen configuration travel with your metadata instead of trailing behind it, the hardcoded constants start looking like the liability they are.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.