SfSkills Domains Docs GitHub

Home / data / data-model-design-patterns

data-model-design-patterns

Use when designing, reviewing, or troubleshooting Salesforce object relationships and field type choices — lookup vs master-detail, junction object modeling, indexing strategy, and data model anti-patterns. NOT for drawing or diagramming the model — use architect/salesforce-erd-and-diagramming. NOT for the Setup steps to create the object itself — use admin/object-creation-and-design. NOT for hierarchical or external relationship types, polymorphic lookups, or the per-object relationship limits — use admin/lookup-and-relationship-design.

Open the skill folder on GitHub


Data Model Design Patterns

This skill activates when a practitioner needs to choose or review Salesforce object relationship types, field types, or indexing strategy. It covers the full decision surface from green-field modeling through production performance troubleshooting.


Before Starting

Gather this context before working on anything in this domain:

ContextWhat to confirm
Object list and volumesWhich objects are involved? Record counts now, in 1 year, in 3 years?
Query patternsWhat fields appear in WHERE, ORDER BY, or GROUP BY in the most frequent SOQL queries?
Relationship requirementsIs cascade delete acceptable? Are rollup summaries needed? Can a child exist without a parent?
Integration requirementsDoes any external system upsert records? If so, what is the natural key?
Common wrong assumptionLookup relationships do NOT support rollup summary fields; only master-detail children do.
Key limits in play2 master-detail per child object; 40 lookup per object; 25 external ID fields per object; custom indexes via Salesforce Support case (not self-serve).

Core Concepts

Lookup Relationships

A lookup relationship is a loosely coupled reference from a child object to a parent object. The child record can exist without a parent (the parent field is optional unless you mark it required on the page layout). Deleting the parent does not automatically delete child records — the lookup field is simply cleared (Metadata API deleteConstraint: SetNull, the default). You can self-serve switch that to Restrict, which blocks the parent delete. Cascade is not a third option in the field wizard: it requires Salesforce Support to enable the org feature "cascade delete on custom lookup relationships", it is unavailable for lookups to standard objects, and it bypasses security and sharing (see references/gotchas.md). Lookups do not support rollup summary fields on the parent.

Each object supports up to 40 lookup relationship fields. Standard relationship fields (OwnerId, CreatedById, LastModifiedById, RecordTypeId) are indexed by default and do not count toward the custom lookup limit.

Use lookup when: The child record has independent business meaning, the parent may be deleted without removing children, or you need more than 2 parent references on the same child.

Master-Detail Relationships (MDR)

A master-detail relationship is a tightly coupled parent-child relationship. The parent field on the child is required — a child record cannot exist without a parent. Deleting the parent cascades and permanently deletes all detail records. Master-detail relationships enable rollup summary fields (COUNT, SUM, MIN, MAX) on the parent object.

Each child object supports a maximum of 2 master-detail relationships. You cannot convert a lookup to master-detail if records already exist with a blank parent field (all existing children must have a populated parent). MDR fields are indexed automatically.

Cascade delete chain risk: If Object A is the master of Object B, and Object B is the master of Object C, deleting an A record cascades through B into C. Chains deeper than two levels are a production risk — a single parent delete can remove thousands of grandchild records silently.

Use master-detail when: The child's entire existence depends on the parent (e.g., Order Line Item depends on Order), rollup summaries are required, and you are certain cascade delete is the desired behavior.

Many-to-Many: Junction Objects

Salesforce does not support native many-to-many relationships. The standard pattern is a junction object: a custom object with two master-detail relationships, one to each side of the many-to-many. This gives you:

Both legs must be legal. Eight standard objects can never be the master: BusinessHours, Idea, Lead, OrderItem, PriceBook2, Product2, QuoteLineItem, User. A junction to any of them must use a lookup on that leg, which means no native rollup on that parent — Account_Product__c with a master-detail to Product2 is not buildable. No standard object can be the detail of a custom object either.

If you model the junction with two lookup fields instead of two MDR fields, you lose rollup summary capability on both sides and the junction records survive even after one parent is deleted — which creates orphan records and referential integrity problems.

Field Type Selection for Data Integrity

Choosing the wrong field type loses platform-enforced validation, formatting, and UI affordances:

DataCorrect TypeWrong TypeWhat You Lose
Phone numberPhoneText(255)Click-to-dial, mobile formatting
Email addressEmailText(255)Click-to-email, email validation
PercentagePercentNumberAutomatic % display, decimal semantics
Currency amountsCurrencyNumberMulti-currency support, locale formatting
Long unformatted textLong Text AreaText(255)Storage — Text truncates at 255 chars
Structured rich contentRich Text AreaLong Text AreaFormatting, inline images
Unique record keyExternal ID (+ Unique)TextIndexed by default, available for upsert
True/FalseCheckboxPicklistCompact storage, formula compatibility

Indexing Strategy

Salesforce automatically indexes a small set of standard fields on every object: Id, Name, OwnerId, CreatedDate, SystemModstamp, RecordTypeId, and all standard and custom relationship fields.

Custom indexes extend selective filtering to other fields. They are not self-serve — you request them via a Salesforce Support case. Conditions for a field to be eligible for a custom index:

Skinny tables are a Salesforce-managed performance optimization for large objects with frequent queries on a specific set of fields. A skinny table is a denormalized projection of selected fields, maintained internally. You request skinny tables via Salesforce Support. They are most useful when a single object has millions of records and a small fixed set of non-indexed fields is repeatedly queried together.


Common Patterns

Pattern: Junction Object for Many-to-Many with MDR Fields

When to use: Two objects need a many-to-many relationship and you need rollup summaries or tight referential integrity.

How it works:

  1. Create a custom junction object (e.g., Contact_Campaign__c to link Contact and Campaign).
  2. Add a master-detail field to the first parent (e.g., Contact__c MDR to Contact).
  3. Add a second master-detail field to the second parent (e.g., Campaign__c MDR to Campaign).
  4. Add any relationship-attribute fields to the junction (e.g., Role__c, Response_Date__c).
  5. Optionally add rollup summary fields on each parent to aggregate junction data.

Why not two lookups: Lookup-based junction objects cannot have rollup summary fields on either parent, and orphan junction records accumulate when parents are deleted.

Pattern: External ID for Integration Upsert

When to use: An external system needs to create or update Salesforce records without knowing the Salesforce Id, using a natural key from the source system.

How it works:

  1. Create a custom field with type Text (or Number), mark it as External ID and Unique.
  2. The field becomes indexed automatically — no Support case required.
  3. Use the REST API upsert endpoint or Bulk API 2.0 upsert with the external ID field as the key.
  4. Salesforce matches on the external ID value: if found, updates; if not found, inserts.

Limit: Each object supports up to 25 external ID fields. External ID fields indexed by default; standard text fields are not.

Why not use Name as the key: Name is not guaranteed unique and is not marked as an external ID, so the upsert API will not match on it.

Pattern: Custom Index for Selective Filter on Non-Standard Field

When to use: A large object (500k+ records) is queried frequently with a WHERE filter on a field that is not a standard indexed field, and the query is timing out or performing full table scans.

How it works:

  1. Confirm the filter is selective (the filtered result is less than ~10% of total records, or under 333k records).
  2. Confirm the field is not a formula, encrypted, or multi-select picklist.
  3. Open a Salesforce Support case requesting a custom index on the specific field and object.
  4. After the index is created, re-run the query and confirm execution plan shows index use (use SOQL EXPLAIN via Tooling API or Developer Console).

Why not just add an external ID: External ID fields are indexed, but they carry uniqueness semantics. Use custom index requests for non-unique filter fields.


Decision Guidance

SituationRecommended ApproachReason
Child record must not exist without a parentMaster-DetailParent field is required; enforce referential integrity at the platform level
Rollup summary needed on parentMaster-DetailRollup summary fields only work on MDR parent objects
Child record has independent meaning; parent may be deletedLookupChild survives parent deletion; parent field is optional
Many-to-many relationship requiredJunction object with two MDR fieldsEnables rollup summaries and cascade delete on both sides
Many-to-many but rollup not needed and orphan records are acceptableJunction object with two LookupsLooser coupling; junction survives parent deletion
Phone or email data to display in UIPhone / Email field typePlatform formatting, click-to-dial/email, mobile display
Integration upsert with a source-system natural keyExternal ID field (Unique)Automatically indexed; supported by upsert API endpoints
Large object with slow non-indexed filter queriesCustom index (via Support case)Selective index on the filter field reduces full-table scans
Very large object, small set of repeatedly queried fieldsSkinny table (via Support)Denormalized projection eliminates wide-row scans
Need to store more than 255 charactersLong Text AreaText field hard-truncates at 255 characters

Mode 1: Build from Scratch

Use this mode when modeling a new set of objects or relationships before any data exists.

  1. Map entities and cardinality: For each entity pair, determine whether the relationship is one-to-one, one-to-many, or many-to-many.
  2. Select relationship type: Apply the Decision Guidance table above. Default to lookup unless cascade delete + rollup summaries are needed, or until you are certain the child's existence is fully dependent on the parent.
  3. Select field types: For each attribute, choose the most semantically specific field type (Phone over Text, Currency over Number, etc.).
  4. Plan external IDs: For every object that an external system will write to, identify the natural key and create an External ID field.
  5. Identify index candidates early: List fields likely to appear in WHERE clauses. Standard fields are already indexed. Flag any custom fields for a future Support case if the object is expected to grow past 500k records.
  6. Document the model: Fill in the data-model-design-patterns-template.md with the decisions made and their rationale.

Mode 2: Review an Existing Model

Use this mode when auditing an org's data model for quality or before a major integration or migration project.

  1. Run scripts/check_data_model.py against the exported metadata to detect structural anti-patterns automatically.
  2. Check all junction objects: do they use two MDR fields or two lookups? Flag lookup-based junctions.
  3. Check for Text fields storing phone or email data by inspecting field labels and common values.
  4. Check external ID coverage: does each object integrated with an external system have an External ID field?
  5. Check for MDR chains deeper than 2 levels — these create hidden cascade delete risk.
  6. Check the SOQL query log (from Debug Logs or Event Monitoring) for queries performing full table scans on large objects. Cross-reference against available indexes.
  7. Fill the Review Checklist below and report findings using the template.

Mode 3: Troubleshoot

Use this mode when a production issue traces back to the data model.

SymptomLikely cause / next step
Unexpected record deletionCheck for MDR cascade delete chains. A deleted grandparent may have silently removed grandchildren.
Rollup summary not availableRelationship is likely a lookup, not a master-detail. Rollups require master-detail.
SOQL timeout or governor limit on large objectCheck query execution plan. If full table scan, identify filter field and request a custom index if eligible.
Upsert matching not workingConfirm the target field is marked External ID. Upsert won't match on non-External-ID fields.
Orphaned children after a lookup parent deleteLookup delete behavior is SetNull (clears the field) by default. Cascade is not self-serve — it needs a Salesforce Support case and bypasses sharing. Use Restrict, master-detail, or a before-delete trigger instead.
Text field values getting truncatedField is likely Text(255). Migrate to Long Text Area.

Step-by-step instructions for an AI agent or practitioner activating this skill:

  1. Gather context — confirm the org edition, relevant objects, and current configuration state
  2. Review official sources — check the references in this skill's well-architected.md before making changes
  3. Implement or advise — apply the patterns from Core Concepts and Common Patterns sections above
  4. Validate — run the skill's checker script and verify against the Review Checklist below
  5. Document — record any deviations from standard patterns and update the template if needed

Review Checklist

Run through these before marking data model work complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. MDR cascade delete is silent and permanent — Deleting a master record immediately and permanently deletes all detail records, including any detail-of-detail records two levels down. There is no recycle bin recovery for cascade-deleted records. A single accidental parent delete can wipe thousands of related records.
  2. Converting lookup to master-detail requires all children to have a parent — You cannot convert an existing lookup field to master-detail if any child records have a null parent field. You must first populate the parent field on all records, then perform the conversion — which may require a data migration step on large datasets.
  3. Lookup filter conditions add to query cost, not reduce it — Lookup filters restrict which records appear in the lookup search but do not create a database index. If the filter references a non-indexed field on the parent object, the lookup search itself may perform slowly at scale.
  4. External ID limit is 25 per object, but only 3 are indexed in older API versions — While the current platform allows up to 25 external ID fields per object (all indexed), integrations using older API versions (pre-Spring '16) may only respect the first 3. Always verify API version compatibility when adding more than 3 external ID fields.
  5. Skinny tables are not updated in real time for all operations — Skinny tables can lag during bulk data loads. If you run a report or query immediately after a large Bulk API job, the skinny table projection may not reflect the freshest data until the background refresh completes.
  6. Junction object with two lookups loses rollup summary on both sides — This is the most common many-to-many modeling mistake. Once a junction is built with lookup fields and populated with data, converting to MDR requires clearing and repopulating those fields — a non-trivial data migration.

Output Artifacts

ArtifactDescription
data-model-design-patterns-template.mdFill-in-the-blank output document capturing relationship decisions, field type choices, indexing plan, and anti-pattern findings
check_data_model.pystdlib Python checker that validates junction object MDR structure in exported Salesforce metadata

Package files