Patterns

Patterns and the anti-patterns they replace

12 architecture patterns for CRM and CPQ systems, each paired with the specific anti-pattern it replaces, from recursion guards to least-privilege sharing. Filter by area or search.

PAT-01

Transaction-scoped recursion guard

A guard that stops an automation from re-firing on its own write, scoped to the transaction and keyed to what actually changed, so a save runs the logic once instead of looping into the governor limit.

WhenAny after-save flow or Apex trigger that updates a field on the same object it fires on, or touches a related record whose automation writes back. The tell is intermittent CPU-limit save failures that only appear under bulk load.

    Do it right
  1. In Apex, a static Boolean or a static Set of Ids in a helper class; statics persist for the transaction and reset between transactions.
  2. In Flow, guard with entry criteria (ISCHANGED on the driving field, plus a check that the field is not already at its target value), since Flow has no static variable.
  3. Gate on the field that actually changed, not a blunt already-ran flag, so legitimate later updates in the same save still process.
  4. Keep same-record field writes in before-save, which never triggers a second save and cannot recurse.

The point of the guard is to run the logic exactly once per record per save, not once per transaction; a boolean answers the wrong question and drops records, and misplacing the logic means you are guarding a problem you created by choosing the wrong timing.

PAT-02

Before-save vs after-save placement

A placement rule that puts same-record field writes in before-save and everything else (related-record DML, record creation, callouts) in after-save, so you spend zero extra DML on field stamping and never reference an Id that does not exist yet.

WhenEvery record-triggered flow and trigger. Decide placement before you build, not after the save starts failing on volume or throwing null-Id errors.

    Do it right
  1. Same-record field updates go before-save: no extra DML, the value is set before validation rules evaluate, and it is free.
  2. Cross-object writes, record creation, email, and anything needing the record Id go after-save.
  3. Before-save on insert has no record Id yet; if you need the Id, you are in after-save.
  4. Route callouts through after-save into an async path (Queueable or future); you cannot call out from before-save.

Before-save runs before the record is written and after-save runs after; putting field writes in the wrong context either wastes the transaction budget and invites recursion, or references a record and Id that do not exist yet.

PAT-03

Declarative first, Apex only when it earns it

A build-layer decision that reaches for a validation rule or Flow by default and escalates to Apex only for logic a flow genuinely cannot express bulk-safely, so the org stays admin-maintainable and Apex debt stays small.

WhenEvery new automation requirement, and any time you inherit an org where Apex was the reflex. Salesforce has retired Workflow Rules and Process Builder for new creation, so the real choice is Flow versus Apex.

    Do it right
  1. Validation rule when a formula can express the check; it gates bad data at the save with no code.
  2. Flow for the bulk of business logic: field updates, record creation, routing, notifications.
  3. Apex only for heavy bulk, complex branching that gets unreadable in Flow, callouts with retry and error handling, and fine-grained recursion control.
  4. Every Apex trigger should have a written reason a flow could not do the job; if it does not, it is debt.

Declarative is cheaper to change and readable in one place, but a flow that has outgrown what Flow expresses cleanly is its own trap; the goal is the right layer for the logic, not declarative for its own sake.

PAT-04

Declarative rollup over a custom trigger

An aggregation built on a native roll-up summary field where the relationship allows it, and a declarative rollup (DLRS or a well-guarded flow) where it does not, so Apex triggers are reserved for volumes and logic that genuinely need them.

WhenAny time a parent needs a count or sum of children (open pipeline on the Account, line total on the Quote, case count on the Contact).

    Do it right
  1. Native roll-up summary field first; it recalculates on the platform and needs no maintenance, but requires a master-detail relationship.
  2. On a lookup relationship, use a declarative tool like DLRS or a bulk-safe flow rather than jumping straight to Apex.
  3. Reserve Apex trigger rollups for high volume, cross-object math, or conditions the declarative tools cannot express.
  4. Whatever the mechanism, it must survive a 200-record bulk update to the children.

Every rollup mechanism you pick becomes standing code to maintain and a load-time cost; the native and declarative options push that cost onto the platform, and skipping them for a trigger is how a simple sum becomes a governor-limit incident.

PAT-05

Bulkify every automation

A build discipline that keeps all SOQL and DML out of loops, queries related data once into maps, and commits collections in a single DML, so an automation that works on one record survives the 200-record batch a data load will eventually send.

WhenEvery trigger and record-triggered flow, without exception. The record volume that breaks a non-bulk-safe automation is a matter of when, not if.

    Do it right
  1. No SOQL and no DML inside a loop; query once, build a Map of Id to record, then iterate in memory.
  2. Operate on collections and issue one DML per object after the loop.
  3. In Flow, use Get Records and a loop that collects into a record variable, then a single update element outside the loop.
  4. Test at 200 records before calling it done, because triggers process in batches of 200.

Governor limits are per-transaction, not per-record, so anything inside a loop multiplies against batch size; a single-record test proves nothing about the load that will actually run it.

PAT-06

Selective SOQL on indexed fields

A query design that filters on indexed, selective fields so the database seeks instead of full-scanning, keeping list views, reports, and trigger queries fast as the object grows past a million rows.

WhenAny query against a large object (Opportunity, Task, a high-volume custom object), and mandatory once a trigger queries an object that will cross the large-data-volume threshold, where a non-selective query throws at runtime.

    Do it right
  1. Filter on indexed fields: Id, Name, unique External Id, lookup and master-detail fields, and audit fields like CreatedDate.
  2. Keep the filter selective: a standard index wants under ~30% of the first million rows (custom indexes are stricter, roughly 10%), not most of the table.
  3. Avoid the killers: leading-wildcard LIKE, not-equals, NOT, and negative filters, none of which use an index.
  4. Request a custom index from Salesforce support for a high-selectivity field that is not indexed by default; consider skinny tables for wide read-heavy objects.

Selectivity is judged against real row counts, so a query that seeks in a small sandbox scans in a large production org; the exception fires where the data actually lives, not where you tested.

PAT-07

Async for heavy work

A pattern that offloads heavy or slow work (large recalcs, callouts, chained processing) off the save transaction into Queueable, Batch, or future Apex, which run with higher governor limits and do not make the user wait for the save.

WhenCallouts (which cannot run synchronously from a trigger), processing that exceeds the synchronous SOQL or CPU budget, and anything operating over more records than a single transaction can hold.

    Do it right
  1. Queueable for chainable, stateful async work with higher limits than a synchronous transaction; the modern default over future methods.
  2. Batch Apex for large data volumes, processing in chunks (default 200) across many transactions, up to tens of millions of records.
  3. Future methods only for simple fire-and-forget with primitive parameters and no chaining need.
  4. Callouts always async (future or Queueable), since a trigger cannot make a synchronous callout.

Async trades the save blocking behavior for eventual consistency; without a monitored fault path you have swapped a visible save failure for an invisible one, which is worse because every dashboard stays green.

PAT-08

Archive cold data for volume

A data-lifecycle pattern that moves cold, high-volume records (old tasks, closed cases, aged activity) off the transactional object into a Big Object or an off-platform archive, keeping the working object lean so queries, reports, and automation stay fast.

WhenAny object growing into the millions of rows where reports slow, list views time out, and triggers start throwing non-selective query errors, usually Task, Event, closed Case, or a high-volume custom log object.

    Do it right
  1. Define a retention policy: what is cold, how long the working object keeps it, where it goes.
  2. Big Objects for on-platform archive of billions of rows, queried through their composite index in prefix order (not arbitrary filters).
  3. Off-platform archive to the warehouse plus a delete on the transactional object where you do not need it in Salesforce at all.
  4. A scheduled, idempotent archive job keyed on a stable id, so a rerun does not double-write.

A Big Object you can write to but not query the way you need is a dead end, because the index is set at creation and cannot be reshaped later; the query pattern has to be designed before the first row lands.

PAT-09

Record type per process, not per variation

A modeling rule that creates a record type only when a genuinely different process needs its own picklist values, page layout, or sales/support process, and uses a picklist to categorize when the process is identical.

WhenAdding a new motion with a real lifecycle difference (new-business versus renewal opportunities, a distinct support process), not every time a team wants a slightly different field or label.

    Do it right
  1. Record type when the stage set, picklist values, or layout genuinely fork by process.
  2. A categorization picklist when the process is the same and you only need to tag or segment records.
  3. Map each record type to its layout and its restricted picklist values deliberately, since that assignment is the reason the record type exists.
  4. Keep the count low; every record type is standing layout and picklist maintenance on every future field change.

Record types cost maintenance on every future change and picklists do not, so the choice is a standing tax; picking the heavy tool for a light difference (or the light tool for a real process fork) makes every later edit slower or the data messier.

PAT-10

Validation rule over universally-required field

A requiredness pattern that enforces conditional requirements (required at a stage, for a record type, or when another field is set) with a validation rule, and reserves the field-definition Required setting for fields that are truly mandatory on every record at every moment.

WhenAny make-this-field-required request, because almost none of them mean required always, everywhere, for every record type and every integration.

    Do it right
  1. Validation rule scoped by stage, record type, profile, or a companion field for anything conditionally required.
  2. Field-definition Required only for a value that must exist on every record from creation, including API and import.
  3. Scope rules so early-stage records and system integrations are not blocked by requirements that only apply late.
  4. Keep the rule set small enough that reps clear validation on the first save; a persistently low first-save pass rate means a rule is fighting the team.

Universal requiredness and layout-required sit at opposite failure modes: one blocks everything including the paths that should be exempt, the other enforces almost nothing; a scoped validation rule is the only place the requirement matches the actual business condition.

PAT-11

Least-mechanism sharing model

A record-visibility model that opens access with the lightest mechanism that fits the pattern (role hierarchy for vertical, sharing rules for lateral and criteria-based, manual sharing for one-offs) and reserves Apex managed sharing for cases no declarative option can express.

WhenAny org with private or restricted org-wide defaults where some users need access to records they do not own. Decide the mechanism per access pattern, not per ticket.

    Do it right
  1. Role hierarchy for manager-sees-reports vertical access, which the platform grants automatically.
  2. Sharing rules (owner-based or criteria-based) to open records laterally to public groups or roles.
  3. Manual sharing for genuine one-off exceptions, not as a standing pattern.
  4. Apex managed sharing only for programmatic logic no declarative rule can express, and document why.

Broad all-data permissions and scattered manual shares are the access-review nightmare, solving today ticket by creating a standing exposure nobody remembers to revoke; the declarative mechanism that matches the pattern is the one an auditor can actually read.

PAT-12

Field-level security over page-layout hiding

A field-protection pattern that restricts sensitive fields with field-level security (enforced at the data layer, in the API, reports, and list views) rather than removing them from a page layout, which hides them on one screen while leaving the data fully accessible everywhere else.

WhenAny field carrying sensitive data (comp, margin, PII, internal scores) where some users must not see the value, not just not see it on one layout.

    Do it right
  1. Set field-level security through permission sets, granting read or edit only to the personas that need it.
  2. Verify the field is hidden in the API, reports, list views, and exports, not just the layout, since FLS covers all of them.
  3. Use page-layout placement for usability (what to show and where), never as the security boundary.
  4. In Apex that must respect FLS, enforce it explicitly (WITH SECURITY_ENFORCED, stripInaccessible, or user-mode), because system-mode Apex bypasses FLS.

Page-layout removal is a display choice, not access control, so the field stays exposed through every other read path; and even correct FLS leaks through Apex unless the code opts into enforcement, which is where the quiet exposure hides.