SfSkills Domains Docs GitHub

Home / apex / apex-custom-settings-hierarchy

apex-custom-settings-hierarchy

Use when reading or writing Hierarchy Custom Settings from Apex to resolve per-user/per-profile/org configuration. Covers `getInstance()` resolution order, DML cost, cache semantics, and when to prefer Custom Metadata Types instead. NOT for Custom Metadata Types in Apex — use apex/custom-metadata-in-apex. NOT for the CMT vs Custom Settings choice — use admin/custom-metadata-types-and-settings.

Open the skill folder on GitHub


Apex Custom Settings Hierarchy

Activates when Apex reads or writes Hierarchy Custom Settings — the cached, per-user/per-profile/per-org configuration store. Produces correct resolution, null-safe defaults, and guidance on when to pick Custom Metadata Types instead.


Before Starting


Core Concepts

The Three Accessors Resolve Differently

Hierarchy Custom Settings have three distinct Apex accessors:

When the running user has no User-level override, Salesforce falls back to their Profile-level override, and if none exists, to the org default. An empty record is returned, not null, when no tier is configured — field values will be null but the record itself exists. This surprises practitioners who null-check the record.

It's Cached, But Not Free

Reads are cached within a transaction, so repeated getInstance() calls in the same execution are cheap. But the cache is per-transaction — a long-running batch that calls getInstance() in every execute() re-reads. DML on the setting counts against governor DML limits; mass-upserting one record per user is the wrong shape.

Custom Metadata Is Often The Better Fit

Hierarchy Custom Settings predate Custom Metadata Types (CMDTs). CMDTs are deployable (packageable, source-tracked, sandbox-migration-friendly) while Custom Settings values are data, not metadata — they don't move with deploys. If your "config" is actually static code-adjacent decisions (feature toggle per environment, integration endpoint URL, retry caps), CMDT is the right home. Reserve Custom Settings for values admins must change in production Setup UI.


Common Patterns

Pattern 1: Null-Safe Read With Org Default Fallback

When to use: Any feature-flag or threshold lookup in Apex.

How it works:

public with sharing class ApiRetryConfig {
    public static Integer maxRetries() {
        RetrySettings__c s = RetrySettings__c.getInstance();
        // s is never null for Hierarchy Settings, but field values can be null.
        return (s != null && s.MaxRetries__c != null)
            ? s.MaxRetries__c.intValue()
            : 3;
    }
}

Why not the alternative: getOrgDefaults() misses per-user overrides. getInstance() with a nested null-check is the universal safe shape.

Pattern 2: Bulk-Safe Upsert Of Per-User Overrides

When to use: A one-time job seeds per-user settings during onboarding.

How it works:

List<PerUserFlag__c> rows = new List<PerUserFlag__c>();
for (Id userId : userIds) {
    rows.add(new PerUserFlag__c(SetupOwnerId = userId, Enabled__c = true));
}
upsert rows SetupOwnerId;

Why not the alternative: insert row-by-row burns DML statements. Batch upsert by the SetupOwnerId external-ID-like key inserts new and updates existing in one DML.


Decision Guidance

SituationRecommended ApproachReason
Static config that changes at deploy onlyCustom Metadata TypePackageable, deploys with source, no DML
Admin-editable per-user or per-profile configHierarchy Custom SettingOnly CS offers hierarchy fallback
Feature flag changed during an incidentHierarchy Custom Setting at Org tierAdmins can flip in Setup without deploy
Lookup table (e.g., country → currency)Custom Metadata Type with custom fieldsRelationships and SOQL; better than List CS
Per-transaction cache of computed valuesPlatform Cache (Cache.Org / Cache.Session)Custom Settings are not a computation cache

  1. Classify the data: runtime-mutable admin config, deploy-time code config, or derived/cached compute? Only the first belongs in Hierarchy Custom Settings.
  2. Confirm the object's Privileged flag matches the security posture — set it if end users should not write.
  3. Read through getInstance() for per-user fallback; document why if you pick getOrgDefaults() explicitly.
  4. Null-check the field (not the record), with a sane code default for missing fields.
  5. For writes, batch by SetupOwnerId and use upsert ... SetupOwnerId; never loop-inserting.
  6. Write a test with System.runAs(user) to confirm the hierarchy resolves as expected.
  7. Document in the setting's Description field who edits this in production and when.

Review Checklist


Salesforce-Specific Gotchas

See references/gotchas.md for the full list.

  1. getInstance() never returns null for Hierarchy Settings — it returns an empty record with null fields. Null-check fields, not the record.
  2. getOrgDefaults() CAN return null if no org-default row exists. Different semantics than getInstance().
  3. CS values do not deploy with metadata — seeding production after a sandbox deploy is manual or requires a data loader.
  4. Privileged checkbox on the object controls whether non-admins can write. Without it, a user trigger calling insert newCSRecord can fail for low-privilege users.
  5. SetupOwnerId is polymorphic — it accepts User or Profile IDs. The wrong type inserts as the wrong hierarchy tier silently.

Output Artifacts

ArtifactDescription
references/examples.mdRealistic read/write patterns and a List-CS to CMDT migration
references/gotchas.mdHierarchy resolution surprises and packaging warnings
references/llm-anti-patterns.mdCommon LLM mistakes: null-checking the record, getOrgDefaults drift
references/well-architected.mdOpEx / Reliability framing and CMDT vs CS tradeoffs
scripts/check_apex_custom_settings_hierarchy.pyStdlib lint for anti-pattern usage

Package files