DEV Community

Samcorp
Samcorp

Posted on

Flow vs Apex: When We Chose Wrong and What It Cost

Flow vs Apex: When We Chose Wrong and What It Cost
The expensive part wasn't choosing Flow.

It wasn't choosing Apex either.

It was choosing one of them for the wrong reason.

Sometimes we picked Flow because it was faster to build.

Sometimes we picked Apex because the requirement sounded technical.

Both approaches worked initially.

Then the org grew.

More automation appeared. Data volumes increased. One team added another Flow. Another team introduced an Apex trigger. A related-record update triggered another automation path.

Eventually, debugging one field change meant tracing half the Salesforce org.

That changed how we thought about Flow vs Apex.

The question stopped being:

Can Flow do this?

or:

Can Apex do this?

The better question became:

Which approach will remain understandable, scalable, and maintainable when this process becomes more complicated?


1. We Chose Flow Because It Was Faster

The first requirement was straightforward:

When an Opportunity closes, update a few related records.

Flow handled it perfectly.

Opportunity Updated
        ↓
Check Conditions
        ↓
Get Related Records
        ↓
Calculate Values
        ↓
Update Records
Enter fullscreen mode Exit fullscreen mode

No custom trigger.

No large Apex class.

The automation remained visible to administrators.

For that version of the requirement, Flow was the right choice.

Then the requirement grew.

Opportunity Updated
        ↓
Update Account
        ↓
Update Contract
        ↓
Update Forecast
        ↓
Check Partner Rules
        ↓
Recalculate Territory
        ↓
Create Follow-Up Records
        ↓
Trigger Integration
Enter fullscreen mode Exit fullscreen mode

The Flow hadn't become a bad technology.

The problem had changed.

That's an important distinction.

A useful Salesforce automation strategy for choosing Flow vs Apex should consider not only how quickly an automation can be built, but also its complexity, dependencies, risk, ownership, and long-term maintenance cost.

Our mistake was treating the original design decision as permanent.


2. The Mega-Flow Became Hard to Understand

Visual automation is useful because the process is visible.

But visual does not automatically mean simple.

A Flow can eventually become:

Decision_1
Decision_2
Decision_2B
Get_Account
Get_Account_Again
Assignment_14
Update_Records_6
Subflow_ProcessSomething
Enter fullscreen mode Exit fullscreen mode

Our largest Flows accumulated:

  • Multiple decisions
  • Record queries
  • Cross-object updates
  • Calculations
  • Subflows
  • Fault paths
  • Asynchronous actions
  • Integrations

The original argument had been:

Admins will be able to maintain it.

Eventually, even developers were hesitant to modify it.

That is a real cost.

Not only runtime cost.

Cognitive cost.


3. Some Logic Wanted Real Data Structures

This became one of the clearest signals that Apex might be a better fit.

Imagine needing to process hundreds of Opportunities grouped by Account.

In Apex:

Map<Id, List<Opportunity>> opportunitiesByAccount =
    new Map<Id, List<Opportunity>>();
Enter fullscreen mode Exit fullscreen mode

From there, Maps, Sets, helper methods, reusable classes, and explicit algorithms make complex transformations much easier to express.

Trying to model increasingly sophisticated transformations through dozens of visual elements can become harder to maintain than the equivalent code.

That doesn't mean:

Complex = always Apex.

It means the nature of the complexity matters.

When the requirement involves:

Large collections
Complex grouping
Advanced calculations
Reusable algorithms
High-volume transformations
Enter fullscreen mode Exit fullscreen mode

Apex often becomes easier to reason about.


4. Then We Made the Opposite Mistake

After dealing with an oversized Flow, we became too eager to use Apex.

A requirement arrived:

Set a field when an Opportunity exceeds a threshold.

We could write:

trigger OpportunityTrigger on Opportunity (
    before insert,
    before update
) {
    for (Opportunity opp : Trigger.new) {
        if (opp.Amount > 100000) {
            opp.Requires_Executive_Review__c = true;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Nothing is technically wrong with that.

But the requirement is also a natural candidate for a before-save Record-Triggered Flow.

Opportunity Before Save
        ↓
Amount > Threshold?
        ↓
Set Requires Executive Review = True
Enter fullscreen mode Exit fullscreen mode

That version is easier for an administrator to understand and adjust later.

The lesson was:

Writing code doesn't automatically make the solution more engineered.

Sometimes it simply creates code someone must maintain.


5. Apex Increased Change Lead Time

The difference became obvious when business rules changed.

Suppose:

Amount > $100,000
Enter fullscreen mode Exit fullscreen mode

becomes:

Amount > $150,000
Enter fullscreen mode Exit fullscreen mode

With a well-designed Flow, that can remain a relatively small configuration change.

With Apex, the change normally stays within the development lifecycle:

Modify Apex
    ↓
Update tests
    ↓
Code review
    ↓
Deployment
Enter fullscreen mode Exit fullscreen mode

That process is valuable when the complexity justifies it.

It is unnecessary overhead when the requirement is fundamentally simple.

Our rule became:

Use Apex because the problem benefits from Apex—not because a developer happens to be implementing the ticket.


6. Flow and Apex Started Competing on the Same Object

This was where troubleshooting became expensive.

Imagine:

Account Update
     ↓
Record-Triggered Flow
     ↓
Updates Contact
     ↓
Contact Automation
     ↓
Updates Account
     ↓
Account Apex Trigger
     ↓
Updates Custom Object
     ↓
Another Flow
Enter fullscreen mode Exit fullscreen mode

Now ask:

Why did this field change?

The answer may be spread across several automation mechanisms.

That leads to:

  • Difficult execution-order reasoning
  • Recursion risk
  • Duplicate logic
  • Harder debugging
  • Multiple ownership boundaries

The issue wasn't:

Flow + Apex = bad
Enter fullscreen mode Exit fullscreen mode

The issue was:

Uncoordinated Flow
+
Uncoordinated Apex
+
More automation
=
No clear owner
Enter fullscreen mode Exit fullscreen mode

Once an object becomes heavily automated, architecture needs a clear center of gravity.


7. Hybrid Architecture Worked Better

Eventually, Flow vs Apex stopped being a competition.

Some processes genuinely benefited from both.

Imagine:

Case Created
     ↓
Determine SLA Type
     ↓
Calculate SLA Deadline
     ↓
Assign Queue
     ↓
Notify Team
Enter fullscreen mode Exit fullscreen mode

The orchestration is easy to understand visually.

Flow is a natural fit.

But SLA calculations may involve:

Business hours
Holidays
Service levels
Time zones
Complex date calculations
Enter fullscreen mode Exit fullscreen mode

That logic may be easier to isolate in Apex.

A better design is:

Record-Triggered Flow
        ↓
Determine Process Path
        ↓
Invocable Apex
        ↓
Complex Calculation
        ↓
Return Result
        ↓
Continue Flow
Enter fullscreen mode Exit fullscreen mode

Flow owns the business process.

Apex owns the specialized computation.

That hybrid model often gave us the best balance between maintainability and control.


8. Hybrid Doesn't Mean Calling Apex Everywhere

Hybrid architecture can go too far.

Flow
  ↓
Apex
  ↓
Subflow
  ↓
Apex
  ↓
Another Flow
  ↓
Apex
Enter fullscreen mode Exit fullscreen mode

Now the architecture is fragmented again.

We started asking:

If someone needs to understand this business process, where should they look first?

If the answer is Flow, Flow should actually own the orchestration.

If the answer is an Apex trigger framework, the transaction shouldn't secretly depend on several unrelated Flows.

There needs to be a clear architectural owner.


9. High-Volume Data Exposed Weak Designs

An automation can work perfectly when one salesperson edits one record.

Then a Data Loader import arrives.

Or an integration updates several thousand records.

Now the workload looks very different.

Bulk API
   ↓
Thousands of records
   ↓
Flows
   ↓
Queries
   ↓
Loops
   ↓
Related updates
   ↓
Other automation
Enter fullscreen mode Exit fullscreen mode

Suddenly:

  • SOQL usage matters
  • DML usage matters
  • CPU time matters
  • Recursion matters
  • Downstream automation matters

This is where understanding Salesforce Flow loops best practices becomes especially useful. Collections, bulk updates, avoiding DML inside loops, and recognizing when large workloads should move toward Apex or batch processing can prevent an otherwise clean Flow from becoming a production bottleneck.

The lesson wasn't:

Flow cannot process collections.

It can.

The lesson was:

Don't validate production architecture using only one-record tests.


10. Avoid Database Work Inside Loops

One of the easiest Flow performance mistakes looks conceptually like:

Loop Records
    ↓
Update Record
    ↓
Next Record
Enter fullscreen mode Exit fullscreen mode

A healthier approach is:

Loop Records
    ↓
Modify Collection
    ↓
Finish Loop
    ↓
Update Collection Once
Enter fullscreen mode Exit fullscreen mode

The same principle exists in Apex.

Bad:

for (Contact contact : contacts) {
    update contact;
}
Enter fullscreen mode Exit fullscreen mode

Better:

for (Contact contact : contacts) {
    // modify values
}

update contacts;
Enter fullscreen mode Exit fullscreen mode

This is a useful reminder that good automation architecture matters more than whether the implementation is declarative or programmatic.

You can build inefficient Flow.

You can also build inefficient Apex.


11. Transaction Control Was a Strong Apex Signal

Some requirements eventually needed:

Savepoints
Rollback behavior
Partial success
Fine-grained exception handling
Complex transactional logic
Enter fullscreen mode Exit fullscreen mode

Those requirements made the decision easier.

Apex gives developers much more explicit transaction control.

When we genuinely needed that level of control, forcing the workflow to remain entirely declarative created more complexity than it removed.

That became another rule:

Let the requirement reveal when you've crossed the boundary into Apex.


12. Error Handling Influenced the Decision

Flow can provide useful fault paths:

Action
  ↓
Failure
  ↓
Fault Connector
  ↓
Log
  ↓
Notify
Enter fullscreen mode Exit fullscreen mode

For many business processes, that is enough.

Other scenarios require:

Record A succeeds
Record B fails
Record C succeeds
Record D needs retry
Custom user error required
Enter fullscreen mode Exit fullscreen mode

When failure handling becomes this detailed, Apex often provides cleaner control.

Again, the goal isn't picking a winner.

The goal is matching the tool to the failure model.


13. Flow Still Needs Testing Discipline

One dangerous attitude is:

It's Flow, so we'll just click Debug.

That isn't enough for production automation.

The workflow should be tested against scenarios like:

Expected record
Unexpected record
Missing related data
Bulk processing
Negative case
Permission differences
Fault path
Enter fullscreen mode Exit fullscreen mode

Think:

Define behavior
      ↓
Build Flow
      ↓
Create repeatable tests
      ↓
Exercise important paths
      ↓
Deploy
      ↓
Regression test changes
Enter fullscreen mode Exit fullscreen mode

Declarative automation is still application logic.

It deserves the same engineering discipline.


14. Apex Coverage Doesn't Guarantee Good Architecture

The opposite assumption also causes problems.

Code coverage = 90%
Enter fullscreen mode Exit fullscreen mode

doesn't mean the automation is well designed.

A test may execute the code without proving the actual business outcome.

Instead of asking:

Did these lines run?

ask:

Given:
An enterprise Opportunity above the threshold

When:
The Opportunity closes

Then:
Executive review is required
and the expected downstream behavior occurs
Enter fullscreen mode Exit fullscreen mode

Test business behavior.

Not only implementation mechanics.

That rule applies equally to Apex and Flow.


15. Entry Conditions Were One of the Cheapest Optimizations

A Flow that never needs to run is cheaper than one that runs and exits later.

Instead of:

Every Account Update
        ↓
Start Flow
        ↓
Check whether relevant fields changed
Enter fullscreen mode Exit fullscreen mode

prefer precise start criteria where possible:

Relevant change occurred?
        ↓
YES
        ↓
Run automation
Enter fullscreen mode Exit fullscreen mode

This reduced:

  • Unnecessary executions
  • Extra queries
  • Unexpected downstream actions
  • Debugging noise

One of our simplest architecture rules became:

Make the automation prove that it needs to run before doing any work.


16. Before-Save vs After-Save Matters Too

Sometimes the real decision isn't simply:

Flow or Apex?

It is:

Before-save Flow?
After-save Flow?
Flow + Invocable Apex?
Apex Trigger?
Async Apex?
Scheduled Flow?
Batch Apex?
Enter fullscreen mode Exit fullscreen mode

For simple same-record updates:

Record changes
     ↓
Calculate
     ↓
Set fields
Enter fullscreen mode Exit fullscreen mode

a before-save Flow can be an excellent fit.

For:

Create related record
Update another object
Send notification
Trigger integration
Enter fullscreen mode Exit fullscreen mode

after-save processing is usually more appropriate.

Choosing the correct execution model can matter as much as choosing the language.


What Choosing Wrong Actually Cost

The cost wasn't just additional developer hours.

Maintenance Cost

Simple requirement
+
Unnecessary Apex
=
Permanent developer dependency
Enter fullscreen mode Exit fullscreen mode

Debugging Cost

Flow
+
Trigger
+
More automation
+
No clear ownership
=
Long incident investigation
Enter fullscreen mode Exit fullscreen mode

Performance Cost

Complex Flow
+
Large record volume
+
Poor bulk design
=
Limit pressure
Enter fullscreen mode Exit fullscreen mode

Change Cost

Mega-Flow
+
Years of patches
=
Nobody wants to touch it
Enter fullscreen mode Exit fullscreen mode

Governance Cost

Multiple teams
+
No automation standard
=
Automation sprawl
Enter fullscreen mode Exit fullscreen mode

That final one was often the most expensive.


The Decision Framework We Use Now

Choose Flow When

Business logic is straightforward

Automation density is relatively low

Admins benefit from visibility

Same-record updates are simple

Cross-object logic remains manageable

Visual orchestration improves understanding

Declarative ownership has real value
Enter fullscreen mode Exit fullscreen mode

Choose Apex When

Automation density is high

Large data volumes are common

Complex data transformations are required

Maps, Sets, and algorithms improve clarity

Precise transaction control is required

Performance needs tight control

Error handling is sophisticated

A dedicated trigger framework is justified
Enter fullscreen mode Exit fullscreen mode

Choose Flow + Invocable Apex When

The business process is visually understandable

But one part contains complex computation

Flow should own orchestration

Apex can expose a small reusable capability

The organization needs both admin visibility
and developer-level control
Enter fullscreen mode Exit fullscreen mode

The point is not to minimize Apex.

It is not to maximize Flow.

It is to keep the complexity of the solution proportional to the complexity of the requirement.


Practical Flow vs Apex Checklist

Before building automation, ask:

[ ] How much automation already exists on this object?

[ ] Is this simple same-record logic?

[ ] How many related records will be touched?

[ ] What data volume must this handle?

[ ] Does the logic require complex collections?

[ ] Do Maps or Sets make the implementation clearer?

[ ] Do we need precise transaction control?

[ ] What happens when one record fails?

[ ] Who will maintain the automation?

[ ] Should administrators be able to change it?

[ ] Does the complexity actually justify Apex?

[ ] Would Flow + Invocable Apex create a cleaner boundary?

[ ] Are execution-order assumptions clear?

[ ] Are Flow and Apex competing on the same object?

[ ] Are entry criteria narrow?

[ ] Has bulk behavior been tested?

[ ] Can the important behavior be regression-tested?

[ ] Is the implementation version-controlled?
Enter fullscreen mode Exit fullscreen mode

The Biggest Lesson

Our biggest mistake in the Flow vs Apex debate was assuming one of them needed to win.

It doesn't.

Flow is not "Apex for admins."

Apex is not "Flow for developers."

They are different tools with overlapping capabilities.

Our thinking changed from:

Can Flow do this?
Enter fullscreen mode Exit fullscreen mode

to:

Should Flow own this?
Enter fullscreen mode Exit fullscreen mode

And from:

Can we implement this in Apex?
Enter fullscreen mode Exit fullscreen mode

to:

Does the requirement actually justify Apex?
Enter fullscreen mode Exit fullscreen mode

The answer can also change over time.

A Flow that is ideal today may become part of a hybrid design as automation density grows.

A complex transaction may eventually justify an Apex trigger framework.

A tiny Apex trigger written years ago may now be easier to maintain as a before-save Flow.

Changing architecture as the system changes isn't failure.

Ignoring the changing system is.

The best choice isn't Flow.

It isn't Apex.

It is the approach whose complexity, scale, ownership, testing model, and maintenance cost match the actual problem.


Top comments (0)