Of all the architectural concepts in Salesforce, sharing and visibility generates the most confusion, the most support tickets, and the most security audit findings. The reason is that most developers learn the pieces independently without ever seeing how they form a layered system with strict rules about which layers interact with which.
This article is the unified mental model I wish I had when I started. It covers every layer of the sharing model, how they interact, and how to implement the most complex scenarios.
The Sharing Architecture: Five Layers
Access to a record in Salesforce is determined by evaluating five layers in order. Each layer can only grant access — never restrict it. Once any layer grants access, the user can see the record.
Layer 1: Organization-Wide Defaults
OWD is the most important setting in your sharing model. It answers the question: “What access does a user have to a record they do not own and are not explicitly shared on?”
The options for most objects:
- Private — only the record owner and users above them in the role hierarchy can access the record
- Public Read Only — all users can view, only the owner (and hierarchy) can edit
- Public Read/Write — all users can view and edit
- Controlled by Parent — access matches the parent record’s access (used for detail objects in master-detail)
My default recommendation for any object containing sensitive data (opportunities, contacts, financial data): start with Private and open access as needed. It is far easier to open access than to find and close accidental over-sharing that has been accumulating for years.
Layer 2: Role Hierarchy
The role hierarchy is not an org chart — it is an access escalation structure. Users higher in the role hierarchy inherit access to all records owned by users below them (when “Grant Access Using Hierarchies” is enabled on the object, which it is by default).
This creates implicit sharing that many developers forget about. A VP of Sales can see every Opportunity owned by every Sales Rep in their region without any explicit sharing rule. If this is not the intended behavior, you need to disable role hierarchy access for that object, which moves you to a territory-based or sharing-rule-based model.
Common architecture mistake: creating very flat role hierarchies to avoid complexity, then discovering that all Regional VP users can see each other’s pipeline because they share a role above a common node.
Layer 3: Sharing Rules
Sharing rules extend access beyond the role hierarchy for two types of criteria:
Ownership-based: “Share all Opportunities owned by users in the West Region role with users in the East Region role (Read Only)”
Criteria-based: “Share all Accounts where Industry = ‘Healthcare’ with the Healthcare Specialists public group (Read/Write)”
What about the 300-rule-per-object limit?
The 300-rule-per-object limit is rarely hit in practice, but if you find yourself approaching it, it usually signals an architecture problem — too many special cases that should be handled by rethinking the role hierarchy or using Apex Managed Sharing.
Layer 4: Manual Sharing
Manual sharing is straightforward — record owners or admins click “Share” on a record and grant access to specific users or groups. It is useful for one-off access grants but does not scale.
When you change an object’s OWD from Private to Public Read/Write and then back to Private, Salesforce recalculates sharing and removes all manual sharing grants. They do not return when you revert. This causes data access incidents in orgs that toggle OWD during development.
Layer 5: Apex Managed Sharing
This is the most powerful and most misunderstood layer. Apex Managed Sharing lets you programmatically control record access using the __Share objects that Salesforce generates for every custom object.
public class ProjectSharingService {
// Share a project with a specific user when they are assigned as a reviewer
public static void shareProjectWithReviewer(Id projectId, Id reviewerUserId) {
// Create the share record
Project__Share shareRecord = new Project__Share();
shareRecord.ParentId = projectId; // The record being shared
shareRecord.UserOrGroupId = reviewerUserId; // Who gets access
shareRecord.AccessLevel = 'Read'; // 'Read', 'Edit', or 'All' (All = owner-level)
shareRecord.RowCause = Schema.Project__Share.RowCause.Reviewer_Access__c;
// RowCause must be a custom Sharing Reason defined in Object Manager
// Using a custom RowCause means this share record persists through
// sharing recalculations. The built-in 'Manual' RowCause does not.
Database.SaveResult result = Database.insert(shareRecord, false);
if (!result.isSuccess()) {
System.debug('Sharing failed: ' + result.getErrors()[0].getMessage());
}
}
// Remove sharing when reviewer is unassigned
public static void removeProjectSharing(Id projectId, Id reviewerUserId) {
List<Project__Share> sharesToDelete = [
SELECT Id FROM Project__Share
WHERE ParentId = :projectId
AND UserOrGroupId = :reviewerUserId
AND RowCause = :Schema.Project__Share.RowCause.Reviewer_Access__c
];
if (!sharesToDelete.isEmpty()) {
delete sharesToDelete;
}
}
}Creating Custom Sharing Reasons
Before the code above works, you must create a custom Sharing Reason in Setup. Navigate to Object Manager, then your custom object, then Sharing Reasons, then New. Name it “Reviewer Access” and the API name becomes Reviewer_Access__c.
Using a custom Sharing Reason (RowCause) instead of Manual is critical. Records shared with RowCause = Manual are deleted during sharing recalculation. Records shared with a custom RowCause persist — they are considered “owned” by your Apex logic and Salesforce does not touch them during recalculation.
Scenario: A developer builds an Apex controller for a community portal. They write public class PortalAccountController with no sharing declaration. Community users can see accounts they own — but so can every other community user. A security audit reveals that any authenticated community user can retrieve every Account in the org by calling the controller directly, bypassing the intended Private OWD setting.
Add with sharing to every user-facing Apex class: public with sharing class PortalAccountController. Without the keyword, Apex inherits the sharing context of its caller. If invoked as the entry point (e.g., from anonymous Apex), it runs without sharing enforcement. Declaring with sharing explicitly ensures the running user’s record access is enforced at the database query level regardless of the calling context.
With Sharing vs Without Sharing in Apex
Every Apex class that accesses data should explicitly declare its sharing behavior. The three options:
with sharing
// Respects the running user's record access
// Use for: all user-facing controllers, APIs, triggers
public with sharing class AccountController {
public static List<Account> getMyAccounts() {
// Only returns accounts the running user can see
return [SELECT Id, Name FROM Account LIMIT 100];
}
}without sharing
// Ignores all sharing rules — queries return all records
// Use for: scheduled jobs, system-level operations, admin tools
public without sharing class AccountSyncBatch {
public static List<Account> getAllAccounts() {
// Returns ALL accounts regardless of running user
return [SELECT Id, Name FROM Account LIMIT 100];
}
}inherited sharing
// Inherits sharing context from the calling class
// Use for: utility methods called by both user-facing and system contexts
public inherited sharing class AccountUtils {
public static String formatAccountName(String name) {
return name?.trim()?.toUpperCase();
}
}The most dangerous mistake: writing public class AccountController with no sharing declaration. Without the keyword, Apex inherits the sharing context of its caller. If invoked as the entry point (e.g., from anonymous Apex or a VF page), it runs without sharing enforcement — your user-facing controller bypasses the entire sharing model and returns records the user should not see. Always declare sharing keywords explicitly. Never rely on the inherited default.
Common Architecture Scenarios
Scenario 1: Territory-Based Access with Exceptions
A sales org has territories. Accounts belong to a single territory. Sales reps should only see accounts in their territory, but account managers need read access to all accounts in their region.
Solution Details
- OWD: Private on Account
- Role hierarchy: Region Manager roles above Territory Rep roles
- Sharing Rules: “Accounts owned by Territory A users, share with Territory A users (Read/Write)”
- Role hierarchy handles Region Manager access automatically (they are above reps in the hierarchy)
Scenario 2: Dynamic Sharing Based on Business Logic
A custom object Project__c needs to be visible to the project team members stored in a junction object Project_Member__c. Standard sharing rules cannot handle this — they cannot reference related object data.
Solution: Apex Managed Sharing trigger that runs when Project_Member__c records are inserted or deleted.
trigger ProjectMemberTrigger on Project_Member__c (after insert, after delete) {
if (Trigger.isInsert) {
List<Project__Share> shares = new List<Project__Share>();
for (Project_Member__c member : Trigger.new) {
shares.add(new Project__Share(
ParentId = member.Project__c,
UserOrGroupId = member.Team_Member__c,
AccessLevel = 'Read',
RowCause = Schema.Project__Share.RowCause.Team_Member__c
));
}
Database.insert(shares, false);
}
if (Trigger.isDelete) {
Set<Id> memberIds = new Set<Id>();
for (Project_Member__c member : Trigger.old) {
memberIds.add(member.Id);
}
// Query and delete associated share records
delete [SELECT Id FROM Project__Share
WHERE RowCause = :Schema.Project__Share.RowCause.Team_Member__c
AND ParentId IN (
SELECT Project__c FROM Project_Member__c WHERE Id IN :memberIds
)];
}
}Scenario 3: Read Access Across the Entire Org for One Object
An object stores product catalog information that all users need to read but only the catalog team should edit.
Solution Details
- OWD: Public Read Only
- Assign catalog team to a “Catalog Managers” role with edit access via ownership (they own the records)
- No sharing rules needed — all users can read, owners and hierarchy can edit
What the Sharing Model Cannot Do
The sharing model controls record-level access. It does not control:
- Field-level access — use Field-Level Security (FLS) on Permission Sets and Profiles
- Object-level access — use object-level CRUD permissions on Permission Sets
- What users see in related lists — controlled by object access, not record sharing
- Report and dashboard access — controlled by Report/Dashboard folder sharing, a separate system
When auditors ask “can User X see Field Y on Record Z?”, the answer involves three separate systems: record sharing (can they access the record at all?), object permissions (can they read the object?), and FLS (can they see that specific field?). A record being shared does not grant field access if FLS restricts it.
Summary
The sharing model’s power comes from its layered, additive nature. Design it starting from the bottom up: set OWD to the most restrictive setting that makes sense for the data, then selectively open access using the appropriate layer for each use case.
What is the most complex sharing scenario you have had to implement? I am particularly curious whether teams reach for Apex Managed Sharing when they should be using criteria-based sharing rules, or vice versa.
Test Your Knowledge
How did this article make you feel?
Comments
Salesforce Tip