---
name: apex-development
description: Write production-grade Salesforce Apex — bulkified triggers with a trigger framework, service/selector/domain layering, SOQL and DML best practices, governor-limit-aware design, sharing and security (with sharing, USER_MODE, FLS), asynchronous Apex (Queueable, Batch, Schedulable, Platform Events, future), error handling and logging, and Apex test classes that assert behaviour. Use this whenever the user asks to write, refactor, review or debug Apex classes, triggers, batch jobs, queueables, schedulers, or unit tests; hits a governor limit (SOQL 101, CPU time, heap, DML rows); asks about trigger recursion, mixed DML, order of execution, or "how do I do X in Apex". Also apply it when reviewing Apex code.
metadata:
  technology: Salesforce
  type: development
---

# Apex Development

Apex runs in a multi-tenant sandbox with hard limits per transaction. Every design choice starts from: "this will be called with 200 records at once, by a user with restricted sharing, inside a transaction that already used half its limits." Write for that case and the single-record case is free.

## 1. Layering (one class per responsibility)

```
OrderTriggerHandler      ← trigger delegates here; routes by context, no logic
OrderService             ← business rules, orchestration, DML via Unit of Work
OrderSelector            ← all SOQL for Order__c; USER_MODE; field sets
OrderDomain (optional)   ← record-level validation/defaults (fflib Domain pattern)
OrderController          ← thin @AuraEnabled / @InvocableMethod façade → Service
```

- **Triggers contain one line**: `new OrderTriggerHandler().run();` — one trigger per object, always.
- Use an established **trigger framework** (fflib, Kevin O'Hara's TriggerHandler, or the org's) with per-object bypass flags and recursion control via static `Set<Id>` of processed records, not a global boolean.
- **Selectors** own SOQL: `selectById(Set<Id>)`, `selectOpenByAccount(Set<Id>)`. Nobody writes SOQL in a service or trigger.
- **Services** are static-free instance classes with an interface so they can be mocked (`OrderService.newInstance()` via a factory / `Application` class).
- Cross-cutting: `Logger`, `Constants`, `Application` (factories), `UnitOfWork`.

## 2. Bulkification — the non-negotiables

- Never SOQL or DML inside a loop. Collect Ids → one query into a `Map<Id, SObject>` → loop in memory → one DML.
- Aggregate in SOQL (`GROUP BY`, `SUM`) rather than in Apex loops when possible.
- Use `Map<Id, List<Child__c>>` groupings built in one pass; avoid nested loops over two large lists (O(n²) burns CPU).
- Trigger context: iterate `Trigger.new`, compare with `Trigger.oldMap.get(id)` for change detection; only process records whose relevant fields changed.
- DML with `Database.insert(records, false)` when partial success is acceptable; inspect `SaveResult`s and log failures. `allOrNone = true` (default) when the operation must be atomic.
- Limits to design against: 100 SOQL / 150 DML statements / 10 000 DML rows / 50 000 query rows / 10 s CPU / 6 MB heap (sync); async doubles most. Check `Limits.getQueries()` in tests for hot paths.

## 3. SOQL and DML

- **`WITH USER_MODE`** on every query in user-facing paths; **`WITH SYSTEM_MODE`** only in explicitly system-context code with a comment saying why.
- Select only the fields you use; use **Field Sets** or a constant field list in the Selector; never `SELECT *`-style dynamic field dumps.
- Filter on indexed fields (Id, Name, external Id, lookup, custom indexed); avoid `!=`, `NOT IN`, leading `%` wildcards, and formula fields in `WHERE` on large tables — they're non-selective and fail over 100k+ rows.
- Relationship queries (`SELECT Id, (SELECT Id FROM Lines__r) FROM Order__c`) instead of a second query when the child volume is bounded.
- `FOR UPDATE` when you'll mutate records that others may touch concurrently; keep the transaction short.
- Bind variables always (`:accountIds`) — no string concatenation into SOQL; use `String.escapeSingleQuotes` only for unavoidable dynamic SOQL.
- Dynamic SOQL via `Database.queryWithBinds(query, bindMap, AccessLevel.USER_MODE)`.
- DML in **USER_MODE**: `Database.insert(records, AccessLevel.USER_MODE)` enforces CRUD/FLS; wrap in Unit of Work to order inserts/updates and register relationships.

## 4. Security

- Class-level `with sharing` by default; `inherited sharing` for utilities; `without sharing` only in a dedicated, reviewed class with a documented reason.
- CRUD/FLS via USER_MODE (preferred) or `Security.stripInaccessible` when you must query in system mode and return to a user.
- Never trust input from LWC/API: validate types, ranges, ownership (does this user own/see this Id?).
- No hardcoded Ids, profile names, or record type Ids — use Custom Metadata, `Schema.getGlobalDescribe()` sparingly (it's expensive; prefer `SObjectType.Order__c.getRecordTypeInfosByDeveloperName()`).
- Secrets in Named Credentials / Protected Custom Settings, never in code.

## 5. Asynchronous Apex — pick correctly

| Need | Use | Notes |
|---|---|---|
| Fire-and-forget after commit, small payload | **Queueable** (`System.enqueueJob`) | Chainable, supports complex types; `Transaction Finalizer` for retry |
| Process millions of rows | **Batch Apex** (`Database.Batchable`) | 200/scope default; make `execute` idempotent; `Database.Stateful` only if needed |
| Run on a schedule | **Schedulable** → enqueues a Queueable/Batch | Keep the `execute` tiny |
| Callout from a trigger | Queueable (or Platform Event → subscriber) | Triggers can't call out synchronously |
| Decouple producers/consumers, cross-system | **Platform Events** / CDC | At-least-once; subscriber trigger must be idempotent; `EventBus.publish` after commit |
| Legacy | `@future` | Avoid in new code — no chaining, no complex params |

Async rules: idempotent by design (re-runs happen), log job Ids, respect the 50 queued jobs limit (chain, don't fan out), and test with `Test.startTest()/stopTest()` to force execution.

## 6. Order of execution and pitfalls

- Know the sequence: before triggers → validation rules → duplicate rules → after triggers → assignment/auto-response → workflow/Flow (before-save Flows run before before-triggers) → escalation → roll-up summary → post-commit (emails, events, async).
- **Mixed DML**: setup objects (User, PermissionSet) and non-setup in one transaction → `MIXED_DML_OPERATION`; move one side to a Queueable or `System.runAs` in tests.
- Recursion: after-update triggers re-firing on their own updates — guard with processed-Id sets, and prefer before-context field changes (no extra DML).
- `Trigger.new` is read-only in after context; field changes belong in `before`.
- Roll-ups: DLRS/Flow for simple; Apex only when volume/logic demands.

## 7. Error handling and logging

- Catch specific exceptions (`DmlException`, `QueryException`, `CalloutException`); never empty `catch`.
- Rethrow as a domain exception (`OrderService.OrderException`) or `AuraHandledException` with a user-safe message at the controller boundary.
- Log with a proper logger (Nebula Logger or the org's) to a custom object/Platform Event so logs survive rollback; include record Ids, user, quiddity, stack.
- `Database.setSavepoint()` / `Database.rollback(sp)` around multi-step DML that must be atomic across catches.

## 8. Tests that mean something

- `@IsTest` class per class under test; `@TestSetup` with a **TestDataFactory**; no `SeeAllData=true`.
- Every test: **positive**, **bulk (200+)**, **negative** (validation/exception), **sharing/`runAs`** where relevant.
- `Test.startTest()`/`stopTest()` to reset limits and flush async; assert on **outcomes** (`Assert.areEqual(expected, actualRecord.Status__c, 'status after approval')`) — not just "no exception".
- Mock callouts with `HttpCalloutMock`; mock services via interfaces + `Stub API` (`Test.createStub`) or a mocking library (ApexMocks).
- Assert governor usage on hot paths: `Assert.isTrue(Limits.getQueries() < 5, 'query count')`.
- Aim ≥ 85 % on service/selector classes; the org-wide 75 % minimum is a floor, not a target.

## 9. Style

- PMD ruleset in CI (`ApexUnitTestClassShouldHaveAsserts`, `AvoidSoqlInLoops`, `ApexCRUDViolation`, `AvoidHardcodingId`, cyclomatic complexity).
- Prettier Apex plugin; PascalCase classes, camelCase members, `Test` suffix on test classes, `Selector`/`Service`/`TriggerHandler` suffixes.
- ApexDoc on public methods; small methods (< 40 lines); no God classes.

## Anti-patterns to reject

- SOQL/DML inside loops; `SELECT` without `WHERE` on large objects.
- Logic in triggers; multiple triggers per object; static boolean recursion flags.
- `without sharing` on controllers; missing USER_MODE; string-concatenated SOQL.
- `@future` in new code; chaining more than a few Queueables without a finalizer.
- Hardcoded Ids/names; `Schema.getGlobalDescribe()` in loops.
- Tests with `SeeAllData`, no asserts, or single-record-only coverage.
- Swallowed exceptions; `System.debug` as the logging strategy.
