A many-to-many relationship in Salesforce lets each record on one object relate to many records on another object, and each of those to many back. Salesforce has no built-in “many-to-many” field type — you implement it with a junction object: a custom object that sits between the two related objects and holds two master-detail relationships, one to each side. Each row of the junction represents a single connection between one record from each side.
The shape of a many-to-many in Salesforce
Student Student_Class__c Class
┌───────┐ ┌───────────────┐ ┌────────┐
│ Id │◄──── M-D ─────┤ Student__c │ │ Id │
│ Name │ │ Class__c ├──── M-D ────►│ Name │
└───────┘ │ Grade__c │ └────────┘
│ Enrolled_Date │
└───────────────┘
one student → many junction rows → many classes
one class → many junction rows → many students
A student can take many classes; a class has many students. Each enrollment is one row in Student_Class__c holding the linkage plus any per-enrollment data (grade, enrolled date, status).
Why the junction object usually carries extra data
The junction is not just plumbing — it’s the right place to store anything that depends on the combination of both sides:
- Student × Class → Grade, Enrolled Date, Active flag
- Project × Resource → Hours Allocated, Role on Project
- Account × Product → Subscription Status, Discount %
- Opportunity × Contact → Role (Decision Maker, Influencer, etc.) — this is what
OpportunityContactRoledoes as a standard junction
If you find yourself storing data on one side that “really belongs to the pair,” that’s a sign the junction should hold it instead.
Primary master vs secondary master
When you create the junction with two master-detail fields, the first master-detail you create becomes the primary master. This matters because:
- The primary master controls the junction’s ownership and sharing.
- The junction record appears in the related list of the primary master with default placement.
- Cascade delete behavior: deleting the primary master deletes all junction records pointing to it (and that does not delete the secondary master’s records — only the junction rows).
- Roll-up summary fields can be created on either master, summarizing junction-level data.
You can change which master is primary later by manipulating the field order, but it has sharing implications — design this carefully up front.
Standard many-to-many relationships in Salesforce
Salesforce ships with several built-in junction-style relationships:
| Junction | Sides | Per-junction data |
|---|---|---|
| OpportunityContactRole | Opportunity ↔ Contact | Role, Is Primary |
| CampaignMember | Campaign ↔ Lead/Contact | Status, Has Responded |
| AccountContactRelation | Account ↔ Contact (Contacts to Multiple Accounts feature) | Role, Direct |
| CaseTeamMember | Case ↔ User | Team Role, Access |
| OpportunityTeamMember | Opportunity ↔ User | Team Role, Access |
| UserRecordAccess | (system table for sharing) | — |
These are not implemented as user-defined junction objects exactly the same way — some use master-details, some use lookup pairs with system fields. But conceptually they all serve the many-to-many purpose.
Junction limits and considerations
| Concern | Detail |
|---|---|
| Master-detail per junction | Exactly 2 (one to each side) — the standard junction pattern |
| Required-ness | Both master-details are required (it’s master-detail, after all) |
| Reparenting | Allowed if “Allow reparenting” is checked on the secondary master field |
| Sharing | Controlled by the primary master’s OWD and sharing |
| Cascade delete | Junction rows deleted when either master is deleted |
| Undelete | If the primary master is undeleted from the Recycle Bin within 15 days, its junction rows come back; if a non-primary master is undeleted, junction rows do not automatically come back |
| Roll-up summary | Available on either master |
Real scenario
“You’re modeling Projects and Skills. A project requires multiple skills; a skill can be needed by many projects. You also need to track required proficiency level for each (skill, project) combination.”
Build:
Project__c(custom object — or use a standard if it fits).Skill__c(custom object).Project_Skill__c(junction custom object).- Master-Detail 1:
Project__c(creates the primary master). - Master-Detail 2:
Skill__c. - Custom fields:
Required_Proficiency__c(Picklist: Junior/Mid/Senior),Hours_Estimate__c(Number).
- Master-Detail 1:
- On Project, optional roll-up:
Skill_Count__c = COUNT(Project_Skill__c).
Now each project has many skills; each skill is referenced by many projects; the proficiency level is captured exactly once per pair.
Lookup-based many-to-many — when and why
Strictly, you can build a many-to-many with two lookup fields on the junction instead of master-detail. Salesforce sometimes calls this a “weakly-linked junction.” Reasons to consider:
- Junction records should keep their own ownership and sharing (e.g., a “Recommendation” between two records that the recommender owns).
- You want junction records to not cascade-delete with either side.
- Either side could be missing (e.g., during data migration).
Trade-off: no roll-up summaries are possible on either master, you lose cascade delete, and sharing must be designed independently for the junction.
The master-detail pattern is the strong default; lookup-based junctions exist for the specific cases above.
Verified against: Salesforce Help — Many-to-Many Relationships and Object Relationships Overview. Last reviewed 2026-05-17.