Skip to main content

Well-Architected anti-patterns

vulkro-sf ships two distinct commands:

  • vulkro-sf scan: security findings. The default Vulkro surface: CRUD/FLS, injection, XSS, secrets, sharing, posture, identity, third-party, AI / Agentforce. The output gates the AppExchange readiness report.
  • vulkro-sf antipatterns: Salesforce Well-Architected anti-pattern detection. The output is a separate report.

The two commands are intentionally split. This page explains why, documents the AP-001 through AP-014 series, and shows the invocation.

What this command is not

vulkro-sf antipatterns is not security analysis. The findings it emits do not map to the AppExchange Security Review checklist sections, do not carry OWASP categories, and do not raise CVSS-style severity. They are architecture-quality findings about governor-limit risk, code maintainability, test-class coverage, and the operational patterns Salesforce architects publish as the Well-Architected Framework (see Methodology section 1).

A SOQL query inside a for loop is a Well-Architected anti-pattern (AP-001): the org will hit the SOQL query governor limit at scale and the operation will fail at runtime. That is a reliability and maintainability problem, not a security problem. Mixing it into the security finding stream would confuse the signal both for engineers triaging security and for reviewers reading the AppExchange readiness report.

Why it is split from scan

Three reasons:

  1. Different signal class. Security findings answer "could this be exploited?". Anti-pattern findings answer "will this break at scale?" or "is this idiomatic Salesforce?". Mixing the two surfaces in one stream makes triage harder.
  2. Different severity model. Security severity tracks exploitability and blast radius. Anti-pattern severity tracks how reliably the pattern will fail at production volume and how expensive the fix is later. The two are not directly comparable and should not share a Critical / High / Medium / Low scale.
  3. Different consumer. AppExchange Security Reviewers read the security output; engineering leads and Salesforce architects read the anti-pattern output. The audiences want different reports; they get different commands.

The AP-001 through AP-014 series

Anti-pattern IDs are stable. Each ID is permanent once shipped; a finding's ID does not move between releases. The full series is in the table below; each row links to its detection deep-dive in the Methodology coverage matrix.

IDPillar / DimensionPattern
AP-001Trusted / ReliableSOQL query inside a for / while / do loop. Hits the per-transaction query governor limit (100 SOQL queries in synchronous Apex). Fix by bulkifying: collect IDs first, query once outside the loop.
AP-002Trusted / ReliableDML statement inside a loop. Hits the per-transaction DML statement limit (150). Same bulkification fix as AP-001.
AP-003Adaptable / Future-ProofHardcoded Salesforce record ID. A 15- or 18-character ID baked into source. Breaks on org migration; couples the package to a single org. Use Custom Metadata, Custom Settings, or Named Credentials instead.
AP-004Trusted / ReliableApex class with no apparent test class. Cross-file: looks for a sibling-named *Test.cls or a substring reference in any @isTest class. AppExchange requires 75% code coverage.
AP-005Adaptable / ComposableMultiple triggers on the same SObject. Two or more trigger files target the same on SObject. Order of execution becomes implementation-defined. Use a single trigger plus a handler pattern.
AP-006Trusted / ReliableRecursive trigger without a static guard. A trigger that performs DML on Trigger.new or Trigger.old without a static-boolean recursion guard. Re-enters on the same transaction.
AP-007Trusted / ReliableMixed-DML transaction (setup + non-setup sObjects). A single transaction touching a setup object (User, Group, UserRole, Profile, QueueSObject, etc.) and a non-setup object (Account, custom object, etc.) without separating them with System.runAs or async. Raises MIXED_DML_OPERATION at runtime. Excludes @isTest and explicit System.runAs blocks.
AP-008Easy / AutomatedSchema.describe* call inside a loop. Schema.SObjectType.X.fields.getMap() and similar describes are expensive; calling them per iteration in a loop wastes CPU. Cache the describe result outside the loop.
AP-009Adaptable / Composable@future method abuse: callout-violation patterns. @future(callout=true) invoked from a context that already issued a callout, or @future chained from another @future. Use Queueable for chained async or Continuation for parallel callouts.
AP-010Trusted / Reliable@isTest(SeeAllData=true). Production data visibility in tests. Brittle (depends on org data state), forbidden by AppExchange Security Review, and prevents parallel test execution. Use a TestDataFactory pattern with @testSetup.
AP-011Trusted / ReliableMixed-DML transactional rules. Stricter check than AP-007: flags constructor patterns that build setup-object instances and non-setup-object instances within the same file scope, even when no DML has fired yet, because the pattern is a near-certain mixed-DML at the next persistence step.
AP-012Adaptable / ComposableHardcoded URL in callout (use Named Credential). HttpRequest.setEndpoint('http://...') or setEndpoint('https://...') where the argument is a literal URL string rather than a callout:NamedCredentialName/path reference. Named Credentials let the org admin rotate the credential and the endpoint without a deployment.
AP-013Trusted / ReliableNon-restrictive SOQL. SELECT ... FROM ... with no WHERE and no LIMIT. Full-object scan; hits the row limit on a real org.
AP-014Trusted / ReliableTest class methods without assertions. Test methods that call into production code but do not call any Assert.* or System.assert*. The test runs and contributes to coverage but does not actually verify behavior. AppExchange Security Review reads this as low test quality.

Resource Optimiser: the governor-limit group

A subset of the Trusted (Reliable) rules share one theme: they each burn a slice of a per-transaction governor limit. Vulkro for Salesforce groups them under a Resource Optimiser label so a reviewer can read all the "manage your governor limits" findings as one set. The group covers AP-001, AP-002, AP-008, AP-013, AP-040, AP-042 and the five callout / async-design rules below.

IDPillar / DimensionPattern
AP-059Trusted / ReliableHTTP callout inside a loop. An Http.send, new HttpRequest, .send(req), or Database.getRecords / getQueryLocator callout issued per record. Each iteration consumes one of the 100 callouts allowed per transaction. Hoist the callout out of the loop or batch the work into one request.
AP-065Trusted / Reliable@future method invoked per-record. A same-file @future static method called from inside a loop. Apex caps @future at 50 invocations per transaction. Redesign the method to accept a collection and call it once.
AP-071Trusted / ReliableSystem.enqueueJob inside a loop. A Queueable enqueued per record. A transaction may add at most 50 jobs to the async queue (one from inside a running Queueable chain). Enqueue a single job over the whole collection.
AP-072Trusted / ReliableBatchable start() query not bounded. A Database.Batchable class whose start() SOQL returns a QueryLocator with no LIMIT. The locator can stream the entire object. Advisory: bound the scope with a WHERE clause and an explicit executeBatch scope size.
AP-073Trusted / ReliableMethod consumes a large share of the transaction budget. A single method body that statically issues more than 15 SOQL queries or more than 15 DML statements (even outside a loop). Intra-method advisory; distinct from the in-loop rules AP-001 / AP-002.

In --format table output, every finding in this group is tagged (Resource Optimiser) next to its [AP-xxx] line. This is a display-only label: the serialized pillar field in --format json, sarif, and junit output is always the underlying Trusted (Reliable) value, so machine consumers are unaffected.

Invocation

vulkro-sf antipatterns ./force-app

Same input shape as vulkro-sf scan: an SFDX project root, a retrieved metadata folder, or any directory containing Apex source. The output is grouped by anti-pattern ID, lists every triggering file and line, and follows the standard exit-code contract:

  • 0: no anti-patterns triggered.
  • 1: at least one anti-pattern fired.
  • 2: error.

Output formats

--format accepts the same shapes as vulkro-sf scan:

vulkro-sf antipatterns ./force-app --format text # default
vulkro-sf antipatterns ./force-app --format sarif -o ap.sarif
vulkro-sf antipatterns ./force-app --format json -o ap.json
vulkro-sf antipatterns ./force-app --format html -o ap.html

The SARIF output uses a separate Vulkro tool name (vulkro-sf-antipatterns) so downstream tools that consume both streams can split the views cleanly.

CI gating

The recommended pattern is to gate vulkro-sf scan (security) on PR, and to gate vulkro-sf antipatterns on the nightly run or on main-branch pushes. Anti-patterns rarely need to block a single PR; they need to block a release. See CI/CD integration for the wiring.

Where to go next