ESC

AI-powered search across all blog posts and tools

Architecture · December 12, 2025

10 Salesforce Data Model Anti-Patterns to Avoid

The data model mistakes that create technical debt, performance problems, and unhappy users — and how to fix them.

☕ 9 min read 📅 December 12, 2025
  • Choosing lookup over master-detail when ownership and security should cascade is the most common and costly mistake
  • Polymorphic lookups to multiple object types create query complexity that compounds as the org grows
  • Schema decisions made early are expensive to change — design for scale before writing the first record

I’ve inherited a lot of Salesforce orgs. Some were pristine — thoughtful schema, clean naming, obvious relationships. Most were not. After enough org audits, certain mistakes appear over and over: the same anti-patterns, implemented with the best intentions, creating years of technical debt.

Here are the ten data model anti-patterns I encounter most often, why they happen, and what to do instead.


Anti-Pattern 1: Using Lookup When Master-Detail Is Correct

This is the single most common mistake I see. A developer creates a child object with a Lookup to its parent instead of a Master-Detail relationship, usually because “it feels less strict.”

The cost of this decision:

  • No roll-up summary fields (you can’t roll up from Lookup children)
  • No automatic deletion of orphaned children
  • Security and ownership don’t cascade from parent
  • Reports treat the relationship as outer join by default, hiding data

Use Master-Detail When

  • The child record has no meaning without its parent (line item without an order, milestone without a project)
  • You need roll-up summary fields on the parent
  • Security and ownership should cascade from parent to child
  • Orphaned children should be automatically deleted

Use Lookup When

  • The child can exist independently
  • You need a many-to-many relationship (via a junction object with two lookups)
  • You’re relating to a standard object that doesn’t support being a Master-Detail parent

Anti-Pattern 2: Over-Objectification (Too Many Custom Objects)

Every new business concept becomes a custom object. Six months in, the org has 40 custom objects, half of which hold under 1,000 records, and users spend more time navigating between tabs than doing actual work.

⚠️ Signs of Over-Objectification
  • Objects with only 3-4 fields that could have been a related list
  • Objects used purely as configuration tables (replace with Custom Metadata Types)
  • Objects that mirror fields already on a standard object with minor variations

Fix: before creating a new object, ask whether the information could be represented as:

  • Additional fields on an existing object
  • Custom Metadata Types (for configuration data)
  • A related list on an existing object

Good vs. Bad Entity Relationships

Data Model Comparison: Anti-Pattern vs. Better Design
Anti-PatternBetter DesignContract__c (flat, 60+ fields)Customer_Name__c (text) — should be LookupCustomer_Email__c (text) — duplicatedProduct_Name__c (text) — should be LookupProduct_Price__c (currency) — stale copyApprover_1__c, Approver_2__c, Approver_3__cLine_Item_1_Qty__c … Line_Item_10_Qty__cProblem: duplicate data, no normalization, unmaintainableContact__c has5 Product lookupsProduct__c hasmultiple Contact fieldsAnti-pattern: repeated lookup fieldsAccountContract__cContract_Line__cProduct2M-DM-DLookupContact_Product__cJunction objectwith two M-DClean many-to-manyforcenaut.com — Data Model Comparison

Anti-Pattern 3: Storing Repeated Lookup Fields Instead of Junction Objects

Contact__c.Product_1__c, Contact__c.Product_2__c, Contact__c.Product_3__c. I see this constantly. When a Contact needs to be associated with multiple Products, someone adds three lookup fields instead of a junction object.

The limit is always wrong. Three becomes five, five becomes ten. Querying for “all Contacts associated with Product X” requires checking ten fields. Roll-ups are impossible.

💡 Pro Tip

Fix: create Contact_Product__c as a junction object with Master-Detail lookups to both Contact and Product. Clean, scalable, reportable.


Anti-Pattern 4: Polymorphic Lookups Without Justification

Polymorphic lookups (where a field can point to one of several object types — like the WhatId on Activity, which can reference Account, Opportunity, Case, or any custom object) are powerful but complex.

When I see custom objects with polymorphic lookups, it’s often implemented without understanding the query implications:

Problematic Query

// You cannot filter on polymorphic fields across types in one query
[SELECT Id, What.Name FROM Task WHERE What.Type = 'Opportunity' AND What.StageName = 'Closed Won']

Correct TYPEOF Query

[SELECT Id,
    TYPEOF What
        WHEN Opportunity THEN StageName, Amount
        WHEN Case THEN Subject, Status
    END
FROM Task
WHERE WhatId != null]

Unless you have a genuine use case for one field pointing to multiple object types, use a standard typed lookup. The query simplicity is worth it.


Anti-Pattern 5: Not Using Record Types for Multi-Process Objects

A single Request__c object handles IT requests, HR requests, and Facilities requests. Each type has completely different fields, page layouts, and validation rules — but they’re all on the same object with no Record Type differentiation.

⚠️ Warning

The result: 60 fields on one object, 90% of which are blank for any given record. Users see irrelevant fields. Reports are confusing. Validation rules require complex OR conditions to exclude the wrong record types.

Fix: implement Record Types from the start for any object that supports multiple distinct processes. Separate picklist values, separate page layouts, separate process automation via entry criteria.


Anti-Pattern 6: Formula Fields That Reference Formula Fields That Reference Formula Fields

Three levels of formula field chaining causes problems. Salesforce limits formula field complexity by “formula size” (measured in characters when compiled), and chained formulas burn through this limit quickly.

Why formula chaining is dangerous

More critically: deeply chained formula fields can cause cross-object formula limits to be hit unexpectedly. Salesforce limits cross-object references to 10 unique objects per formula, and chaining can obscure how many you’re actually referencing.

Fix: denormalize selectively. If a formula is chaining through three objects to get a value, consider a Flow that copies that value into a regular field when the source changes.


The Problem

Scenario: An Account with 8,000 Opportunities used a Roll-Up Summary field to count open Opportunities. Every time a rep updated an Opportunity stage — a routine daily action — users experienced a 10-15 second save delay. The Account record was locking during the roll-up recalculation.

The Solution

Remove the Roll-Up Summary field. Replace it with a Batch Apex job that runs every 30 minutes, queries COUNT() from Opportunities per Account, and writes the result to a regular Number field. Save times returned to under 1 second. The number displayed is at most 30 minutes stale — acceptable for this use case.

Anti-Pattern 7: Roll-Up Summary Fields on Large Data Volumes

Roll-up summary fields (RSF) recalculate synchronously when any child record changes. On an Account with 10,000 Opportunities, updating a single Opportunity triggers a recalculation that aggregates all 10,000 rows.

At scale, this causes:

  • Record save timeouts for end users
  • Batch job slowdowns
  • Lock contention when multiple records update simultaneously
💡 Pro Tip

Fix: for high-volume relationships, replace roll-up summary fields with a Batch Apex job that recalculates the aggregate nightly (or on a schedule) and stores the result in a regular currency/number field. Users get the aggregate; you avoid the real-time recalculation penalty.


Anti-Pattern 8: External IDs Only on Fields That Aren’t Truly Unique

External_ID__c is a powerful field type — it enables upsert operations and creates an index. But I regularly see External ID fields on data that isn’t truly unique (e.g., a legacy system ID that can be reused after deletion).

🚨 Important

Non-unique External IDs cause upsert failures that are hard to debug and can result in duplicate records. If the field isn’t guaranteed unique in the source system, don’t mark it as an External ID.


Anti-Pattern 9: Ignoring Object Relationships for Security

Salesforce’s record sharing model is deeply tied to the data model. A Master-Detail relationship inherits the parent’s sharing rules. A Lookup relationship does not.

⚠️ Warning

I’ve seen orgs where a custom Order_Item__c object is related to Order__c via Lookup (instead of Master-Detail) because “it seemed simpler at the time.” This means sharing rules that control Order access don’t flow down to the order items — users can see order items for Orders they shouldn’t have access to.

Always consider the security implications of your relationship type. Ask: should a user who can’t see the parent be able to see the child?


Anti-Pattern 10: Naming Objects and Fields for Today, Not for Scale

Discount_Approval_Process_2023__c. Temp_Flag__c. Old_Product_Code__c. These names accumulate. Two years later, nobody knows which approval process is current, whether the temp flag is still used, or what the old product code was for.

💡 Treat API Names as Permanent — Because They Are

You can rename a field’s label at any time. You cannot rename its API name without breaking every Apex class, Flow variable, integration mapping, and report that references it. Before clicking Save on a new field, pause on the API name. Strip out year references, avoid “Temp” or “Old” prefixes, and write a name that a developer unfamiliar with the org can understand without opening documentation. Five seconds of thought on an API name can save five days of refactoring later.

Field and object naming rules I follow:

  • No year references in API names (use Discount_Approval__c, versioned elsewhere)
  • No Temp_, Old_, New_, Test_ prefixes — if it’s temporary, delete it when done
  • Append _c only to custom objects; use the __c suffix consistently
  • Descriptive names that a developer unfamiliar with the org can understand without documentation

A Framework for Schema Decisions

Before creating any new object or field, I run through this checklist:

  1. Does this object represent a distinct business entity, or should it be fields on an existing object?
  2. What is the relationship to its parent — can it exist independently?
  3. What is the estimated maximum record volume in 3 years?
  4. What will users query, report on, and filter?
  5. Are there security implications for how the relationship type is chosen?
  6. What will be the rollup or aggregate needs?

Five minutes of schema planning prevents five months of refactoring.

What’s the worst data model anti-pattern you’ve encountered in a Salesforce org, and how long did it take to unwind? I’m especially curious about junction object refactors — those tend to be the most disruptive. Share your story in the comments.


Test Your Knowledge

Why is a Lookup relationship a problem when the child record has no meaning without its parent?
What is the correct way to model a many-to-many relationship between Contact and Product?

How did this article make you feel?

Comments

Salesforce Tip

🎉

You finished this article!

What to read next

Contents