Skip to content

Solution Architecture Best Practices

Best practices and anti-patterns for solution architecture in Salesforce CTA scenarios. These patterns emerge from real-world enterprise implementations and are the kind of practical wisdom the review board expects a CTA to demonstrate.

Design Principles

1. Declarative First, Programmatic When Necessary

Use platform capabilities before writing code. This is not dogma — it is a pragmatic principle that reduces maintenance burden and increases the pool of people who can support the solution.

When to break this principle: When declarative solutions create more complexity than code (e.g., a Flow with 50 Decision elements and 200 elements is harder to maintain than 100 lines of well-tested Apex).

2. One Automation Per Object

Consolidate all record-triggered automations into a single Flow per object per trigger context (before-save, after-save). Use subflows for logical separation.

Why: Predictable execution order, shared governor limit awareness, easier debugging, and single point of documentation.

3. Separation of Concerns

Separate business logic from UI logic from integration logic. In Apex, this means:

  • Service classes for business logic (reusable across triggers, batch, API)
  • Selector classes for SOQL queries (centralized, optimized)
  • Domain classes for trigger logic (object-specific behavior)
  • Controller classes for UI logic (LWC wire adapters, Aura controllers)
flowchart TD
    subgraph UI["UI Layer"]
        LWC[LWC Components]
        Aura[Aura Components]
    end
    subgraph Controllers["Controller Layer"]
        LC[Lightning Controllers]
        API[REST/SOAP APIs]
    end
    subgraph Services["Service Layer"]
        SVC[Service Classes]
    end
    subgraph Domain["Domain Layer"]
        DOM[Domain / Trigger Handlers]
        SEL[Selector Classes]
    end
    subgraph Platform["Platform"]
        DB[(Salesforce Objects)]
    end
    LWC --> LC
    Aura --> LC
    API --> SVC
    LC --> SVC
    SVC --> DOM
    SVC --> SEL
    DOM --> DB
    SEL --> DB

4. Configuration Over Customization

Use Custom Metadata Types, Custom Settings, and Custom Labels for values that change across environments or over time. Never hardcode IDs, URLs, thresholds, or business rules.

5. Design for Bulk

Every piece of automation — whether Flow or Apex — must handle bulk operations. A solution that works for one record but fails for 200 is not a solution.

6. Fail Gracefully

Every automation should have error handling. Flows need Fault paths. Apex needs try-catch blocks. Integrations need retry logic and dead-letter queues.

Architecture Patterns

Pattern: Invocable Action Bridge

Problem: Complex business logic needs to be accessible to both Flows and Apex code.

Solution: Implement the logic as an @InvocableMethod Apex class, callable from Flow and from other Apex.

When to use: When business logic is too complex for Flow but needs to be accessible to admins through Flow Builder.

Pattern: Platform Event Decoupling

Problem: Synchronous processing in a user transaction causes performance issues or governor limit pressure.

Solution: Publish a Platform Event from the user transaction, and handle the complex processing in a Platform Event-Triggered Flow or Apex trigger.

When to use: Integration callouts, complex calculations, non-critical updates that can be eventually consistent.

sequenceDiagram
    participant U as User Transaction
    participant PE as Platform Event Bus
    participant S as Subscriber (Flow/Apex)
    participant EXT as External System
    U->>U: Save record
    U->>PE: Publish event
    U-->>U: Transaction completes (fast)
    PE->>S: Deliver event (async)
    S->>EXT: Callout / complex logic
    EXT-->>S: Response
    S->>S: Update related records

Pattern: Custom Metadata Configuration

Problem: Business rules change frequently and vary by business unit, region, or product line.

Solution: Store business rules in Custom Metadata Types. Automation reads from metadata rather than hardcoded values. Changes deploy through metadata, not code changes.

When to use: Routing rules, approval thresholds, integration endpoints, feature flags.

Pattern: Apex Enterprise Patterns

Problem: Large-scale Apex development becomes unmaintainable without structure.

Solution: Adopt the Apex Enterprise Patterns (fflib):

  • Unit of Work for transactional DML
  • Selector Layer for SOQL
  • Domain Layer for trigger logic
  • Service Layer for business logic

When to use: Enterprise-scale orgs with dedicated development teams and complex business logic.

CTA Signal

Recommending Apex Enterprise Patterns shows the review board that you think about long-term maintainability, not just solving today’s problem. But only recommend it when the org’s complexity warrants it — it is over-engineering for a simple org.

Anti-Patterns

Anti-Pattern: The God Flow

What it looks like: A single Flow with 500+ elements that handles every possible scenario for an object.

Why it is bad: Impossible to debug, test, or modify without risk of breaking unrelated logic. Approaches the 250K element limit quickly.

Fix: Break into a master Flow with subflows. Each subflow handles one logical concern.

Anti-Pattern: Apex Everywhere

What it looks like: Every requirement is implemented in Apex, even simple field updates and validations.

Why it is bad: Creates developer dependency for all changes. Increases deployment complexity. Ignores platform capabilities.

Fix: Evaluate each requirement against the decision guides. Use Apex only when the decision framework clearly points to it.

Anti-Pattern: AppExchange Without Evaluation

What it looks like: Installing AppExchange packages without a formal evaluation, TCO analysis, or exit strategy.

Why it is bad: Creates vendor lock-in, unknown governor limit impact, data model pollution, and upgrade risk.

Fix: Use the vendor evaluation scorecard for every AppExchange decision.

Anti-Pattern: Hardcoded Everything

What it looks like: Record Type IDs, profile IDs, queue IDs, and endpoint URLs hardcoded in Apex or Flow.

Why it is bad: Breaks during deployment to different environments. Creates hidden dependencies.

Fix: Use Custom Metadata Types for configurable values. Use Schema.SObjectType describes for IDs. Use Named Credentials for endpoints.

Anti-Pattern: No Error Handling

What it looks like: Flows without Fault paths. Apex without try-catch. Integrations without retry logic.

Why it is bad: Users see cryptic error messages. Data ends up in inconsistent states. Integration failures go unnoticed.

Fix: Every Flow gets a Fault path that logs errors and shows user-friendly messages. Every Apex class gets try-catch with structured error logging. Every integration gets retry logic with alerting.

Anti-Pattern: Legacy Automation Soup

What it looks like: A mix of Workflow Rules, Process Builder, and Flow on the same objects. Some objects have all three plus Apex triggers.

Why it is bad: Unpredictable execution order. Governor limit overrun. Impossible to debug.

Fix: Consolidate all automation to Flow + Apex triggers. Workflow Rules and Process Builder are deprecated — migrate systematically.

Checklist: Solution Architecture Review

Before finalizing a solution architecture in a CTA scenario, verify:

  • Every automation has a clear owner (admin vs developer)
  • Governor limits have been estimated for peak transaction scenarios
  • Error handling exists for every automation and integration
  • No hardcoded IDs, URLs, or business rules
  • One automation per object per trigger context
  • AppExchange packages have been evaluated with the vendor scorecard
  • Exit strategies exist for all third-party dependencies
  • The solution accounts for bulk data operations (data loader, integration bulk loads)
  • Testing strategy covers both declarative and programmatic components
  • Performance has been considered for the largest transaction scenarios

Sources