If there is one Salesforce architecture discussion that can become unnecessarily complicated, it is this one:
“Should we use Flow or Apex?”
I have been part of enough Salesforce design discussions to know that this question rarely has a simple answer.
One team says:
“Everything should be Flow. Salesforce is a low-code platform.”
Another says:
“For anything serious, use Apex.”
I don’t agree with either extreme.
Flow and Apex are not competing technologies where one needs to win.
They are two different tools for implementing business behavior on the Salesforce platform.
The real architecture question is:
Which implementation approach gives us the right balance of capability, performance, maintainability, governance and long-term flexibility?
That is the question I use when making this decision.
The Mistake of Choosing Technology First
One of the patterns I have seen repeatedly is starting the conversation with the technology.
For example:
“This requirement needs automation. Let’s build a Flow.”
Or:
“This is complex. Let’s write Apex.”
I prefer to reverse the process.
Start with the behavior.
Understand:
- What triggers the automation?
- What data is involved?
- How complex is the decision logic?
- How much data can be processed?
- Does the operation need to be synchronous?
- Does it need transaction control?
- Does it call external systems?
- How frequently will it execute?
- Who will maintain it?
- Is the logic expected to evolve?
- Is the behavior reusable across multiple processes?
Once those questions are answered, the technology choice becomes much easier.
My Basic Rule
If I had to reduce the decision to one sentence:
Use Flow for business automation that can be expressed clearly and safely through configuration; use Apex when the business behavior requires programmatic control, complex processing, reusable logic or capabilities that Flow cannot handle cleanly.
The important word here is cleanly.
Just because Flow can technically implement something doesn’t mean Flow is the right architecture.
Likewise, just because Apex can implement something doesn’t mean Apex should be used.
Where Flow Fits
Flow has become the default automation tool for many Salesforce requirements, and for good reason.
It provides:
- Low-code development
- Faster delivery
- Easier visibility into business logic
- Easier administration
- Strong Salesforce platform integration
- Screen-based experiences
- Record-triggered automation
- Scheduled automation
- Orchestration capabilities
- Integration actions
- Reusable subflows
For many business processes, Flow is exactly what we should use.
For example:
When an Opportunity moves to Closed Won, create the onboarding task, update the appropriate fields and notify the account team.
I would not start writing Apex for this.
A well-designed Flow is simpler, easier to understand and easier for the Salesforce team to maintain.
Where Apex Fits
Apex becomes more attractive when the requirement moves beyond straightforward orchestration.
Examples include:
- Complex algorithms
- Sophisticated transaction processing
- Large-volume processing
- Reusable domain logic
- Complex validation
- Advanced error handling
- Dynamic processing
- Complex integrations
- Custom asynchronous processing
- Logic requiring precise transaction control
- Scenarios where Flow becomes difficult to understand or maintain
For example, suppose we need to calculate a complex pricing adjustment across hundreds of related records using multiple business rules and external reference data.
Could Flow potentially do it?
Possibly.
Would I necessarily want to build it entirely in Flow?
Probably not.
At that point, Apex may provide a much cleaner implementation.
Declarative Does Not Mean Automatically Better
This is an important architectural point.
There is sometimes an assumption that:
Declarative = good
and
Code = bad
I don’t see architecture that way.
The objective is not to minimize code.
The objective is to minimize unnecessary complexity.
A 15-element Flow that requires several nested decisions, loops, assignments, subflows and workarounds may be harder to understand than a small, well-designed Apex class.
Likewise, writing 300 lines of Apex to replace a simple record update is unnecessary engineering.
The implementation should match the complexity of the problem.
The Complexity Threshold
One of my practical tests is this:
Can another architect understand what this automation does by looking at it five minutes from now?
If the answer is yes, Flow may be a good choice.
If understanding the Flow requires tracing:
- Multiple subflows
- Nested loops
- Complex formulas
- Several decision branches
- Invocable Apex
- Platform Events
- Scheduled paths
- Multiple record-triggered Flows
then I start asking whether the design has crossed the threshold where Apex would provide better structure.
This isn’t about Flow having a technical limitation.
It is about human complexity.
And human complexity is an architecture problem.
Performance Matters
Performance is another area where the decision needs more thought.
A Flow can perform very well when designed correctly.
But poor automation design can create serious performance problems regardless of whether the implementation is Flow or Apex.
For example:
- Querying inside loops
- Updating records repeatedly
- Excessive automation chaining
- Unnecessary record retrieval
- Large collections
- Recursive processing
- Multiple automations responding to the same event
These patterns can hurt performance.
The question should therefore not be:
“Is Apex faster than Flow?”
The better question is:
“What execution pattern are we creating?”
A well-designed Flow can be better than poorly designed Apex.
A well-designed Apex transaction can also be significantly more appropriate than an overly complicated Flow.
The Governor Limit Perspective
Salesforce’s multitenant architecture means that governor limits matter.
This is one of the areas where architects need to look beyond the visual simplicity of an automation.
Consider:
Record Update
|
v
Record-Triggered Flow
|
+---- Update Account
| |
| v
| Account Flow
| |
| v
| Update Contact
| |
| v
| Contact Flow
|
+---- Apex
|
+---- Platform Event
The individual components may look reasonable.
Together, they can create a very complicated transaction.
This is why I strongly believe Salesforce architecture needs an automation architecture, not just individual Flow and Apex decisions.
The Automation Landscape Matters More Than the Individual Flow
Imagine an Opportunity has:
- Flow A
- Flow B
- Apex Trigger
- Process Builder legacy automation
- Managed package automation
- Platform Event subscriber
- Integration middleware
- Another Flow updating Account
The problem isn’t necessarily any individual component.
The problem is the interaction between them.
This is where mature Salesforce architecture differs from simply building functionality.
We need to understand:
What happens when this record changes?
not just:
What does this Flow do?
Flow First Doesn’t Mean Flow Everywhere
I generally support a Flow-first mindset for appropriate business automation.
But I don’t support a Flow-only architecture.
For example:
Good Flow candidates
- Simple field updates
- Notifications
- Straightforward record creation
- Approval-related orchestration
- User interactions
- Guided screens
- Simple business decisions
- Scheduled business processes
- Basic record synchronization
Strong Apex candidates
- Complex calculations
- Large-volume processing
- Sophisticated transaction logic
- Reusable business services
- Complex data transformations
- Advanced integrations
- Specialized asynchronous processing
- Logic requiring programmatic abstraction
The important point is that these are architectural tendencies, not absolute rules.
Maintainability Is Often the Deciding Factor
This is where I personally place a lot of weight.
The first version of an automation is rarely the problem.
The fifth modification is.
Imagine a Flow built today by a project team.
Six months later:
- A new business rule is added.
- Another exception appears.
- Another team needs to reuse the logic.
- A new integration is introduced.
- A new field becomes mandatory.
- A new product changes the process.
The Flow grows.
Eventually someone looks at it and says:
“Who built this?”
Then someone else says:
“Nobody knows. But don’t touch it because it works.”
That is a warning sign.
Good architecture should make change easier, not more dangerous.
The “God Flow” Anti-Pattern
One of the patterns I actively try to avoid is the giant Flow.
It starts innocently.
Opportunity Flow
|
+--- Update Account
|
+--- Create Tasks
|
+--- Send Email
|
+--- Update Contact
|
+--- Calculate Discount
|
+--- Call External API
|
+--- Create Case
|
+--- Publish Event
|
+--- More Decisions
|
+--- More Loops
Eventually, the Flow becomes the application.
That’s when I become uncomfortable.
A Flow should orchestrate business behavior.
It shouldn’t become a visual replacement for an entire application architecture.
The “Apex for Everything” Anti-Pattern
The opposite is equally problematic.
I have seen teams create Apex for requirements such as:
“When a Case is created, set Priority to High when the customer is marked VIP.”
That doesn’t require a sophisticated Apex framework.
A simple Flow may be the better solution.
Every unnecessary Apex implementation creates additional:
- Code
- Testing requirements
- Deployment complexity
- Developer dependency
- Maintenance overhead
The objective is not to write more code.
The objective is to build the right architecture.
Flow + Apex Is Often the Best Answer
The most useful discussion is sometimes not:
Flow OR Apex
but:
Flow AND Apex
For example:
Business Event
|
v
Flow
|
+---- Simple Decisions
|
+---- Standard Updates
|
+---- User Interaction
|
+---- Invoke Apex
|
v
Complex Business Logic
|
v
Return Result
|
v
Flow
This can provide a clean separation.
Flow handles orchestration.
Apex handles specialized business logic.
This is one of the patterns I find particularly useful when designed carefully.
Invocable Apex: Powerful but Easy to Abuse
Invocable Apex provides a bridge between declarative and programmatic development.
It can be useful when:
- Flow needs functionality that requires Apex
- A complex operation needs encapsulation
- Logic needs to be reused
- External processing is required
But there is a danger.
If every few elements in a Flow call a different Apex action, the Flow becomes difficult to reason about.
At that point, we have simply moved complexity around.
The goal should be clear boundaries, not mixing technologies randomly.
Transaction Boundaries Matter
This becomes particularly important for architects.
Suppose a business transaction performs:
- Update Opportunity
- Update Account
- Create Order
- Call external system
- Create fulfillment record
- Send notification
Do we want everything to happen synchronously?
Should some operations be asynchronous?
What happens if step 4 fails?
Should steps 1–3 roll back?
Should the external call be retried?
These aren’t Flow-versus-Apex questions.
They are transaction architecture questions.
Both Flow and Apex need to fit into the larger transaction model.
When Performance Becomes an Architecture Concern
A simple automation may process ten records today.
But what happens when:
- Data migration loads 500,000 records?
- Integration sends 20,000 updates?
- A bulk update happens?
- A nightly job processes millions of records?
This is where architects need to think beyond the happy path.
I always ask:
What is the expected transaction volume?
And:
What is the worst reasonable volume?
An automation designed for 10 records should not automatically be assumed to be suitable for 100,000 records.
Bulkification Is Not Just an Apex Concept
Bulkification is often associated with Apex, but the architectural principle applies to Flow as well.
A good design should minimize:
- Queries
- DML operations
- Repeated processing
- Unnecessary loops
- Recursive automation
The question isn’t whether the tool is declarative or programmatic.
The question is whether the implementation respects Salesforce’s execution model.
Error Handling
This is another area where I often see a difference between a prototype and an enterprise solution.
What happens when automation fails?
A mature design should consider:
- Error capture
- Logging
- Retry
- User messaging
- Operational monitoring
- Partial failure
- Integration failure
- Notification
- Supportability
A Flow with no error handling may work perfectly until the first unexpected condition.
Apex doesn’t automatically solve this either.
Error handling needs to be deliberately designed.
Debugging and Observability
When something goes wrong in production, someone needs to answer:
What happened?
A good architecture makes that question answerable.
For critical automation, consider:
- Structured logging
- Correlation IDs
- Error records
- Platform monitoring
- Transaction tracing
- Integration monitoring
- Meaningful fault paths
This becomes especially important when Flow and Apex interact.
The user doesn’t care whether the failure happened in Flow or Apex.
They care that the business transaction failed.
Ownership and Skills
There is another dimension that is sometimes ignored:
Who will maintain this after the project team leaves?
If your Salesforce organization has a strong administrator and declarative development team, Flow may provide significant long-term advantages.
If the organization has a mature engineering team and sophisticated development practices, Apex may be easier to govern for complex application behavior.
Architecture needs to consider the operating model.
The technically elegant solution isn’t necessarily the most sustainable solution for the organization.
A Practical Decision Matrix
| Requirement | Flow | Apex |
|---|---|---|
| Simple automation | Excellent | Usually unnecessary |
| Screen-based experience | Excellent | Usually unnecessary |
| Straightforward record updates | Excellent | Usually unnecessary |
| Simple branching logic | Excellent | Usually unnecessary |
| Complex algorithms | Limited | Excellent |
| Highly reusable business logic | Moderate | Excellent |
| Large-volume processing | Requires careful design | Strong fit |
| Advanced transaction control | Limited | Strong |
| Complex integrations | Possible | Strong |
| Administrator maintainability | Strong | Lower |
| Developer maintainability | Strong for simple logic | Strong for complex logic |
| Complex error handling | Moderate | Strong |
| Long-term extensibility | Depends on design | Strong for complex domains |
The word “depends” appears frequently in this table intentionally.
Architecture rarely benefits from absolute rules.
My Flow vs Apex Decision Tree
When I am reviewing a design, I usually walk through something like this:
Business Requirement
|
v
Is the logic simple?
/ \
Yes No
| |
v v
Flow Can Flow express it
cleanly and clearly?
/ \
Yes No
| |
v v
Flow Apex
|
v
Reusable / Complex
Business Logic
Then I add another question:
What happens at scale?
That can change the answer.
And finally:
Who will maintain it?
That can change it again.
A Real-World Example
Consider a customer onboarding process.
The requirement is:
- Opportunity becomes Closed Won.
- Create onboarding tasks.
- Update Account status.
- Notify the implementation team.
- Calculate a complex implementation score.
- Call an external provisioning platform.
- Retry provisioning if it fails.
- Record the provisioning response.
- Start downstream processes.
I would not build this as one massive Flow.
I might design it like this:
Opportunity Closed Won
|
v
Flow
|
+---- Create Tasks
|
+---- Update Account
|
+---- Notify Team
|
+---- Invoke Apex
|
+---- Calculate Score
|
+---- Provisioning Service
|
+---- Error Handling
|
+---- Retry Strategy
|
v
Return Result
|
v
Flow
|
v
Continue Process
The Flow owns orchestration.
The programmatic layer owns the complex operation.
The integration architecture owns external communication patterns.
That separation makes the solution much easier to evolve.
What I Would Avoid
I would avoid these patterns:
1. Flow because “admins can maintain it”
If the Flow is already too complicated for administrators to understand, that argument no longer holds.
2. Apex because “it is enterprise”
Code does not automatically make an architecture enterprise-grade.
3. One Flow per object without understanding interactions
Multiple Flows can still create a complicated automation graph.
4. One giant Flow
A Flow should not become the entire application.
5. Apex trigger calling dozens of services synchronously
That can create transaction, performance and operational problems.
6. Mixing business logic everywhere
If the same business rule exists in:
- Flow
- Apex
- Validation Rules
- Integration middleware
- External applications
then ownership becomes unclear.
7. Ignoring bulk scenarios
A solution that works perfectly for one record can fail badly under integration or data-load volumes.
The Bigger Architectural Principle
Flow versus Apex is actually a small part of a much larger question:
Where should business logic live?
Some logic belongs in Salesforce.
Some belongs in integration services.
Some belongs in external systems.
Some belongs in configuration.
Some belongs in data architecture.
For example:
Business Capability
|
+----------------+----------------+
| | |
v v v
Configuration Flow Apex
|
v
Salesforce Logic
|
+---------+---------+
| |
v v
Platform Integration
Events Layer
|
v
External Systems
The mistake is not choosing Flow.
The mistake is allowing business logic to spread across the landscape without clear ownership.
My Rule of Thumb
My personal preference is:
Start declarative.
Then challenge the design.
Ask:
- Is it still readable?
- Is it performant?
- Is it reusable?
- Is it testable?
- Is it scalable?
- Is it observable?
- Can another team maintain it?
- Can the business change it safely?
If the answer remains yes, keep it in Flow.
If the design starts becoming complicated, don’t keep adding Flow elements just to avoid Apex.
Move the complexity into the right abstraction.
That might be Apex.
It might be an asynchronous pattern.
It might be an integration service.
It might even mean redesigning the business process.
Final Thought
I don’t believe good Salesforce architecture is about being “pro-Flow” or “pro-Apex.”
It is about being pro-clarity.
Flow has made Salesforce automation dramatically more accessible, and I would absolutely use it where it provides the right balance of simplicity and capability.
But I would not use Flow simply because it avoids code.
Likewise, Apex remains one of the most powerful tools available to a Salesforce architect, but I would not introduce code where a simple Flow communicates the business intent better.
The best architecture is usually the one where you can look at the solution six months later and still understand why the logic is where it is.
For me, that is the real test.
Use Flow to orchestrate. Use Apex to encapsulate complexity. Use the platform deliberately. And most importantly, make sure the business logic has a clear home.