← Back to Field Notes

Sharing Rules vs Manual Sharing vs Apex Sharing: When to Use What

·9 min read

The worst sharing model I ever inherited had 41 criteria-based sharing rules on a single custom object, a nightly batch job that re-created manual shares that kept disappearing, and around 300 lines of Apex doing roughly the same job three different ways. Nobody in the organisation could tell me who could see what. More to the point, neither could Salesforce — not without a sharing recalculation that took most of an afternoon and locked group membership changes while it ran.

That org taught me the thing I now say at the start of every security review. Sharing isn't one tool, it's four or five, and reaching for the wrong one is how you end up with 41 rules and a batch job holding the whole thing together with tape. Sharing rules, manual sharing, and Apex managed sharing each solve a different problem. Knowing which is which is most of the battle.

Org-Wide Defaults Decide Everything Downstream

Everything about sharing starts with one decision: the org-wide default (OWD) for each object. Get that wrong and no amount of clever sharing fixes it.

The critical thing to internalise is that every sharing mechanism only ever opens access up. Sharing rules, manual shares, Apex shares, the role hierarchy — all additive. None of them can take access away from someone who already has it through a broader grant. If you set an object to Public Read/Write, the Share button disappears, the __Share object stops mattering, and every sharing rule you write is a no-op. You've handed everyone the keys and there's nothing downstream that will claw them back.

So the real architectural question is never "how do I share this record." It's "what's the most restrictive OWD I can start from and still let people do their jobs." Start Private, then open up deliberately. I've never regretted starting too restrictive. I've regretted the opposite plenty.

This is also where sharing and the object permission model stop overlapping. Sharing decides which records a user can see; permission sets decide whether they can touch the object or field at all. They're separate enforcement paths — which is one of the reasons I keep pushing teams toward the model in moving from profiles to permission sets. A user with no Read on the object won't see the record no matter how many shares point at it.

Sharing Rules Are for Broad, Declarative Access

Sharing rules are the first tool I reach for, and usually the last one I need. They're declarative, they recalculate automatically, and an admin can maintain them without a deployment.

Two flavours. Ownership-based rules share records owned by one group or role with another. Criteria-based rules share records that match a field condition — Region equals APAC, Stage equals Closed Won, that sort of thing. Both grant to groups, roles, or roles-and-subordinates. They never grant to a single named user. If you're trying to share one record with one person, a sharing rule is the wrong tool.

The trap is volume. Salesforce caps criteria-based sharing rules at 50 per object by default, and long before you hit that ceiling you feel the recalculation cost. Every time you edit a rule, change the role hierarchy, or move users between public groups, Salesforce recalculates sharing — and on a large object with many rules that recalc is not free. The 41-rule org I opened with spent real time locked during recalcs, with group membership changes queuing up behind them.

My rule of thumb: if the access pattern is "this category of records should be visible to this group of people," it's a sharing rule, and it should stay a sharing rule. The moment the logic needs something a criteria filter can't express — a related record, a running total, the result of a callout — that's your signal to stop.

Manual Sharing Is a Decision, Not an Architecture

Manual sharing is the Share button on a record. A user with Full Access grants another user or group access to that one record. Behind the scenes it writes a row to the __Share object with a RowCause of Manual.

I like manual sharing for exactly what it is: a human making a one-off call. A deal owner loops in a specialist for a single opportunity. A case gets escalated to someone outside the normal queue. These are genuine exceptions and they shouldn't be encoded in rules.

What manual sharing is not is a foundation you build on. Two properties make that a trap. First, manual shares only exist when the OWD is more restrictive than Public Read/Write — otherwise there's nothing to share. Second, and this is the one that catches people, a manual share is deleted automatically when the record's owner changes. Salesforce assumes the new owner's access context is different and wipes manual shares on that record.

That second property is exactly why the nightly batch job in my inherited org existed. Someone had scripted manual shares through the Data Loader, ownership changes kept erasing them, and rather than fix the cause they built a job to re-create the shares every night. It worked, in the sense that it ran. It was also the wrong tool wearing the right tool's clothes.

Apex Managed Sharing Handles the Logic You Can't Declare

When the access rule is real logic — it depends on related data, computed values, or the outcome of a process — you write Apex managed sharing.

Mechanically it's the same __Share object, just written from code. For a custom object Deal__c, the share object is Deal__Share, and inserting a record grants access. The important part is the RowCause. On custom objects you can define a custom Apex sharing reason and reference it, and that's what you should do:

public with sharing class DealSharingService {

    // Called from the Deal__c trigger's after-insert and after-update contexts.
    public static void syncUnderwriterAccess(List<Deal__c> deals) {
        List<Deal__Share> shares = new List<Deal__Share>();

        for (Deal__c deal : deals) {
            if (deal.Underwriter__c == null) {
                continue;
            }
            Deal__Share share = new Deal__Share();
            share.ParentId      = deal.Id;
            share.UserOrGroupId = deal.Underwriter__c;
            share.AccessLevel   = 'Edit';
            share.RowCause      = Schema.Deal__Share.RowCause.Underwriter__c;
            shares.add(share);
        }

        // allOrNone=false so one duplicate share doesn't roll back the whole batch.
        Database.SaveResult[] results = Database.insert(shares, false);
    }
}

A few things in there are load-bearing and not obvious.

The RowCause is a custom Apex sharing reason — Underwriter — that I defined on the object, not the built-in Manual reason. This matters more than anything else in the class. Shares written with a custom reason are yours: they survive owner changes, and you can query and recalculate them as a set. If you write shares from Apex with RowCause Manual, they behave like UI manual shares — wiped on owner change — and you can't tell them apart from shares a user created by hand. I've watched a team burn two days on "disappearing access" that came down to exactly this.

AccessLevel has to be higher than the object's OWD or the insert throws. You can grant Edit on a Private object; you can't grant Read on an object that's already Public Read Only. And I insert with allOrNone false on purpose, because a duplicate share — same parent, same user, same reason — throws, and I don't want one collision rolling back a bulk load.

Where this code lives matters too. It belongs in a dedicated sharing service class that the object's trigger delegates to, in the after-insert and after-update contexts specifically, using the same trigger framework I've written about. Sharing recalculation is a service concern, not something you inline in a trigger body.

When Apex Sharing Bites Back

Apex sharing is powerful and it's the one I'm most cautious recommending, because it's the easiest to get subtly wrong.

The failure mode is drift. Declarative sharing recalculates itself; Apex sharing recalculates when you tell it to. If the underwriter on a deal changes and your code inserts the new share but forgets to delete the old one, you've silently left access behind. Multiply that across a few years of edits and you get an org where the __Share table has grown a long tail of shares that no longer correspond to any live rule. On one org we found around 8,000 orphaned share rows on a single object, give or take — access that should have been revoked and never was.

The honest fix is a recalculation class. Salesforce lets you register an Apex class against your custom sharing reason so the platform can rebuild those shares during a Recalculate action or after an OWD change. Write it at the same time as the code that creates the shares, not eighteen months later when someone notices the drift. We added recalculation to that 8,000-row object, cleaned it up once, and it's held since. I won't pretend we retrofitted it everywhere, though. Some of the older objects in that org are still running insert-only sharing and I know it. It's on the list.

How I Actually Choose

Stripped down, the decision is short.

If the access pattern is a category of records for a group of people, it's a sharing rule. Declarative, admin-maintainable, self-recalculating. Start here and stay here as long as you can.

If it's a genuine one-off — one record, one person, a human judgement call — it's manual sharing, and it should stay a rare exception rather than something a script performs at scale.

If the rule is logic a criteria filter can't express, it's Apex managed sharing, with a custom sharing reason and a recalculation class written on day one, not day five hundred.

The mistake I see most often isn't picking the wrong one once. It's picking manual or Apex sharing to paper over an OWD that was too open, or a sharing rule that was too broad. When the sharing feels hard, the answer is usually a level up — revisit the OWD — not another share row.

And record-level sharing is only half the security picture. What a user can do with the fields on a record they can already see is a separate enforcement path, one I've written about separately in CRUD/FLS enforcement in Apex. Get both right or neither is real. If you've inherited a sharing model nobody can explain and want a second pair of eyes before you start pulling threads, that's the kind of review I do.

A clean sharing model is quiet. You can look at any record and say who sees it and why in one sentence, and the platform can recalculate the whole thing without anyone noticing. The one I inherited needed a batch job, an afternoon, and a prayer. I know which org I'd rather be on call for.

Liked this?

New essays in your inbox when I publish. No cadence, no spam.