The long-term stability of your Salesforce implementation is determined before you implement anything.
It begins with an architecture that serves the business and works for the developers responsible for building and extending it. An architecture created for developers is destined to fail.
Despite the popular assumption that architecture should be defined through technical elegance and maintainability, you will achieve neither by neglecting the business you are building for.
My architecture playbook consists of three decisions:
- Topology: Where should each part of the solution live?
- Separation: What should each part own and be responsible for?
- Framework: How should teams implement those parts consistently?
Get these three decisions right, and a change in one capability should not require full regression testing of everything around it.
The Right Topology
Topology depends on whom we are building the Salesforce implementation for, how many business units or products are involved, and how much coordination the business expects between them.
At this point, do not dive into technical details. I need you to forget about SObjects, Apex, Agentforce, Platform Events and everything else that makes you feel productive.
Your first goal is to understand what the business actually does.
One of the most common requests you will hear is:
We'd like to keep everything in one Salesforce org so that all our teams can easily coordinate with each other.
“Coordinate with each other” may be the most misleading phrase you will hear during architecture planning. That simplified coordination is exactly what can make your days, nights and weekends so enjoyable.
The truth is that the topology decision rests on two elephants:
- Compliance
- Boundaries
Compliance
Start by identifying whether the implementation contains highly regulated data or processes. This may include PII, financial data, health information, confidential documents or anything governed by strict access, audit, retention, residency or operational requirements.
Then ask whether those capabilities should live in the same environment as rapidly evolving areas such as Sales, Marketing or Customer Support.
Compliance does not automatically require another Salesforce org. Sometimes a strong access model, encryption (hi shield) and logical separation inside one org are good enough.
In other cases, the regulatory requirements and operational ownership are so strict that a separate org becomes the only choice you have.
That said, even when regulations do not explicitly demand physical separation, mixing heavily regulated capabilities with rapidly changing ones will slow delivery and make the organization unnecessarily complicated.
In my experience, teams end up creating an entire custom structure only to accommodate one regulated capability.
For reference – external file storage (AWS, GCP). Regular files and files containing regulated personal data may require completely different access controls, retention policies, deletion processes, audit histories and storage locations. Eventually, you create separate storage, separate permissions, separate automation and separate operational procedures.
At some point, you should ask whether forcing them into the same architecture is still simplifying anything.
Boundaries
The second elephant is boundaries.
Two separate organizations naturally provide stronger physical boundaries. They also introduce more integrations, duplicated administration, separate deployments and additional support effort.
That brings us to the real question:
Can a single Salesforce org have clear boundaries?
I say it can.
It depends on the business and on how many independent products, units or capabilities the org must support.
Ask yourself:
If I change Product A, will I need to perform regression testing on Product B?
If the answer is no, ask the more difficult question:
Can our development team keep it that way as both products continue to evolve?
Whether we like it or not, architecture is not only about the business or the architect. It is also about the people who will maintain the solution.
Sometimes it is not about the brilliance of your diagram. It is about the developers sitting at the desks around you who keep interrupting you.
A boundary that exists only in an architecture document is not a boundary.
Understanding whether your organization can preserve those boundaries is part of being a good architect. If the boundaries cannot be protected inside one org, you must return to the business and explain the real cost of keeping everything together:
- coupling between unrelated capabilities
- shared-code collisions
- larger regression cycles
- coordinated releases
- growing operational risk
When that cost becomes greater than the cost of splitting the solution, the solution should be split.
Compliance and boundaries are not the only topology considerations. People will throw such phrases at you as Inverse Conway Maneuver, cognitive load and fracture-plane criteria.
Those concepts are good, yet my practical position is simpler:
If compliance is manageable and the boundaries are clear and enforceable, you can build an excellent architecture inside a single Salesforce org.
The Right Separation
Once the topology is understood, the next step is separation.
I prefer to explain this stage through the Separation of Concerns, or SoC principle. SoC means dividing a solution into distinct parts, with each part addressing a separate concern.
Many developers already apply this principle at the implementation level. They ask:
- Should querying be placed in a selector?
- Should email delivery have its own service?
- Should price calculation live in a separate class?
Those are valid applications of SoC, but they are too detailed for this stage.
At this point, forget about all of this.
Instead, separate the operational concerns of the business by asking:
Which teams and capabilities are involved, and what does each of them own?
Your initial capability may include:
Customer Management
Human Resources
Customer Support
Billing
Document ManagementThis is also the moment to identify external providers that support those capabilities.
For example:
Billing -> Stripe
Human Resources -> Workday
Document Management -> Conga ComposerThe provider itself does not necessarily define your capability. Stripe may process payments, but your Billing capability must still own the business meaning of an invoice, a credit or an outstanding balance.
Once the capabilities are identified, answer the following questions:
- Which business responsibilities belong to each capability?
- What data does each capability own?
- Which capability may modify which records?
- Who is the business & technical owner?
- What behavior does the capability expose to others?
- What information does it receive from others?
- What outcomes does it publish back?
Pay particular attention to the idea of exposing behavior.
The goal is to define each capability so that it can operate independently. Other capabilities should request its behavior instead of reaching into its internal data and implementing its rules themselves.
I have seen the opposite many times:
A support user updates a customer’s phone number and receives an error such as:
This customer cannot be updated while an invoice is being processed.
The support user has nothing to do with invoicing. They did not modify an invoice, initiate a payment or request a billing adjustment. And yet, a Billing concern has leaked into an unrelated operation and blocked it.
You may argue that this problem should be addressed later through the framework and code. No, it should not.
It should be addresses right now. If the capability separation is wrong, no framework will rescue the implementation without forcing you to reconsider the separation first.
Defining a Billing Capability
Suppose we are defining Billing.
Its purpose might be:
Manage invoices, charges, credits, outstanding balances and payment status.
It may own:
Invoice
Invoice Line
Billing Account
Credit Adjustment
Payment StatusIts business owner may be:
Finance OperationsIt may expose operations such as:
Create Invoice
Issue Credit
Cancel Draft Invoice
Get Outstanding BalanceIt may publish outcomes such as:
Invoice Issued
Credit Issued
Invoice Cancelled
Payment Status ChangedAt this stage, we are not deciding whether Billing uses:
- Layered Architecture
- Platform Events
- Apex or Flow
- Selectors
We are defining what Billing is.
The resulting capability boundary may look like this:
Billing
├── Invoice Management
├── Credit and Adjustment Management
├── Billing Account Management
└── Payment Status TrackingThis tells us what belongs inside the Billing boundary. It also helps us recognize what does not belong there.
This is one of the most important parts of architecting a solution.
Good architects identify components. Great architects identify boundaries, ownership and the consequences of crossing them.
The Right Framework
Once the capability boundaries are defined, the framework answers the next question:
How will teams consistently implement each capability without destroying the boundaries we just created?
A framework is not a collection of abstract classes, selectors, services and impressive naming conventions.
It is an implementation model.
It tells developers how a request enters a capability, where orchestration happens, where business decisions live, how data is accessed, how external systems are called, how transactions are controlled, and which dependencies are allowed.
A generic execution model might look like this:
Entry Point
↓
Application Operation
↓
Business Rules
↓
Data or External SystemsIn Salesforce, an entry point may be:
LWC
Flow
Trigger
Scheduled Job
Invocable ApexThe entry point should translate the incoming request and invoke an application operation.
For example:
Issue Invoice
Approve Credit
Close Case
Confirm Order
Generate DocumentThe application operation coordinates the use case. It loads the required information, invokes business rules, manages the transaction, persists the result and communicates the outcome.
The business rules decide what is allowed.
The data and integration boundaries handle technical work such as SOQL, DML and external APIs.
This sounds fancy, but the truth is that the exact names don't matter at all. You can call them services, use cases, domains, handlers, repositories, selectors or gateways.
What matters is that their responsibilities are understood and consistently applied.
Framework Decisions
When I define a framework, I want it to answer questions such as:
- What is an entry point allowed to do?
- Where does business orchestration happen?
- Where do business rules live?
- Who controls the transaction?
- Where are SOQL and DML performed?
- How do capabilities communicate?
- How are security and permissions enforced?
- How are errors translated and logged?
- How are dependencies replaced during testing?
- How do we extend behavior without filling the solution with
if-elseconditions?
Applying the Framework to Billing
Suppose another capability needs Billing to issue an invoice.
From the outside, the interaction begins with an operation exposed by Billing:
Request InvoiceThat operation represents the capability contract. Sales may request an invoice, but it should not create Invoice__c records, calculate invoice totals or decide whether an order is eligible for billing.
The technical entry point depends on how the request reaches Salesforce. It could be: Apex Controller or Flow Action or Trigger Handler.
For example:
public with sharing class BillingController {
@AuraEnabled
public static InvoiceResult requestInvoice(Id orderId) {
return BillingApplication.requestInvoice(orderId);
}
}The controller does not contain Billing logic. It receives the request and passes it to the application operation responsible for the use case.
public with sharing class BillingApplication {
public static InvoiceResult requestInvoice(Id orderId) {
Order orderRecord = OrderSelector.selectById(orderId);
InvoiceDraft draft = InvoiceDomain.createInvoice(orderRecord);
Invoice__c invoice = InvoiceRepository.doInsert(draft);
BillingEventPublisher.publishInvoiceIssued(invoice);
return InvoiceResult.fromRecord(invoice);
}
}The implementation flow now becomes more concrete:
BillingController.requestInvoice()
↓
BillingApplication.requestInvoice()
↓
InvoiceDomain.createInvoice()
↓
InvoiceRepository.doInsert()
↓
InvoiceIssued EventEach part has a specific responsibility.
The entry point translates the external request. The application operation coordinates the use case. The domain component applies Billing rules. The repository handles persistence. The event publisher communicates the completed outcome.
The eligibility and calculation rules could look something like this:
public class InvoiceDomain {
public static InvoiceDraft createInvoice(Order orderRecord) {
if (orderRecord.Status != 'Activated') {
throw new BillingException(
'Only activated orders can be invoiced.'
);
}
if (orderRecord.TotalAmount <= 0) {
throw new BillingException(
'An invoice total must be greater than zero.'
);
}
return new InvoiceDraft(
orderRecord.Id,
orderRecord.AccountId,
orderRecord.TotalAmount
);
}
}The important part is not the class names. Another framework may call these components services, use cases, domains, selectors, repositories or handlers.
What matters is that the responsibilities remain separate.
A Flow action, trigger handler or event subscriber should still invoke the same application operation:
BillingApplication.requestInvoice(orderId);The entry point may change. The Billing rules should not.
External Payment Providers
The same rule applies when Billing communicates with an external payment provider.
Billing should understand business operations such as:
Authorize Payment
Capture Payment
Refund PaymentIt should not spread Stripe-specific request objects, status codes and API terminology throughout the capability.
Instead, the framework can define an interface owned by Billing:
public interface PaymentGateway {
PaymentResult capturePayment(
Decimal amount,
String currencyCode,
String paymentReference
);
}A Stripe-specific adapter implements that interface:
public class StripePaymentGateway implements PaymentGateway {
public PaymentResult capturePayment(Decimal amount, String currencyCode, String paymentReference) {
StripeCaptureRequest request = new StripeCaptureRequest();
request.amount = amount;
request.currency = currencyCode;
request.reference = paymentReference;
StripeCaptureResponse response = StripeClient.capturePayment(request);
return new PaymentResult(
response.isSuccessful,
response.transactionId,
response.failureMessage
);
}
}The Billing operation depends on the business-facing interface rather than directly on Stripe:
public class PaymentApplication {
private PaymentGateway gateway;
public PaymentApplication(PaymentGateway gateway) {
this.gateway = gateway;
}
public PaymentResult captureInvoicePayment(Invoice__c invoice) {
return gateway.capturePayment(
invoice.Balance__c,
invoice.CurrencyIsoCode,
invoice.Id
);
}
}The dependency now looks like this:
Billing Operation
↓
PaymentGateway
↓
StripePaymentGateway
↓
Stripe APIBilling understands the business operation. The adapter understands Stripe.
That distinction matters.
Without it, external API terminology, response formats, authentication logic and provider-specific behavior eventually spread throughout the capability. Replacing Stripe then becomes less of an integration change and more of an organizational crisis.
A good framework does not avoid technical complexity. It gives that complexity an explicit place to live.
The Right Guardrails
- Triggers do not contain business logic.
- UI controllers do not orchestrate complete business processes.
- Capabilities do not directly modify another capability’s owned data.
- Business rules do not depend on a specific UI or integration.
- External providers are accessed through explicit boundaries.
- All collection-based entry points must remain bulk-safe.
FFLIB, AWAF and similar approaches can help implement parts of this model. They provide patterns, abstractions and conventions that prevent every team from inventing its own structure.
But choosing a framework should never begin with:
Should we use FFLIB?
It should begin with:
What implementation behavior must be consistent across our solution?
Only then can you decide whether an existing framework supports your model, whether it must be adapted or whether a simpler custom structure is enough.
At the end of the day, always keep it simple.
Comments
Join the discussion
Subscribe or sign in to leave a comment on this article.