Merging Apex and declarative work into a shared org
Salesforce orgs are changed by two populations who rarely share a workflow. When their work meets in a release, the result is a metadata conflict that no merge tool can settle on its own — because the decision that matters is usually not in the file being merged.
Work in feature branches. Write Apex classes, triggers and Lightning components. Changes arrive as pull requests and get reviewed as code, by people who read code.
Work in the Salesforce UI. Change fields, validation rules, flows and layouts directly in a sandbox. The platform generates the metadata XML afterwards — often without ever passing through a branch or a review.
Both are legitimate ways to build on the platform. The problem is the seam between them: a declarative change is a real change to production behaviour, but it arrives with none of the review apparatus that a code change carries. When the two meet at a deployment boundary, someone is asked to reconcile them — usually under time pressure, usually fluent in only one of the two stacks.
A pro-code branch adds Next_Stage__c to the Case object alongside the Apex
that populates it. In parallel, an administrator adjusts the same field directly in the
UAT sandbox. Both changes are saved. Both are correct in isolation.
A three-way merge presents these as three equivalent choices: keep the left value or the right one, three times, then continue. That framing is the problem. It gives a data protection classification exactly the same visual weight as an integer, and it shows the file in isolation from everything that depends on it.
The dependency that decides conflict two is not in the file at all:
public class CaseStageHandler { private static final Integer MAX_STAGE_LEN = 200; public static void applyNextStage(List<Case> cases) { for (Case c : cases) { String stage = buildStageLabel(c); c.Next_Stage__c = stage.abbreviate(MAX_STAGE_LEN); } } }
force-app/main/default/classes/CaseStageHandler.cls
And the dependency that decides conflict three is in a test:
// 3 tests insert Case without Next_Stage__c @isTest static void escalationWithoutNextStage() { Case c = new Case(Subject = 'Escalation'); insert c; }
force-app/main/default/classes/CaseStageHandlerTest.cls
The conflict is not inside the XML. It is between the XML and the Apex that depends on it — and the merge view cannot show you that.
The administrator who set length to 100 had no way to see
CaseStageHandler. The developer who wrote abbreviate(200) had no
way to see a sandbox change made three weeks later. Neither made a mistake. The seam
between them did.
Each conflict is resolved against the change that requested it and against the code on both sides of the platform, not against whichever version happens to be newer.
Keep PII;GDPR.
The classification was not removed by a decision. It was dropped as a side effect of a declarative save in an org where compliance categorisation was never enabled in the UI. Nothing in the originating change asked for it, and removing a data protection tag from a customer-facing field is not something that should happen silently as the third checkbox in a merge screen.
Keep 200.
CaseStageHandler.applyNextStage() abbreviates to a constant of 200 before
assigning. Accepting 100 does not fail the deployment — it fails later, at runtime, as
a truncation on records whose stage labels exceed the shorter limit. This is the
resolution a metadata-only view is least equipped to make, because the deciding
evidence is in an Apex class the merge never opens.
Not resolved automatically. Raised for a human decision.
Making the field required breaks three Apex tests that insert Case without it — so
the mechanical answer is to keep false. But the administrator may have set
it deliberately, to enforce a process that matters more than the tests do. That is a
business question, not a merge question. The agent reports both consequences, names the
three failing tests, and leaves the decision with the people who own the process.
| Conflict | If resolved by newest wins | Outcome |
|---|---|---|
| complianceGroup | PII;GDPR tag silently dropped in UAT and every environment promoted after it | Compliance regression, invisible until an audit |
| length | Field shortened to 100 while Apex still writes up to 200 | Runtime truncation on live records |
| required | Field made mandatory without reviewing what inserts it | Three test failures, and an unexamined process change |
Two of the three would have deployed cleanly. Neither would have appeared in a pull request, because neither began as code. The review that catches them has to read the metadata and the Apex together, and has to know which of the three differences is a compliance control rather than a configuration value.