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.
- 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
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.
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.
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.
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.
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
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).
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.
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.
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
_conly to custom objects; use the__csuffix 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:
- Does this object represent a distinct business entity, or should it be fields on an existing object?
- What is the relationship to its parent — can it exist independently?
- What is the estimated maximum record volume in 3 years?
- What will users query, report on, and filter?
- Are there security implications for how the relationship type is chosen?
- 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
How did this article make you feel?
Comments
Salesforce Tip