← All articles

GTM Systems

Someone Wrote 200 Lines of Apex to Copy One Field, and It Failed a Deploy Two Years Later

The declarative-first rule is not dogma; it is a bet on who can maintain the thing after you leave. Here is the rule, the specific cases that genuinely need Apex, and the honest line between them.

· 8 min read

I once spent an afternoon deleting 200 lines of Apex whose entire job was to copy a field from the Account to the Contact on insert. It had a trigger, a handler class, a test class, and a mocking utility, and it failed a validation deploy two years after the author left because a schema change broke the test and nobody understood the code well enough to fix it. The whole thing was a before-save flow’s worth of work wearing a full software-engineering costume. The person who wrote it was a strong developer who reached for the tool they knew. The org paid for that reach every deploy after they were gone.

That is the case for declarative-first, and it is not a purity argument. It is a bet about maintenance. A flow can be opened, read, and edited by the admin who inherits the org; Apex requires a developer, a test class, code coverage, and a deploy pipeline. When the declarative tool can do the job, using code means you have made the thing harder to maintain in exchange for nothing. The rule is: build it declaratively unless you hit a wall the platform genuinely cannot clear, and know exactly where those walls are so you are not guessing.

200 lines
Of Apex to do one before-save flow's job
75%
Code coverage Apex forces you to carry forever
0
Test classes a flow requires to deploy

The rule is a maintenance bet, not a taste

The reason to prefer declarative is that the population who can safely change a flow is far larger than the population who can safely change Apex. Every Apex class you add is a class someone must understand, test, and keep at 75 percent coverage to deploy anything in that org ever again. A broken test class from a departed developer can block an unrelated deploy for the whole team, which is exactly how the field-copy trigger held a release hostage. Flows have their own failure modes, but “an admin cannot read it” is not one of them. When you choose code over config for work config can do, you are trading away maintainability for familiarity, and the org pays the interest.

Who can safely maintain this after you leave
Relative size of the population that can read and safely change each artifact in a typical org. A flow can be maintained by any admin; Apex needs a developer who also understands the test suite. Choosing code for config-able work shrinks your maintenance pool for no functional gain.
View as table
ItemValue
Point-and-click config90%
A record-triggered flow70%
A well-written Apex class25%
Inherited undocumented Apex8%

Where the declarative wall actually is

Declarative-first does not mean declarative-only. There are cases the platform genuinely cannot clear with config, and pretending otherwise produces flows that are worse than the Apex they replaced: a monstrous flow with forty elements doing loops-within-loops that no admin can read either. Know the real walls. Complex bulk logic across many records where you need fine control over DML and governor limits. True recursion or multi-level tree traversal (walking an account hierarchy to the root). Callouts that need custom retry, chaining, or response parsing beyond what the HTTP action offers. Operations that must run as a single atomic transaction with rollback semantics a flow cannot express. And anything genuinely reusable across many contexts where an invocable Apex method called from flows beats copying flow logic five times.

The declarative wall Config until you hit one of these, then code
Declarative (default)Field updates, defaultsCross-object writes via flowRecord-triggered automationSimple approvals, notificationsStandard HTTP calloutsApex (only past the wall)Bulk logic needing limit controlRecursion / hierarchy traversalComplex callouts, retry, chainingAtomic multi-step transactionsReusable invocable methods
The default is the left column. You cross to Apex only when the work sits in the right column: bulk logic needing limit control, recursion, complex callouts, atomic transactions, or true reuse. Everything else stays declarative.

When you do write Apex, write the piece config cannot do

The best pattern is not all-flow or all-Apex; it is a flow that orchestrates and an invocable Apex method that does the one hard thing. The admin owns the flow, the criteria, and the ordering; the developer owns the narrow, well-tested method the flow calls. The field-copy trigger should have been a flow. But an account-hierarchy roll-up that has to walk to the root and sum across every descendant is a genuine wall, and the right shape is an invocable the flow hands the account to.

// The narrow piece config cannot do: walk an account tree to the root and
// sum. Exposed as invocable so a flow orchestrates and an admin owns the
// trigger criteria. The developer owns only this one hard method.
public with sharing class HierarchyRollup {
    @InvocableMethod(label='Roll up ARR to top parent')
    public static List<Result> run(List<Id> accountIds) {
        // Recursive CTE-style walk is not expressible in Flow; this is a real wall.
        // ... traverse ParentId to root, aggregate child ARR, return top-parent totals
    }
    public class Result { @InvocableVariable public Id topParentId; @InvocableVariable public Decimal totalArr; }
}

The worked example: the field copy, both ways

Reaching for code versus config-first with a clear line

Code by default Declarative-first, code past the wall
A simple field copy 200 lines + test class A four-element flow
Who can maintain it A developer, if they decode it Any admin who can read a flow
Deploy risk One stale test blocks all deploys No coverage debt to carry
A hierarchy roll-up Apex (correct: real wall) Apex (correct: real wall)
When to write code Whenever it feels faster Only when config genuinely cannot
Org two years later A pile of undocumented classes Flows an admin owns, code where it earns it
Two philosophies, applied to the same org over two years.

Here is how I decide declarative versus code

The declarative-first decision
  1. 1

    Start by assuming a flow can do it

    Field updates, defaults, cross-object writes, record-triggered automation, standard callouts, most approvals. The default answer is config. Make code prove it is needed, not the other way around.

  2. 2

    Name the specific capability config lacks

    Finish "the platform cannot ___" with a real capability: recursion, atomic rollback, limit control on large volumes, custom retry. If you cannot name one, there is no wall and no reason for Apex.

  3. 3

    If there is a wall, isolate the hard part

    Write an invocable Apex method that does only the thing config cannot, and let a flow orchestrate around it. The developer owns a narrow, tested method; the admin owns the criteria and ordering.

  4. 4

    Keep the flow readable or you have not won

    A forty-element flow with nested loops is as unmaintainable as the Apex it replaced. If config forces a monster, that is itself a signal you may be past the wall.

  5. 5

    Test the invocable like real code

    The narrow Apex piece still needs a real test asserting behavior. You reduced the code surface; you did not remove the obligation to prove the part that remains works.

  6. 6

    Delete the code that config made obsolete

    When you find a 200-line trigger doing a flow-sized job, replacing it is not scope creep; it is paying down debt that will block your next deploy. Prove behavior parity, then delete.

The 200-line field copy was not written by a bad engineer. It was written by a good one who reached for the tool in their hand instead of the tool the org could maintain. Declarative-first is a bet that the person inheriting your org can read a flow more easily than they can decode a trigger, and that bet pays off every deploy after you are gone. Build it in config by default, name the specific wall when you hit one, and write Apex only for the narrow piece the platform genuinely cannot express. The reward is an org where the automation outlives its authors, which is the same goal behind documenting the firing order and paying down the automation debt an accreted stack leaves behind.

salesforce apex flows

Keep reading

One email. Every week.

One email a week: a systems problem I architected or untangled, with the schema, the config, and what I would change. No roundups, no theory, unsubscribe whenever it stops being useful.

The newsletter opens soon.

Connect a provider in src/config.ts