
Generating Apex
- 1.6k installs
- 763 repo stars
- Updated July 24, 2026
- forcedotcom/afv-library
This is a copy of generating-apex by forcedotcom - installs and ranking accrue to the original listing.
generating-apex is a Salesforce Apex code generation skill that produces consistent production-ready classes and abstract base classes for developers building integrations and backend services on the Salesforce platform.
About
generating-apex is an AFV Library skill from forcedotcom that scaffolds Salesforce Apex following a consistent abstract-base-class pattern. Generated classes define common behavior, extension points, and abstract methods subclasses must implement for specific integrations. Templates include Javadoc-style descriptions, usage examples, and callout configuration such as named credential endpoints like callout:Salesforce_API/services/data/v62.0 plus override hooks for getEndpoint and getHeaders. Developers reach for generating-apex when standing up new Salesforce integration services or backend families that should share structure instead of one-off Apex copies.
- Generates abstract base classes with protected state, constructors, and extension points for subclasses
- Produces concrete implementation examples showing how to override getEndpoint and getHeaders methods
- Enforces Salesforce best practices with 'with sharing' and static constants like DEFAULT_TIMEOUT_MS
- Creates reusable service layer patterns for callouts and API integrations
- Outputs ready-to-extend templates that reduce boilerplate in Force.com projects
Generating Apex by the numbers
- 1,608 all-time installs (skills.sh)
- +2 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/forcedotcom/afv-library --skill generating-apexAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 763 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 24, 2026 |
| Repository | forcedotcom/afv-library ↗ |
How do you generate consistent Salesforce Apex base classes?
Generate consistent, production-ready Apex classes and abstract base classes for Salesforce integrations and backend services.
Who is it for?
Salesforce developers creating integration or backend service families that need shared Apex structure and callout patterns.
Skip if: Non-Salesforce backends or teams that only need one-off Apex snippets without abstract class conventions.
When should I use this skill?
A developer needs production-ready Apex classes or abstract base classes for Salesforce integrations and backend services.
What you get
Production-ready Apex abstract base classes and integration service scaffolds with documented override methods.
- Apex abstract base classes
- Integration service scaffolds
By the numbers
- Example callout template references Salesforce API v62.0 endpoint path
Files
Generating Apex
Use this skill for production-grade Apex: new classes, selectors, services, async jobs, invocable methods, and triggers; and for evidence-based review of existing .cls OR .trigger.
Required Inputs
Gather or infer before authoring:
- Class type (service, selector, domain, batch, queueable, schedulable, invocable, trigger, trigger action, DTO, utility, interface, abstract, exception, REST resource)
- Target object(s) and business goal
- Class name (derive using the naming table below)
- Net-new vs refactor/fix; any org/API constraints
- Deployment targets (default to runSpecifiedTests and use generated tests where applicable)
Defaults unless specified:
- Sharing:
with sharing(see sharing rules per type below) - Access:
public(useglobalonly when required by managed packages or@RestResource) - API version:
66.0(minimum version) - ApexDoc comments: yes
If the user provides a clear, complete request, generate immediately without unnecessary back-and-forth.
---
Workflow
All steps are sequential. Do not skip, merge, or reorder. If blocked, stop and ask for missing context. If not applicable, mark N/A with a one-line justification in the report.
Phase 1 — Author
1. Discover project conventions
- Service-Selector-Domain layering, logging utilities
- Existing classes/triggers and current trigger framework or handler pattern
- Whether Trigger Actions Framework (TAF) is already in use
2. Choose the smallest correct pattern (see Type-Specific Guidance below)
3. Review templates and assets
- Read the matching template from
assets/before authoring (see Type-Specific Guidance for the file mapping) - When a
references/example exists for the type, read it as a concrete style guide - For any test class work, always read and use
generating-apex-testskill
4. Author with guardrails -- apply every rule in the Rules section below
- Generate
{ClassName}.clswith ApexDoc - Generate
{ClassName}.cls-meta.xml
5. Generate test classes -- Load the skill generating-apex-test to create {ClassName}Test.cls and {ClassName}Test.cls-meta.xml. Apex tests are always required to be generated to deploy. No test file creation or edits can occur without loading the generating-apex-test skill to generate tests.
Phase 2 — Validate (required before reporting)
Writing files is the midpoint, not the finish line. Steps 6 and 7 each require a tool invocation and produce output that must appear in the Step 8 report. Do not summarize or present the report until both steps have run and their output is captured.
6. Run code analyzer
- Invoke MCP
run_code_analyzeron all generated/updated.clsfiles. - Remediate all
sev0,sev1, andsev2violations; re-run until clean. - Capture the final tool output verbatim for the report.
- Fallback:
sf code-analyzer run --target <target>. If both are unavailable, recordrun_code_analyzer=unavailable: <error>in the report.
7. Execute Apex tests
- Run org tests including
{ClassName}Testviasf apex run testor MCP. - Delegate all test generation/fixes/coverage work to
generating-apex-test; iterate until the tests pass. - Capture pass/fail counts and coverage percentage for the report.
- If unavailable, record
test_execution=unavailable: <error>in the report.
Phase 3 — Report
8. Report -- use the output format at the bottom of this file.
- The
Analyzerline must contain the actual Step 6 tool output (orrun_code_analyzer=unavailable: <reason>after attempting invocation). - The
Testingline must contain the actual Step 7 results (ortest_execution=unavailable: <reason>after attempting invocation). - A report missing either line is incomplete. Always attempt the tool invocation before recording unavailable.
---
Rules
Hard-Stop Constraints (Must Enforce)
If any constraint would be violated in generated code, stop and explain the problem before proceeding:
| Constraint | Rationale |
|---|---|
| Place all SOQL outside loops | Avoid query governor limits (100 queries) |
| Place all DML outside loops | Avoid DML governor limits (150 statements) |
| Declare a sharing keyword on every class | Prevent unintended without sharing defaults and data exposure |
| Use Custom Metadata/Labels/describe calls instead of hardcoded IDs | Ensure portability across orgs |
| Always handle exceptions (log, rethrow, or recover) | Prevent silent failures |
| Use bind variables for all dynamic SOQL with user input | Prevent SOQL injection |
Use Apex-native collections (List, Map, Set) rather than Java types | Prevent compile errors |
| Verify methods exist in Apex before use | Prevent reliance on non-existent APIs |
Avoid System.debug() in main code paths | Debug statements evaluate even when loggign is not active and consume CPU. Use a logging framework if required on main code paths |
Never use @future methods | Use Queueable with System.Finalizer; @future cannot chain, cannot be called from Batch, and cannot accept non-primitive types |
Bulkification & Governor Limits
- All public APIs accept and process collections; single-record overloads delegate to the bulk method
- In batch/bulk flows, prefer partial-success DML (
Database.update(records, false)) and processSaveResultfor errors - Use
Map<Id, SObject>constructor for efficient ID-based lookups from query results - Use
Map<Id, List<SObject>>to group child records by parent; build the map in a single loop before processing - Use
Set<Id>for deduplication and membership checks; preferSet.contains()overList.contains() - Use relationship subqueries to fetch parent + child records in a single SOQL when both are needed
- Use
AggregateResultwithGROUP BYfor rollup calculations instead of querying and counting in Apex - Only DML records that actually changed — compare against
Trigger.oldMapor prior state before adding to the update list - Use
Limits.getQueries(),Limits.getDmlStatements(),Limits.getCpuTime()to monitor consumption in complex transactions
SOQL Optimization
- Use selective queries with proper
WHEREclauses; use indexed fields (Id,Name,OwnerId, lookup/master-detail fields,ExternalIdfields, custom indexes) in filters when possible SELECT *does not exist in SOQL -- always specify the exact fields needed- Apply
LIMITclauses to bound result sets; useORDER BYfor deterministic results - When querying Custom Metadata Types (objects ending with
__mdt), do NOT use SOQL — use the built-in methods ({CustomMdt__mdt}.getAll().values(),getInstance(), etc.)
Caching
- Use Platform Cache (
Cache.Org/Cache.Session) for frequently accessed, rarely changed data; set a TTL and always handle cache misses — cache can be evicted at any time - Use
private static Mapfields as transaction-scoped caches to prevent duplicate queries within the same execution context; lazy-initialize on first access
Security
- Default to
with sharing; document justification forwithout sharingorinherited sharing WITH USER_MODEin SOQL andAccessLevel.USER_MODEforDatabaseDML for CRUD/FLS enforcement- Validate dynamic field/operator names via allowlist or
Schema.describe - Named Credentials for all external credentials/API keys
AuraHandledExceptionfor@AuraEnableduser-facing errors (no internal details)without sharingrequires a Custom Permission check- Isolate
without sharinglogic in dedicated helper classes; call fromwith sharingentry points to limit elevated-access scope - Encrypt PII/sensitive data at rest via Platform Encryption; never expose PII in debug statements, error messages, or API responses
Security Verification
Before finalizing, verify: CRUD/FLS enforced (SOQL + DML) · explicit sharing keyword on every class · no hardcoded secrets or Record IDs · PII excluded from logs and error messages · error messages sanitized for end users.
Error Handling
- Catch specific exceptions before generic
Exception; include context in messages - Use
try/catchonly around code that can throw (DML, callouts, JSON parsing, casts); avoid defensive wrapping of simple assignments/collection ops/arithmetic - Preserve exception cause chains:
new CustomException('message', cause)(do not replace stack trace with concatenated messages) - Provide a custom exception class per service domain when meaningful
- In
@AuraEnabledmethods, catch exceptions and rethrow asAuraHandledException - Fallback option: when no meaningful domain exception exists, catch generic
Exceptionand either rethrow it or wrap it in a minimal custom exception that preserves the original cause.
Null Safety
- Add guard clauses for null/empty inputs at the top of every public method; match style to context:
returnearly in private/trigger-handler methods,throwexceptions in public APIs,record.addError()in validation services - Return empty collections instead of
null - Use safe navigation (
?.) for chained property access - Never dereference
map.get(key)inline unless presence is guaranteed; usecontainsKey, assignment+null check, or safe navigation first - Use null coalescing (
??) for default values - Prefer
String.isBlank(value)over manual checks likevalue == null || value.trim().isEmpty()
Constants & Literals
- Use enums over string constants whenever possible; enum values follow
UPPER_SNAKE_CASE - Extract repeated literal strings/numbers into
private static finalconstants or a constants class - Use
Label.custom labels for user-facing strings - Use Custom Metadata for configurable values (thresholds, mappings, feature flags)
- Never output HTML-escaped entities in code (e.g.,
'); use literal single quotes'in Apex string literals
Naming Conventions
| Type | Pattern | Example |
|---|---|---|
| Service | {SObject}Service | AccountService |
| Selector | {SObject}Selector | AccountSelector |
| Domain | {SObject}Domain | OpportunityDomain |
| Batch | {Descriptive}Batch | AccountDeduplicationBatch |
| Queueable | {Descriptive}Queueable | ExternalSyncQueueable |
| Schedulable | {Descriptive}Schedulable | DailyCleanupSchedulable |
| DTO | {Descriptive}DTO | AccountMergeRequestDTO |
| Wrapper | {Descriptive}Wrapper | OpportunityLineWrapper |
| Utility | {Descriptive}Util | StringUtil |
| Interface | I{Descriptive} | INotificationService |
| Abstract | Abstract{Descriptive} | AbstractIntegrationService |
| Exception | {Descriptive}Exception | AccountServiceException |
| REST Resource | {SObject}RestResource | AccountRestResource |
| Trigger | {SObject}Trigger | AccountTrigger |
| Trigger Action | TA_{SObject}_{Action} | TA_Account_SetDefaults |
Additional naming rules:
- Classes:
PascalCase - Methods:
camelCase, start with a verb (get,create,process,validate,is,has,can) - Variables:
camelCase, descriptive nouns; Lists as plural nouns (e.g.,accounts,relatedContacts); Maps as{value}By{key}(e.g.,accountsById); Sets as{noun}Ids - Constants:
UPPER_SNAKE_CASE - Use full descriptive names instead of abbreviations (
acc,tks,rec)
ApexDoc
- Required on the class header and every
public/globalmethod - Include: brief description,
@param,@return,@throws,@examplewhere helpful
Class-level format:
/**
* Provides services for geolocation and address conversion.
*/
public with sharing class GeolocationService { }Method-level format:
/**
* @param paramName Description of the parameter
* @return Description of the return value
* @example
* List<Account> results = AccountService.deduplicateAccounts(accountIds);
*/Code Structure & Architecture
- Single responsibility per class; max 500 lines -- split when exceeded
- Return Early: validate preconditions at method top, return/throw immediately
- Extract private helpers for methods over ~40 lines
- Use Dependency Injection (constructor/method params) for testability
- Prefer composition and narrow interfaces over deep inheritance; extend via new implementations, not modifications
- Enforce single-level abstraction per method across layer boundaries:
| Layer | Owns | Must NOT contain |
|---|---|---|
| Trigger | Event routing only | Business logic, orchestration |
| Handler/Service | Flow control, coordination | Inline SOQL/DML/HTTP/parsing |
| Domain | Business rules, validation | Queries, callouts, persistence details |
| Data/Integration | SOQL, DML, HTTP | Business decisions |
- Disallowed: methods mixing orchestration with inline SOQL/DML/HTTP; business rules mixed with parsing internals; validation + persistence + cross-system plumbing in one method
---
Async Decision Matrix
| Scenario | Default | Key Traits |
|---|---|---|
| Standard async work | Queueable | Job ID, chaining, non-primitive types, configurable delay (up to 10 min via AsyncOptions), dedup signatures |
| Very large datasets | Batch Apex | Chunked processing, max 5 concurrent; use QueryLocator for large scopes |
| Modern batch alternative | CursorStep (Database.Cursor) | 2000-record chunks, higher throughput, no 5-job limit |
| Recurring schedule | Scheduled Flow (preferred) or Schedulable | Schedulable has 100-job limit; use only when chaining to Batch or needing complex Apex logic |
| Post-job cleanup | Finalizer (System.Finalizer) | Runs regardless of Queueable success/failure |
| Long-running callouts | Continuation | Up to 3 per transaction, 3 parallel |
| Delays > 10 minutes | System.scheduleBatch() | Schedule a Batch job at a specific future time |
| Legacy fire-and-forget | @future | Do not use in new code — see Hard-Stop Constraints; replace with Queueable + Finalizer |
---
Type-Specific Guidance
Service
- Template:
assets/service.cls· Reference:references/AccountService.cls with sharing; stateless — nopublicfields or mutable instance state; keep public APIs focused andstaticwhere reasonable- Delegate all SOQL to Selectors and SObject behavior to Domains
- Wrap business errors in a custom exception (e.g.,
AccountServiceException)
Selector
- Template:
assets/selector.cls· Reference:references/AccountSelector.cls inherited sharing; one per SObject or query domain- Return
List<SObject>orMap<Id, SObject>; use a shared base field list constant (no inline duplication) - Accept filter parameters; always include
WITH USER_MODE
Domain
- Template:
assets/domain.cls with sharing; encapsulate field defaults, derivations, and validations- Operate on in-memory lists only; no SOQL/DML (belongs in Services/Selectors)
Batch
- Template:
assets/batch.cls· Reference:references/AccountDeduplicationBatch.cls with sharing; implementDatabase.Batchable<SObject>(addDatabase.Statefulwhen tracking across chunks)start()= query definition;execute()= business logic;finish()= logging/notification- Use
QueryLocatorfor large datasets; handle partial failures viaDatabase.SaveResult - Accept filter parameters via constructor for reusability
Queueable
- Template:
assets/queueable.cls with sharing; implementQueueableand optionallyDatabase.AllowsCalloutswhen HTTP callouts are needed- Accept data via constructor
- Add chain-depth guards to prevent infinite chains
- Optionally implement
Finalizerfor recovery/cleanup - Use
AsyncOptionsfor configurable delay (up to 10 min) and dedup signatures
Schedulable
- Template:
assets/schedulable.cls with sharing;execute()delegates to Queueable or Batch- Provide CRON constants and a convenience
scheduleDaily()helper
DTO / Wrapper
- Template:
assets/dto.cls - No sharing keyword needed (pure data containers)
- Simple public properties; no-arg + parameterized constructors;
Comparablewhen ordering matters - Use
@JsonAccesson private/protected inner DTOs that are serialized/deserialized
Utility
- Template:
assets/utility.cls - No sharing keyword needed; all methods
public static;privateconstructor - Pure, side-effect-free; no SOQL/DML
Interface
- Template:
assets/interface.cls - Define clear contracts with ApexDoc on each method signature
Abstract
- Template:
assets/abstract.cls with sharing; offer default behavior viavirtualmethods- Mark extension points
protected virtualorprotected abstract - Include a concrete example in the ApexDoc showing how to extend the class
Custom Exception
- Template:
assets/exception.cls - No sharing keyword; extend
Exceptionwith descriptive names - Supported constructors:
(),('msg'),(cause),('msg', cause)
Trigger
- Template:
assets/trigger.cls - One trigger per object; delegate all logic to handler/TAF action classes
- Include all relevant DML contexts; if TAF:
new MetadataTriggerHandler().run();
Trigger Action (TAF)
- One class per concern per context; implement
TriggerAction.{Context} - Register via
Trigger_Action__mdt(actions are inactive without registration) - Name:
TA_{SObject}_{ActionName}; prefer field-value comparison over static booleans for recursion
Invocable Method (@InvocableMethod)
- Template:
assets/invocable.cls with sharing; innerRequest/Responsewith@InvocableVariable- Method must be
public static; non-static or single-object signatures will not compile - Accept
List<Request>, returnList<Response>; bulkify (SOQL/DML outside loops) - Decorator parameters:
label(required — Flow Builder display name),description,category(groups actions in Builder),callout=true(required when method makes HTTP callouts) @InvocableVariableparameters:label(required),description,required=true/false@InvocableVariablesupports: primitives,Id,SObject,List<T>only (noMap/Set/Blob); useList<Id>orList<SObject>fields for Flow collection I/O- Always include
isSuccess,errorMessage, anderrorType(e.getTypeName()) in Response - Return errors in Response (recommended); throwing an exception triggers the Flow Fault path — reserve for unrecoverable failures only
REST Resource (@RestResource)
- Template:
assets/rest-resource.cls global with sharing; both class and methods must beglobal- Versioned URL:
@RestResource(urlMapping='/{resource}/v1/*') - Use proper HTTP status codes per branch (
200/201/400/404/422/500); never default all errors to500 - Validate inputs (Id format:
Pattern.matches('[a-zA-Z0-9]{15,18}', value)); bind all user input in SOQL - Include
LIMIT/ORDER BYin queries; implement pagination (pageSize/offset) - Standardized
ApiResponsewrapper (success,message,data/records); inner request/response DTOs - Thin controller: delegate business logic to Service classes
@AuraEnabled Controller
with sharing; useWITH USER_MODEin all SOQL- Use
@AuraEnabled(cacheable=true)only for read-only queries; leavecacheableunset for DML operations - Catch exceptions and rethrow as
AuraHandledExceptionwith user-friendly messages
---
Output Expectations
Deliverables per class:
{ClassName}.cls{ClassName}.cls-meta.xml(default API version66.0or higher unless specified){ClassName}Test.cls(generated viagenerating-apex-testskill){ClassName}Test.cls-meta.xml(generated viagenerating-apex-testskill)
Deliverables per trigger:
{TriggerName}.trigger{TriggerName}.trigger-meta.xml(default API version66.0or higher unless specified)
Meta XML template:
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>{API_VERSION}</apiVersion>
<status>Active</status>
</ApexClass>Report in this order:
Apex work: <summary>
Files: <paths>
Design: <pattern / framework choices>
Workflow: all steps completed (1-8); any N/A justified
Risks: <security, bulkification, async, dependency notes>
Analyzer: <REQUIRED -- paste actual run_code_analyzer output or state "run_code_analyzer=unavailable: <reason>">
Testing: <REQUIRED -- paste actual test execution results (pass/fail, coverage) or state "test_execution=unavailable: <reason>">
Deploy: <dry-run or next step>---
Cross-Skill Integration
| Need | Delegate to |
|---|---|
| Apex tests / fix failures | generating-apex-test skill |
| Describe objects/fields | metadata skill (if available) |
| Deploy to org | deploy skill (if available) |
| Flow calling Apex | Flow skill (if available) |
| LWC calling Apex | LWC skill (if available) |
---
Troubleshooting Boundary
This skill handles production .cls/.trigger/.apex issues only: compile/parse failures, deployment dependency errors, runtime governor-limit failures. For test execution, assertions, coverage, or sf apex run test failures, delegate to generating-apex-test.
/**
<<<<<<< Updated upstream
* @description Abstract base class for {describe the family of classes this serves}.
=======
* Abstract base class for {describe the family of classes this serves}.
>>>>>>> Stashed changes
* Provides common behavior and defines extension points for subclasses.
* Subclasses must implement the abstract methods to provide specific behavior.
*
* @example
* // Extending this abstract class:
* public class SalesforceIntegrationService extends {ClassName} {
* protected override String getEndpoint() {
* return 'callout:Salesforce_API/services/data/v62.0';
* }
*
* protected override Map<String, String> getHeaders() {
* return new Map<String, String>{
* 'Content-Type' => 'application/json'
* };
* }
* }
*/
public abstract with sharing class {ClassName} {
// ─── Constants ───────────────────────────────────────────────────────
private static final Integer DEFAULT_TIMEOUT_MS = 30000;
// ─── Protected State ─────────────────────────────────────────────────
protected Integer timeoutMs;
// ─── Constructor ─────────────────────────────────────────────────────
/**
* Initializes the base class with default configuration
*/
protected {ClassName}() {
this.timeoutMs = DEFAULT_TIMEOUT_MS;
}
// ─── Abstract Methods (must be implemented by subclasses) ────────────
/**
* Returns the endpoint URL for this integration.
* Subclasses must provide their specific endpoint.
* @return The endpoint URL as a String
*/
protected abstract String getEndpoint();
/**
* Returns the HTTP headers for this integration.
* Subclasses define their own required headers.
* @return Map of header name to header value
*/
protected abstract Map<String, String> getHeaders();
// ─── Virtual Methods (can be overridden by subclasses) ───────────────
/**
* Hook called before the main operation executes.
* Override to add pre-processing logic.
* Default implementation does nothing.
* @param context Map of contextual data
*/
protected virtual void beforeExecute(Map<String, Object> context) {
// Default: no-op — override in subclass if needed
}
/**
* Hook called after the main operation completes.
* Override to add post-processing logic.
* Default implementation does nothing.
* @param context Map of contextual data
* @param result The result from the operation
*/
protected virtual void afterExecute(Map<String, Object> context, Object result) {
// Default: no-op — override in subclass if needed
}
// ─── Template Method (common workflow) ───────────────────────────────
/**
* Executes the operation using the template method pattern.
* Calls beforeExecute → doExecute → afterExecute in sequence.
* @param context Map of data needed for the operation
* @return The result of the operation
*/
public Object execute(Map<String, Object> context) {
beforeExecute(context);
Object result;
try {
result = doExecute(context);
} catch (Exception e) {
handleError(e);
throw e;
}
afterExecute(context, result);
return result;
}
// ─── Protected Helpers ───────────────────────────────────────────────
/**
* Core execution logic — override this for the main operation.
* Default implementation throws — subclass must provide implementation.
* @param context Map of data needed for the operation
* @return The result of the operation
*/
protected virtual Object doExecute(Map<String, Object> context) {
throw new UnsupportedOperationException(
'Subclass must override doExecute()'
);
}
/**
* Error handler called when doExecute throws.
* Override to customize error handling (e.g., logging, retry).
* @param e The exception that was thrown
*/
protected virtual void handleError(Exception e) {
System.debug(LoggingLevel.ERROR,
this.toString() + ' error: ' + e.getMessage() + '\n' + e.getStackTraceString()
);
}
// ─── Exception ───────────────────────────────────────────────────────
public class UnsupportedOperationException extends Exception {}
}
/**
* Batch Apex class for {describe the batch operation}.
* Processes {SObject} records in configurable batch sizes.
* Implements Database.Stateful to track cumulative results across chunks.
*
* @example
* // Execute with default batch size
* Database.executeBatch(new {ClassName}());
*
* // Execute with custom batch size
* Database.executeBatch(new {ClassName}(), 100);
*/
public with sharing class {ClassName} implements Database.Batchable<SObject>, Database.Stateful {
// ─── Constants ───────────────────────────────────────────────────────
private static final Integer DEFAULT_BATCH_SIZE = 200;
// ─── Stateful Tracking ───────────────────────────────────────────────
private Integer totalProcessed = 0;
private Integer totalErrors = 0;
private List<String> errorMessages = new List<String>();
// ─── Constructor ─────────────────────────────────────────────────────
/**
* Default constructor
*/
public {ClassName}() {
// Default configuration
}
// ─── Batchable Interface ─────────────────────────────────────────────
/**
* Defines the scope of records to process.
* Uses Database.QueryLocator for efficient large-dataset processing.
* @param bc The batch context
* @return QueryLocator for the records to process
*/
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id, Name
// TODO: Add fields needed for processing
FROM {SObject}
// TODO: Add WHERE clause to scope the records
]);
}
/**
* Processes each batch of records.
* Uses Database.update with allOrNone=false for partial success handling.
* @param bc The batch context
* @param scope List of {SObject} records in the current batch
*/
public void execute(Database.BatchableContext bc, List<{SObject}> scope) {
List<{SObject}> recordsToUpdate = new List<{SObject}>();
for ({SObject} record : scope) {
// TODO: Apply business logic to each record
recordsToUpdate.add(record);
}
if (!recordsToUpdate.isEmpty()) {
List<Database.SaveResult> results = Database.update(recordsToUpdate, false);
processResults(results);
}
}
/**
* Performs post-processing after all batches complete.
* Logs a summary of the batch execution.
* @param bc The batch context
*/
public void finish(Database.BatchableContext bc) {
String summary = String.format(
'{0} completed. Processed: {1}, Errors: {2}',
new List<Object>{
{ClassName}.class.getName(),
totalProcessed,
totalErrors
}
);
System.debug(LoggingLevel.INFO, summary);
if (!errorMessages.isEmpty()) {
System.debug(LoggingLevel.ERROR, 'Error details: ' + String.join(errorMessages, '\n'));
}
// TODO: Send completion notification email or post to chatter if needed
}
// ─── Private Helpers ─────────────────────────────────────────────────
/**
* Processes Database.SaveResult list, tracking successes and failures
* @param results List of SaveResult from a DML operation
*/
private void processResults(List<Database.SaveResult> results) {
for (Database.SaveResult result : results) {
if (result.isSuccess()) {
totalProcessed++;
} else {
totalErrors++;
for (Database.Error err : result.getErrors()) {
errorMessages.add(
'Record ' + result.getId() + ': ' +
err.getStatusCode() + ' - ' + err.getMessage()
);
}
}
}
}
// ─── Static Helpers ──────────────────────────────────────────────────
/**
* Convenience method to execute with default batch size
* @return The batch job Id
*/
public static Id run() {
return Database.executeBatch(new {ClassName}(), DEFAULT_BATCH_SIZE);
}
}
/**
* Domain class for {SObject}.
* Encapsulates field-level defaults, derivations, and validations.
* Operates only on in-memory SObject data — no SOQL or DML.
*/
public with sharing class {SObject}Domain {
// ─── Constants ───────────────────────────────────────────────────────
// TODO: Add constants for default values, statuses, etc.
// ─── Field Defaults ──────────────────────────────────────────────────
/**
* Applies default field values to new {SObject} records.
* Call this before insert to ensure consistent defaults.
* @param records List of {SObject} records to apply defaults to
*/
public static void applyDefaults(List<{SObject}> records) {
if (records == null || records.isEmpty()) {
return;
}
for ({SObject} record : records) {
// TODO: Set default field values
// Example:
// if (record.Status__c == null) {
// record.Status__c = DEFAULT_STATUS;
// }
}
}
// ─── Derivations ────────────────────────────────────────────────────
/**
* Derives calculated field values based on other fields.
* Call this before insert and before update.
* @param records List of {SObject} records to derive values for
*/
public static void deriveFields(List<{SObject}> records) {
if (records == null || records.isEmpty()) {
return;
}
for ({SObject} record : records) {
// TODO: Derive calculated field values
// Example:
// record.FullAddress__c = buildFullAddress(record);
}
}
// ─── Validations ────────────────────────────────────────────────────
/**
* Validates {SObject} records and adds errors for any violations.
* Call this before insert and before update.
* @param records List of {SObject} records to validate
*/
public static void validate(List<{SObject}> records) {
if (records == null || records.isEmpty()) {
return;
}
for ({SObject} record : records) {
// TODO: Add validation rules
// Example:
// if (String.isBlank(record.Name)) {
// record.addError('Name is required.');
// }
}
}
// ─── Comparisons ────────────────────────────────────────────────────
/**
* Determines which fields have changed between old and new record versions.
* Useful in before update context.
* @param oldRecord The previous version of the record
* @param newRecord The current version of the record
* @return Set of field API names that have changed
*/
public static Set<String> getChangedFields({SObject} oldRecord, {SObject} newRecord) {
Set<String> changedFields = new Set<String>();
if (oldRecord == null || newRecord == null) {
return changedFields;
}
Map<String, Schema.SObjectField> fieldMap = Schema.SObjectType.{SObject}.fields.getMap();
for (String fieldName : fieldMap.keySet()) {
if (oldRecord.get(fieldName) != newRecord.get(fieldName)) {
changedFields.add(fieldName);
}
}
return changedFields;
}
// ─── Private Helpers ─────────────────────────────────────────────────
// TODO: Add private helper methods as needed
}
/**
* Data Transfer Object for {describe the data this DTO represents}.
* Used to pass structured data between layers without exposing SObjects.
* Serialization-friendly for use with JSON.serialize/deserialize and API responses.
*
* @example
* // Create from constructor
* {ClassName} dto = new {ClassName}('value1', 42);
*
* // Deserialize from JSON
* {ClassName} dto = ({ClassName}) JSON.deserialize(jsonString, {ClassName}.class);
*/
public with sharing class {ClassName} {
// ─── Properties ──────────────────────────────────────────────────────
/** {Describe this property} */
public String name { get; set; }
/** {Describe this property} */
public Id recordId { get; set; }
/** {Describe this property} */
public Boolean isActive { get; set; }
/** {Describe this property} */
public List<String> tags { get; set; }
// TODO: Add additional properties as needed
// ─── Constructors ────────────────────────────────────────────────────
/**
* No-arg constructor for deserialization compatibility
*/
public {ClassName}() {
this.tags = new List<String>();
this.isActive = false;
}
/**
* Parameterized constructor for convenience
* @param name The name value
* @param recordId The associated record Id
*/
public {ClassName}(String name, Id recordId) {
this();
this.name = name;
this.recordId = recordId;
}
// ─── Factory Methods ─────────────────────────────────────────────────
/**
* Creates a DTO instance from an SObject record
* @param record The source {SObject} record
* @return A populated {ClassName} instance
*/
public static {ClassName} fromSObject(SObject record) {
if (record == null) {
return new {ClassName}();
}
{ClassName} dto = new {ClassName}();
dto.recordId = record.Id;
dto.name = (String) record.get('Name');
// TODO: Map additional fields
return dto;
}
/**
* Creates a list of DTOs from a list of SObject records
* @param records The source records
* @return List of populated {ClassName} instances
*/
public static List<{ClassName}> fromSObjects(List<SObject> records) {
List<{ClassName}> dtos = new List<{ClassName}>();
if (records == null) {
return dtos;
}
for (SObject record : records) {
dtos.add(fromSObject(record));
}
return dtos;
}
// ─── Utility Methods ─────────────────────────────────────────────────
/**
* Serializes this DTO to a JSON string
* @return JSON representation of this DTO
*/
public String toJson() {
return JSON.serialize(this);
}
/**
* Deserializes a JSON string into a {ClassName} instance
* @param jsonString The JSON string to deserialize
* @return A {ClassName} instance
*/
public static {ClassName} fromJson(String jsonString) {
return ({ClassName}) JSON.deserialize(jsonString, {ClassName}.class);
}
}
/**
* Custom exception for {describe when this exception is thrown}.
* Use this exception to signal domain-specific errors that callers
* can catch and handle distinctly from system exceptions.
*
* @example
* throw new {ClassName}('Account merge failed: duplicate detected.');
*
* // Wrap a caught exception
* try {
* // ... risky operation
* } catch (DmlException e) {
* throw new {ClassName}('DML failed during account merge: ' + e.getMessage());
* }
*/
public with sharing class {ClassName} extends Exception {
// Apex custom exceptions automatically inherit:
// - getMessage()
// - getCause()
// - getStackTraceString()
// - setMessage(String)
// - initCause(Exception)
//
// And support these constructor patterns:
// - new {ClassName}()
// - new {ClassName}('message')
// - new {ClassName}(causeException)
// - new {ClassName}('message', causeException)
//
// Note: Apex does NOT support custom constructors on Exception subclasses.
// To add context, use the message string or create a wrapper pattern:
//
// Example wrapper pattern (if you need structured error data):
//
// public class {ClassName}Detail {
// public String errorCode;
// public List<Id> failedRecordIds;
// public String detail;
//
// public {ClassName}Detail(String errorCode, List<Id> failedRecordIds, String detail) {
// this.errorCode = errorCode;
// this.failedRecordIds = failedRecordIds;
// this.detail = detail;
// }
//
// public override String toString() {
// return '[' + errorCode + '] ' + detail + ' (Records: ' + failedRecordIds + ')';
// }
// }
}
/**
* Interface for {describe the capability or contract this interface defines}.
* Implement this interface to provide {describe what implementations do}.
*
* @example
* public class EmailNotificationService implements {InterfaceName} {
* public void execute(Map<String, Object> params) {
* // Implementation
* }
* }
*/
public interface {InterfaceName} {
/**
* {Describe what this method should do}
* @param params {Describe the parameter}
* @return {Describe the return value}
*/
// TODO: Define interface methods
// Example:
// void execute(Map<String, Object> params);
// Boolean isEligible(SObject record);
// List<SObject> process(List<SObject> records);
}
/**
* Invocable Apex action for {describe the action}.
* Callable from Flows, Process Builder, and Agentforce.
* Accepts bulkified List<Request>, returns List<Response>.
*/
public with sharing class {ClassName} {
// ─── Invocable Method ────────────────────────────────────────────────
/**
* {Describe what this action does}
* @param requests List of Request inputs from the calling Flow
* @return List of Response outputs returned to the calling Flow
*/
@InvocableMethod(
label='{Display Name}'
description='{Describe the action for Flow Builder}'
category='{Category}'
)
public static List<Response> execute(List<Request> requests) {
List<Response> responses = new List<Response>();
// Collect all Ids upfront for bulkified query
Set<Id> allRecordIds = new Set<Id>();
for (Request req : requests) {
if (req.recordId != null) {
allRecordIds.add(req.recordId);
}
}
// Single bulkified query outside the loop
Map<Id, {SObject}> recordMap = allRecordIds.isEmpty()
? new Map<Id, {SObject}>()
: new Map<Id, {SObject}>([
SELECT Id, Name
// TODO: Add required fields
FROM {SObject}
WHERE Id IN :allRecordIds
WITH USER_MODE
]);
// Process each request
for (Request req : requests) {
responses.add(processRequest(req, recordMap));
}
return responses;
}
// ─── Private Helpers ─────────────────────────────────────────────────
/**
* Processes a single request and returns a response.
* Errors are captured in the response, not thrown.
* @param req The input request
* @param recordMap Pre-queried records keyed by Id
* @return A Response with success/error information
*/
private static Response processRequest(Request req, Map<Id, {SObject}> recordMap) {
Response res = new Response();
try {
{SObject} record = recordMap.get(req.recordId);
if (record == null) {
res.isSuccess = false;
res.errorMessage = 'Record not found: ' + req.recordId;
res.errorType = 'NOT_FOUND';
return res;
}
// TODO: Implement business logic
res.isSuccess = true;
} catch (Exception e) {
res.isSuccess = false;
res.errorMessage = e.getMessage();
res.errorType = e.getTypeName();
}
return res;
}
// ─── Request / Response DTOs ─────────────────────────────────────────
/**
* Input parameters from the calling Flow
*/
public class Request {
@InvocableVariable(label='Record Id' description='The Id of the record to process' required=true)
public Id recordId;
// TODO: Add additional input variables
// @InvocableVariable(label='Field Value' description='Value to set' required=false)
// public String fieldValue;
}
/**
* Output results returned to the calling Flow
*/
public class Response {
@InvocableVariable(label='Success' description='Whether the action succeeded')
public Boolean isSuccess;
@InvocableVariable(label='Error Message' description='Error details if the action failed')
public String errorMessage;
@InvocableVariable(label='Error Type' description='Exception type name if the action failed')
public String errorType;
// TODO: Add additional output variables
// @InvocableVariable(label='Result Id' description='Id of the created/updated record')
// public Id resultId;
}
}
/**
* Queueable Apex class for {describe the async operation}.
* Accepts data through the constructor for stateful processing.
* Optionally implements Database.AllowsCallouts for external integrations.
*
* @example
* // Enqueue the job
* Id jobId = System.enqueueJob(new {ClassName}(recordIds));
*/
public with sharing class {ClassName} implements Queueable /*, Database.AllowsCallouts */ {
// ─── Constants ───────────────────────────────────────────────────────
private static final Integer MAX_CHAIN_DEPTH = 5;
// ─── Instance Variables (Stateful) ───────────────────────────────────
private Set<Id> recordIds;
private Integer chainDepth;
// ─── Constructors ────────────────────────────────────────────────────
/**
* Creates a new queueable job to process the specified records
* @param recordIds Set of record Ids to process
*/
public {ClassName}(Set<Id> recordIds) {
this(recordIds, 0);
}
/**
* Creates a new queueable job with chain depth tracking
* @param recordIds Set of record Ids to process
* @param chainDepth Current depth in the queueable chain
*/
public {ClassName}(Set<Id> recordIds, Integer chainDepth) {
this.recordIds = recordIds ?? new Set<Id>();
this.chainDepth = chainDepth;
}
// ─── Queueable Interface ─────────────────────────────────────────────
/**
* Executes the asynchronous work
* @param context The queueable context
*/
public void execute(QueueableContext context) {
if (this.recordIds.isEmpty()) {
return;
}
try {
// TODO: Implement the async processing logic
// List<{SObject}> records = {SObject}Selector.selectByIds(this.recordIds);
// ... process records ...
// Chain to next job if there's more work and we haven't hit the depth limit
chainIfNeeded();
} catch (Exception e) {
handleError(context.getJobId(), e);
}
}
// ─── Private Helpers ─────────────────────────────────────────────────
/**
* Chains to the next queueable job if needed, with depth guard
*/
private void chainIfNeeded() {
// TODO: Determine if chaining is needed (e.g., remaining records to process)
Set<Id> remainingIds = new Set<Id>();
if (!remainingIds.isEmpty() && this.chainDepth < MAX_CHAIN_DEPTH) {
if (!Test.isRunningTest()) {
System.enqueueJob(new {ClassName}(remainingIds, this.chainDepth + 1));
}
}
}
/**
* Handles errors during execution
* @param jobId The async job Id
* @param e The exception that occurred
*/
private void handleError(Id jobId, Exception e) {
System.debug(LoggingLevel.ERROR,
'{ClassName} failed (Job: ' + jobId + '): ' +
e.getMessage() + '\n' + e.getStackTraceString()
);
// TODO: Persist error to a log object or send notification
}
}
/**
* REST API endpoint for {SObject} operations.
* Exposes CRUD operations via @RestResource at /services/apexrest/{urlPath}/v1/*.
* Uses versioned URL mapping for future-proof API evolution.
* All queries use WITH USER_MODE for CRUD/FLS enforcement.
*
* Authentication: OAuth 2.0 via Connected App
* Base URL: /services/apexrest/{urlPath}/v1/
*/
@RestResource(urlMapping='/{urlPath}/v1/*')
global with sharing class {ClassName} {
// ─── Constants ────────────────────────────────────────────────────────
private static final Integer DEFAULT_PAGE_SIZE = 20;
private static final Integer MAX_PAGE_SIZE = 200;
private static final String ERROR_MISSING_ID = 'Record Id is required in the URL path.';
private static final String ERROR_INVALID_ID = 'Invalid Salesforce Id format.';
private static final String ERROR_MISSING_BODY = 'Request body is required.';
private static final String ERROR_NOT_FOUND = '{SObject} record not found.';
private static final String ERROR_INSUFFICIENT_ACCESS = 'Insufficient access to perform this operation.';
// ─── GET — Retrieve ───────────────────────────────────────────────────
/**
* Retrieves a single {SObject} by Id (URL path) or a paginated list (query params).
* Single: GET /services/apexrest/{urlPath}/v1/{recordId}
* List: GET /services/apexrest/{urlPath}/v1?pageSize=20&offset=0
* @return ApiResponse containing the requested data
*/
@HttpGet
global static ApiResponse doGet() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
try {
String recordId = extractIdFromUri(req.requestURI);
if (String.isNotBlank(recordId)) {
return getSingleRecord(recordId, res);
}
return getRecordList(req, res);
} catch (Exception e) {
return handleError(res, 500, e.getMessage());
}
}
// ─── POST — Create ───────────────────────────────────────────────────
/**
* Creates a new {SObject} record from the JSON request body.
* POST /services/apexrest/{urlPath}/v1/
* @return ApiResponse with the created record Id
*/
@HttpPost
global static ApiResponse doPost() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
try {
if (req.requestBody == null || String.isBlank(req.requestBody.toString())) {
return handleError(res, 400, ERROR_MISSING_BODY);
}
{ClassName}Request payload = ({ClassName}Request) JSON.deserialize(
req.requestBody.toString(),
{ClassName}Request.class
);
// TODO: Map request payload to SObject fields
{SObject} record = new {SObject}(
Name = payload.name
);
Database.SaveResult saveResult = Database.insert(record, true);
if (saveResult.isSuccess()) {
res.statusCode = 201;
return new ApiResponse(true, 'Record created successfully.', record.Id);
}
return handleError(res, 422, saveResult.getErrors()[0].getMessage());
} catch (JSONException e) {
return handleError(res, 400, 'Malformed JSON: ' + e.getMessage());
} catch (DmlException e) {
return handleError(res, 422, 'Validation failed: ' + e.getDmlMessage(0));
} catch (Exception e) {
return handleError(res, 500, e.getMessage());
}
}
// ─── PATCH — Partial Update ───────────────────────────────────────────
/**
* Partially updates an existing {SObject} record.
* PATCH /services/apexrest/{urlPath}/v1/{recordId}
* @return ApiResponse confirming the update
*/
@HttpPatch
global static ApiResponse doPatch() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
try {
String recordId = extractIdFromUri(req.requestURI);
if (String.isBlank(recordId)) {
return handleError(res, 400, ERROR_MISSING_ID);
}
if (!isValidSalesforceId(recordId)) {
return handleError(res, 400, ERROR_INVALID_ID);
}
if (req.requestBody == null || String.isBlank(req.requestBody.toString())) {
return handleError(res, 400, ERROR_MISSING_BODY);
}
List<{SObject}> existing = [
SELECT Id FROM {SObject} WHERE Id = :recordId WITH USER_MODE LIMIT 1
];
if (existing.isEmpty()) {
return handleError(res, 404, ERROR_NOT_FOUND);
}
Map<String, Object> fieldUpdates = (Map<String, Object>) JSON.deserializeUntyped(
req.requestBody.toString()
);
{SObject} record = existing[0];
// TODO: Apply allowed field updates from the payload to the record
// for (String fieldName : fieldUpdates.keySet()) {
// record.put(fieldName, fieldUpdates.get(fieldName));
// }
Database.SaveResult saveResult = Database.update(record, true);
if (saveResult.isSuccess()) {
res.statusCode = 200;
return new ApiResponse(true, 'Record updated successfully.', record.Id);
}
return handleError(res, 422, saveResult.getErrors()[0].getMessage());
} catch (JSONException e) {
return handleError(res, 400, 'Malformed JSON: ' + e.getMessage());
} catch (DmlException e) {
return handleError(res, 422, 'Validation failed: ' + e.getDmlMessage(0));
} catch (Exception e) {
return handleError(res, 500, e.getMessage());
}
}
// ─── DELETE — Remove ──────────────────────────────────────────────────
/**
* Deletes a {SObject} record by Id.
* DELETE /services/apexrest/{urlPath}/v1/{recordId}
* @return ApiResponse confirming the deletion
*/
@HttpDelete
global static ApiResponse doDelete() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
try {
String recordId = extractIdFromUri(req.requestURI);
if (String.isBlank(recordId)) {
return handleError(res, 400, ERROR_MISSING_ID);
}
if (!isValidSalesforceId(recordId)) {
return handleError(res, 400, ERROR_INVALID_ID);
}
List<{SObject}> existing = [
SELECT Id FROM {SObject} WHERE Id = :recordId WITH USER_MODE LIMIT 1
];
if (existing.isEmpty()) {
return handleError(res, 404, ERROR_NOT_FOUND);
}
Database.DeleteResult deleteResult = Database.delete(existing[0], true);
if (deleteResult.isSuccess()) {
res.statusCode = 200;
return new ApiResponse(true, 'Record deleted successfully.', recordId);
}
return handleError(res, 422, deleteResult.getErrors()[0].getMessage());
} catch (DmlException e) {
return handleError(res, 422, e.getDmlMessage(0));
} catch (Exception e) {
return handleError(res, 500, e.getMessage());
}
}
// ─── Private Helpers ──────────────────────────────────────────────────
private static ApiResponse getSingleRecord(String recordId, RestResponse res) {
if (!isValidSalesforceId(recordId)) {
return handleError(res, 400, ERROR_INVALID_ID);
}
List<{SObject}> records = [
SELECT Id, Name, CreatedDate, LastModifiedDate
FROM {SObject}
WHERE Id = :recordId
WITH USER_MODE
LIMIT 1
];
if (records.isEmpty()) {
return handleError(res, 404, ERROR_NOT_FOUND);
}
res.statusCode = 200;
ApiResponse response = new ApiResponse(true, 'Record retrieved successfully.', recordId);
response.data = records[0];
return response;
}
private static ApiResponse getRecordList(RestRequest req, RestResponse res) {
Integer pageSize = getIntParam(req, 'pageSize', DEFAULT_PAGE_SIZE);
Integer offset = getIntParam(req, 'offset', 0);
pageSize = Math.min(pageSize, MAX_PAGE_SIZE);
List<{SObject}> records = [
SELECT Id, Name, CreatedDate, LastModifiedDate
FROM {SObject}
WITH USER_MODE
ORDER BY Name ASC
LIMIT :pageSize
OFFSET :offset
];
res.statusCode = 200;
ApiResponse response = new ApiResponse(true, 'Records retrieved successfully.', null);
response.records = records;
response.pageSize = pageSize;
response.offset = offset;
return response;
}
private static String extractIdFromUri(String uri) {
String lastSegment = uri.substringAfterLast('/');
if (String.isBlank(lastSegment) || lastSegment == 'v1') {
return null;
}
return lastSegment;
}
private static Boolean isValidSalesforceId(String idValue) {
return Pattern.matches('[a-zA-Z0-9]{15,18}', idValue);
}
private static Integer getIntParam(RestRequest req, String paramName, Integer defaultValue) {
String paramValue = req.params.get(paramName);
if (String.isBlank(paramValue)) {
return defaultValue;
}
try {
return Integer.valueOf(paramValue);
} catch (TypeException e) {
return defaultValue;
}
}
private static ApiResponse handleError(RestResponse res, Integer statusCode, String message) {
res.statusCode = statusCode;
return new ApiResponse(false, message, null);
}
// ─── Request / Response DTOs ──────────────────────────────────────────
/**
* Inbound request payload for POST operations.
* Extend with additional fields as needed.
*/
global class {ClassName}Request {
global String name;
// TODO: Add fields matching the expected JSON request body
}
/**
* Standardized API response envelope.
* All endpoints return this structure for consistent client parsing.
*/
global class ApiResponse {
global Boolean success;
global String message;
global String recordId;
global SObject data;
global List<SObject> records;
global Integer pageSize;
global Integer offset;
global ApiResponse(Boolean success, String message, String recordId) {
this.success = success;
this.message = message;
this.recordId = recordId;
}
}
}/**
* Schedulable Apex class for {describe the scheduled operation}.
* Delegates heavy processing to a Batch or Queueable job.
* Keep execute() lightweight — it should only launch other jobs.
*
* @example
* // Schedule to run daily at 2 AM
* String jobId = System.schedule(
* '{ClassName} - Daily',
* {ClassName}.CRON_DAILY_2AM,
* new {ClassName}()
* );
*
* // Or use the convenience method
* String jobId = {ClassName}.scheduleDaily();
*/
public with sharing class {ClassName} implements Schedulable {
// ─── CRON Expressions ────────────────────────────────────────────────
// Seconds Minutes Hours Day_of_month Month Day_of_week Optional_year
/** Runs daily at 2:00 AM */
public static final String CRON_DAILY_2AM = '0 0 2 * * ?';
/** Runs every weekday at 6:00 AM */
public static final String CRON_WEEKDAYS_6AM = '0 0 6 ? * MON-FRI';
/** Runs hourly at the top of the hour */
public static final String CRON_HOURLY = '0 0 * * * ?';
// ─── Schedulable Interface ───────────────────────────────────────────
/**
* Entry point for the scheduled execution.
* Delegates to a Batch or Queueable for the actual work.
* @param sc The schedulable context
*/
public void execute(SchedulableContext sc) {
// Option A: Launch a Batch job
// Database.executeBatch(new {BatchClassName}(), 200);
// Option B: Launch a Queueable job
// System.enqueueJob(new {QueueableClassName}(params));
// TODO: Implement delegation to appropriate async job
}
// ─── Convenience Scheduling Methods ──────────────────────────────────
/**
* Schedules this job to run daily at 2 AM
* @return The scheduled job Id
*/
public static String scheduleDaily() {
return System.schedule(
'{ClassName} - Daily 2AM',
CRON_DAILY_2AM,
new {ClassName}()
);
}
/**
* Aborts this scheduled job by name
* @param jobName The name used when scheduling
*/
public static void abort(String jobName) {
for (CronTrigger ct : [
SELECT Id FROM CronTrigger
WHERE CronJobDetail.Name = :jobName
]) {
System.abortJob(ct.Id);
}
}
}
/**
* Selector class for {SObject} queries.
* Encapsulates all SOQL for {SObject} records.
* All methods return bulkified results (Lists or Maps).
*/
public inherited sharing class {SObject}Selector {
// ─── Field Lists ─────────────────────────────────────────────────────
/**
* Returns the default set of fields to query for {SObject}.
* Centralizes field references to keep queries DRY.
* @return Comma-separated field list as a String
*/
private static String getDefaultFields() {
return String.join(
new List<String>{
'Id',
'Name',
'CreatedDate',
'LastModifiedDate'
// TODO: Add additional fields here
},
', '
);
}
// ─── Query Methods ───────────────────────────────────────────────────
/**
* Selects {SObject} records by their Ids
* @param recordIds Set of {SObject} Ids to query
* @return List of {SObject} records matching the provided Ids
* @example
* Set<Id> ids = new Set<Id>{ '001xx000003DGbY' };
* List<{SObject}> results = {SObject}Selector.selectByIds(ids);
*/
public static List<{SObject}> selectByIds(Set<Id> recordIds) {
if (recordIds == null || recordIds.isEmpty()) {
return new List<{SObject}>();
}
return Database.query(
'SELECT ' + getDefaultFields() +
' FROM {SObject}' +
' WHERE Id IN :recordIds'
);
}
/**
* Selects {SObject} records as a Map keyed by Id
* @param recordIds Set of {SObject} Ids to query
* @return Map of Id to {SObject}
*/
public static Map<Id, {SObject}> selectMapByIds(Set<Id> recordIds) {
return new Map<Id, {SObject}>(selectByIds(recordIds));
}
/**
* Selects {SObject} records by a specific field value
* @param fieldName API name of the field to filter on
* @param values Set of values to match
* @return List of matching {SObject} records
* @example
* List<{SObject}> results = {SObject}Selector.selectByField('Status__c', new Set<String>{ 'Active' });
*/
public static List<{SObject}> selectByField(String fieldName, Set<String> values) {
if (String.isBlank(fieldName) || values == null || values.isEmpty()) {
return new List<{SObject}>();
}
// Validate field name to prevent SOQL injection
Schema.SObjectField field = Schema.SObjectType.{SObject}.fields.getMap().get(fieldName);
if (field == null) {
throw new QueryException('Invalid field name: ' + fieldName);
}
return Database.query(
'SELECT ' + getDefaultFields() +
' FROM {SObject}' +
' WHERE ' + String.escapeSingleQuotes(fieldName) + ' IN :values'
);
}
// ─── Exception ───────────────────────────────────────────────────────
/**
* Custom exception for query errors
*/
public class QueryException extends Exception {}
}
/**
* Service class for {SObject} business logic.
* Follows separation of concerns: delegates queries to {SObject}Selector
* and SObject manipulation to {SObject}Domain where applicable.
*/
public with sharing class {SObject}Service {
// ─── Constants ───────────────────────────────────────────────────────
private static final String ERROR_NULL_INPUT = 'Input cannot be null or empty.';
// ─── Public API ──────────────────────────────────────────────────────
/**
* {Describe the primary operation}
* @param recordIds Set of {SObject} Ids to process
* @return List of processed {SObject} records
* @throws {SObject}ServiceException if processing fails
* @example
* Set<Id> ids = new Set<Id>{ '001xx000003DGbY' };
* List<{SObject}> results = {SObject}Service.process{SObject}s(ids);
*/
public static List<{SObject}> process{SObject}s(Set<Id> recordIds) {
// Guard clause
if (recordIds == null || recordIds.isEmpty()) {
throw new {SObject}ServiceException(ERROR_NULL_INPUT);
}
// Query via Selector
List<{SObject}> records = {SObject}Selector.selectByIds(recordIds);
// Apply business logic
// TODO: Implement business logic here
// DML
try {
update records;
} catch (DmlException e) {
throw new {SObject}ServiceException(
'Failed to update {SObject} records: ' + e.getMessage()
);
}
return records;
}
// ─── Convenience Overloads ───────────────────────────────────────────
/**
* Single-record convenience overload
* @param recordId The {SObject} Id to process
* @return The processed {SObject} record
*/
public static {SObject} process{SObject}(Id recordId) {
List<{SObject}> results = process{SObject}s(new Set<Id>{ recordId });
return results.isEmpty() ? null : results[0];
}
// ─── Private Helpers ─────────────────────────────────────────────────
// TODO: Add private helper methods as needed
// ─── Exception ───────────────────────────────────────────────────────
/**
* Custom exception for {SObject}Service errors
*/
public class {SObject}ServiceException extends Exception {}
}
/**
* {SObject} Trigger
*/
trigger {SObject}Trigger on {SObject} (
before insert,
before update,
before delete,
after insert,
after update,
after delete,
after undelete
) {
// ─── Option A: Trigger Actions Framework (TAF) ───────────────────────
// Delegates to Trigger_Action__mdt-registered action classes.
// Each action class handles one concern in one context.
//
// new MetadataTriggerHandler().run();
// ─── Option B: Custom Handler Pattern ────────────────────────────────
// Delegates to a single handler class that routes by context.
//
// {SObject}TriggerHandler handler = new {SObject}TriggerHandler();
//
// if (Trigger.isBefore) {
// if (Trigger.isInsert) {
// handler.beforeInsert(Trigger.new);
// } else if (Trigger.isUpdate) {
// handler.beforeUpdate(Trigger.new, Trigger.oldMap);
// } else if (Trigger.isDelete) {
// handler.beforeDelete(Trigger.old, Trigger.oldMap);
// }
// } else if (Trigger.isAfter) {
// if (Trigger.isInsert) {
// handler.afterInsert(Trigger.new, Trigger.newMap);
// } else if (Trigger.isUpdate) {
// handler.afterUpdate(Trigger.new, Trigger.oldMap);
// } else if (Trigger.isDelete) {
// handler.afterDelete(Trigger.old, Trigger.oldMap);
// } else if (Trigger.isUndelete) {
// handler.afterUndelete(Trigger.new, Trigger.newMap);
// }
// }
// TODO: Uncomment one option above and remove the other
}
/**
* Utility class for {describe the category of utilities: String, Date, Collection, etc.}.
* All methods are static and side-effect-free (no SOQL, no DML).
* Private constructor prevents instantiation.
*/
public with sharing class {ClassName} {
// ─── Private Constructor ─────────────────────────────────────────────
/**
* Prevents instantiation — use static methods only
*/
@TestVisible
private {ClassName}() {
// Utility class — do not instantiate
}
// ─── Public Methods ──────────────────────────────────────────────────
// TODO: Add utility methods below. Examples:
/**
* Safely converts a String to an Integer, returning a default if parsing fails
* @param value The String to parse
* @param defaultValue The fallback value if parsing fails
* @return The parsed Integer or the default value
* @example
* Integer result = {ClassName}.safeParseInteger('42', 0); // returns 42
* Integer result = {ClassName}.safeParseInteger('abc', 0); // returns 0
*/
public static Integer safeParseInteger(String value, Integer defaultValue) {
if (String.isBlank(value)) {
return defaultValue;
}
try {
return Integer.valueOf(value.trim());
} catch (TypeException e) {
return defaultValue;
}
}
/**
* Chunks a list into smaller sublists of the specified size.
* Useful for processing records in governor-limit-safe batches.
* @param items The list to chunk
* @param chunkSize The maximum size of each chunk
* @return A list of sublists, each containing up to chunkSize elements
* @example
* List<List<String>> chunks = {ClassName}.chunkList(myList, 200);
*/
public static List<List<Object>> chunkList(List<Object> items, Integer chunkSize) {
List<List<Object>> chunks = new List<List<Object>>();
if (items == null || items.isEmpty() || chunkSize <= 0) {
return chunks;
}
List<Object> currentChunk = new List<Object>();
for (Object item : items) {
currentChunk.add(item);
if (currentChunk.size() == chunkSize) {
chunks.add(currentChunk);
currentChunk = new List<Object>();
}
}
if (!currentChunk.isEmpty()) {
chunks.add(currentChunk);
}
return chunks;
}
/**
* Extracts a Set of non-null field values from a list of SObjects
* @param records The SObject records to extract from
* @param fieldName The API name of the field to extract
* @return A Set of non-null String values
* @example
* Set<String> emails = {ClassName}.pluckStrings(contacts, 'Email');
*/
public static Set<String> pluckStrings(List<SObject> records, String fieldName) {
Set<String> values = new Set<String>();
if (records == null || String.isBlank(fieldName)) {
return values;
}
for (SObject record : records) {
Object val = record.get(fieldName);
if (val != null) {
values.add(String.valueOf(val));
}
}
return values;
}
}
Credits & Acknowledgments
This skill is built on established Salesforce Apex development patterns and the collective knowledge of the Salesforce developer community.
---
/**
* Batch Apex class for identifying and flagging duplicate Account records.
* Compares Accounts by Name and BillingPostalCode to find potential duplicates.
* Flags duplicates by setting the Is_Potential_Duplicate__c checkbox.
* Implements Database.Stateful to track results across batch chunks.
*
* @example
* // Run with default batch size (200)
* Id jobId = AccountDeduplicationBatch.run();
*
* // Run with smaller batch size for complex processing
* Database.executeBatch(new AccountDeduplicationBatch(), 50);
*/
public with sharing class AccountDeduplicationBatch implements Database.Batchable<SObject>, Database.Stateful {
// ─── Constants ───────────────────────────────────────────────────────
private static final Integer DEFAULT_BATCH_SIZE = 200;
// ─── Stateful Tracking ───────────────────────────────────────────────
private Integer totalScanned = 0;
private Integer duplicatesFound = 0;
private Integer totalErrors = 0;
private List<String> errorMessages = new List<String>();
// ─── Batchable Interface ─────────────────────────────────────────────
/**
* Queries all active Accounts that haven't already been flagged
* @param bc The batch context
* @return QueryLocator scoped to unflagged active Accounts
*/
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id, Name, BillingPostalCode, Is_Potential_Duplicate__c
FROM Account
WHERE IsDeleted = FALSE
AND Is_Potential_Duplicate__c = FALSE
ORDER BY Name ASC
]);
}
/**
* Processes each batch by building a duplicate key and checking for matches.
* Uses a composite key of normalized Name + BillingPostalCode.
* @param bc The batch context
* @param scope List of Account records in the current batch
*/
public void execute(Database.BatchableContext bc, List<Account> scope) {
totalScanned += scope.size();
// Build duplicate keys for this batch
Map<String, List<Account>> dupeKeyMap = new Map<String, List<Account>>();
for (Account acct : scope) {
String key = buildDuplicateKey(acct);
if (String.isNotBlank(key)) {
if (!dupeKeyMap.containsKey(key)) {
dupeKeyMap.put(key, new List<Account>());
}
dupeKeyMap.get(key).add(acct);
}
}
// Flag records that share a key with another record
List<Account> toUpdate = new List<Account>();
for (String key : dupeKeyMap.keySet()) {
List<Account> group = dupeKeyMap.get(key);
if (group.size() > 1) {
for (Account acct : group) {
toUpdate.add(new Account(
Id = acct.Id,
Is_Potential_Duplicate__c = true
));
}
}
}
if (!toUpdate.isEmpty()) {
List<Database.SaveResult> results = Database.update(toUpdate, false);
processResults(results);
}
}
/**
* Logs a summary of the deduplication batch run
* @param bc The batch context
*/
public void finish(Database.BatchableContext bc) {
String summary = String.format(
'AccountDeduplicationBatch completed. Scanned: {0}, Duplicates flagged: {1}, Errors: {2}',
new List<Object>{ totalScanned, duplicatesFound, totalErrors }
);
System.debug(LoggingLevel.INFO, summary);
if (!errorMessages.isEmpty()) {
System.debug(LoggingLevel.ERROR, 'Error details:\n' + String.join(errorMessages, '\n'));
}
}
// ─── Private Helpers ─────────────────────────────────────────────────
/**
* Builds a normalized composite key for duplicate detection
* @param acct The Account record
* @return Normalized key string, or null if insufficient data
*/
private String buildDuplicateKey(Account acct) {
if (String.isBlank(acct.Name)) {
return null;
}
String normalizedName = acct.Name.trim().toUpperCase().replaceAll('\\s+', ' ');
String postalCode = (acct.BillingPostalCode ?? '').trim().toUpperCase();
return normalizedName + '|' + postalCode;
}
/**
* Processes DML results, tracking successes and failures
* @param results List of Database.SaveResult from update operation
*/
private void processResults(List<Database.SaveResult> results) {
for (Database.SaveResult sr : results) {
if (sr.isSuccess()) {
duplicatesFound++;
} else {
totalErrors++;
for (Database.Error err : sr.getErrors()) {
errorMessages.add(
'Record ' + sr.getId() + ': ' +
err.getStatusCode() + ' - ' + err.getMessage()
);
}
}
}
}
// ─── Static Helpers ──────────────────────────────────────────────────
/**
* Convenience method to execute with default batch size
* @return The batch job Id
*/
public static Id run() {
return Database.executeBatch(new AccountDeduplicationBatch(), DEFAULT_BATCH_SIZE);
}
}
/**
* Selector class for Account queries.
* Encapsulates all SOQL for Account records.
* All methods return bulkified results (Lists or Maps).
*/
public with sharing class AccountSelector {
// ─── Field Lists ─────────────────────────────────────────────────────
/**
* Returns the default set of fields to query for Account.
* Centralizes field references to keep queries DRY.
* @return Comma-separated field list as a String
*/
private static String getDefaultFields() {
return String.join(
new List<String>{
'Id',
'Name',
'AccountNumber',
'Type',
'Industry',
'AnnualRevenue',
'NumberOfEmployees',
'Phone',
'Website',
'OwnerId',
'CreatedDate',
'LastModifiedDate'
},
', '
);
}
/**
* Returns fields needed for billing/territory operations
* @return Comma-separated field list as a String
*/
private static String getBillingFields() {
return String.join(
new List<String>{
'Id',
'Name',
'BillingStreet',
'BillingCity',
'BillingState',
'BillingPostalCode',
'BillingCountry',
'Territory__c'
},
', '
);
}
// ─── Query Methods ───────────────────────────────────────────────────
/**
* Selects Account records by their Ids
* @param recordIds Set of Account Ids to query
* @return List of Account records matching the provided Ids
* @example
* Set<Id> ids = new Set<Id>{ '001xx000003DGbY' };
* List<Account> results = AccountSelector.selectByIds(ids);
*/
public static List<Account> selectByIds(Set<Id> recordIds) {
if (recordIds == null || recordIds.isEmpty()) {
return new List<Account>();
}
return Database.query(
'SELECT ' + getDefaultFields() +
' FROM Account' +
' WHERE Id IN :recordIds'
);
}
/**
* Selects Account records as a Map keyed by Id
* @param recordIds Set of Account Ids to query
* @return Map of Id to Account
*/
public static Map<Id, Account> selectMapByIds(Set<Id> recordIds) {
return new Map<Id, Account>(selectByIds(recordIds));
}
/**
* Selects Accounts with billing address fields for territory assignment
* @param recordIds Set of Account Ids to query
* @return List of Account records with billing address fields populated
*/
public static List<Account> selectWithBillingAddress(Set<Id> recordIds) {
if (recordIds == null || recordIds.isEmpty()) {
return new List<Account>();
}
return Database.query(
'SELECT ' + getBillingFields() +
' FROM Account' +
' WHERE Id IN :recordIds'
);
}
/**
* Selects Accounts by Account Type
* @param accountTypes Set of Account Type values to filter by
* @return List of matching Account records
* @example
* List<Account> prospects = AccountSelector.selectByType(
* new Set<String>{ 'Prospect', 'Customer - Direct' }
* );
*/
public static List<Account> selectByType(Set<String> accountTypes) {
if (accountTypes == null || accountTypes.isEmpty()) {
return new List<Account>();
}
return Database.query(
'SELECT ' + getDefaultFields() +
' FROM Account' +
' WHERE Type IN :accountTypes' +
' ORDER BY Name ASC'
);
}
/**
* Selects Accounts by Industry with a minimum annual revenue
* @param industries Set of Industry values to filter by
* @param minRevenue Minimum AnnualRevenue threshold
* @return List of matching Account records ordered by revenue descending
* @example
* List<Account> techAccounts = AccountSelector.selectByIndustryAndRevenue(
* new Set<String>{ 'Technology' },
* 1000000
* );
*/
public static List<Account> selectByIndustryAndRevenue(Set<String> industries, Decimal minRevenue) {
if (industries == null || industries.isEmpty()) {
return new List<Account>();
}
Decimal revenueThreshold = minRevenue ?? 0;
return Database.query(
'SELECT ' + getDefaultFields() +
' FROM Account' +
' WHERE Industry IN :industries' +
' AND AnnualRevenue >= :revenueThreshold' +
' ORDER BY AnnualRevenue DESC'
);
}
/**
* Selects Accounts with their related Contacts (subquery)
* @param recordIds Set of Account Ids to query
* @return List of Account records with nested Contacts
*/
public static List<Account> selectWithContacts(Set<Id> recordIds) {
if (recordIds == null || recordIds.isEmpty()) {
return new List<Account>();
}
return [
SELECT Id, Name, Type, Industry,
(SELECT Id, FirstName, LastName, Email, Title
FROM Contacts
ORDER BY LastName ASC)
FROM Account
WHERE Id IN :recordIds
];
}
// ─── Aggregate Queries ───────────────────────────────────────────────
/**
* Returns a count of Accounts grouped by Industry
* @return List of AggregateResult with Industry and record count
* @example
* List<AggregateResult> results = AccountSelector.countByIndustry();
* for (AggregateResult ar : results) {
* System.debug(ar.get('Industry') + ': ' + ar.get('cnt'));
* }
*/
public static List<AggregateResult> countByIndustry() {
return [
SELECT Industry, COUNT(Id) cnt
FROM Account
WHERE Industry != NULL
GROUP BY Industry
ORDER BY COUNT(Id) DESC
];
}
}
/**
* Service class for Account business logic.
* Provides account deduplication, enrichment, and territory assignment.
* Delegates queries to AccountSelector and SObject manipulation to AccountDomain.
*/
public with sharing class AccountService {
// ─── Constants ───────────────────────────────────────────────────────
private static final String ERROR_NULL_INPUT = 'Input cannot be null or empty.';
private static final String ERROR_MERGE_FAILED = 'Account merge failed for master Id: ';
private static final Integer MAX_MERGE_BATCH = 3;
// ─── Public API ──────────────────────────────────────────────────────
/**
* Merges duplicate Account records into a master record.
* The master record retains its field values; child records are reparented.
* @param masterIds Map of master Account Id to Set of duplicate Account Ids to merge
* @return List of master Account Ids that were successfully merged
* @throws AccountServiceException if merge processing fails
* @example
* Map<Id, Set<Id>> mergeMap = new Map<Id, Set<Id>>{
* masterAcctId => new Set<Id>{ dupeId1, dupeId2 }
* };
* List<Id> mergedIds = AccountService.mergeAccounts(mergeMap);
*/
public static List<Id> mergeAccounts(Map<Id, Set<Id>> mergeMap) {
if (mergeMap == null || mergeMap.isEmpty()) {
throw new AccountServiceException(ERROR_NULL_INPUT);
}
// Collect all Ids for a single query
Set<Id> allIds = new Set<Id>();
allIds.addAll(mergeMap.keySet());
for (Set<Id> dupeIds : mergeMap.values()) {
allIds.addAll(dupeIds);
}
// Query all records at once via Selector
Map<Id, Account> accountMap = AccountSelector.selectMapByIds(allIds);
List<Id> successfulMerges = new List<Id>();
List<String> errors = new List<String>();
for (Id masterId : mergeMap.keySet()) {
Account master = accountMap.get(masterId);
if (master == null) {
errors.add('Master account not found: ' + masterId);
continue;
}
List<Account> duplicates = new List<Account>();
for (Id dupeId : mergeMap.get(masterId)) {
Account dupe = accountMap.get(dupeId);
if (dupe != null) {
duplicates.add(dupe);
}
}
try {
// Apex merge supports up to 3 records at a time
for (List<Account> chunk : chunkAccounts(duplicates, MAX_MERGE_BATCH)) {
Database.merge(master, chunk);
}
successfulMerges.add(masterId);
} catch (DmlException e) {
errors.add(ERROR_MERGE_FAILED + masterId + ' - ' + e.getMessage());
}
}
if (!errors.isEmpty()) {
System.debug(LoggingLevel.WARN, 'Merge errors: ' + String.join(errors, '\n'));
}
return successfulMerges;
}
/**
* Assigns accounts to territories based on Billing State/Country.
* Uses Custom Metadata Type (Territory_Mapping__mdt) for mappings.
* @param accountIds Set of Account Ids to assign territories for
* @return Number of accounts successfully updated
* @throws AccountServiceException if territory assignment fails
*/
public static Integer assignTerritories(Set<Id> accountIds) {
if (accountIds == null || accountIds.isEmpty()) {
throw new AccountServiceException(ERROR_NULL_INPUT);
}
List<Account> accounts = AccountSelector.selectWithBillingAddress(accountIds);
Map<String, String> territoryMap = loadTerritoryMappings();
List<Account> toUpdate = new List<Account>();
for (Account acct : accounts) {
String key = buildTerritoryKey(acct.BillingState, acct.BillingCountry);
String territory = territoryMap.get(key);
if (territory != null && territory != acct.Territory__c) {
toUpdate.add(new Account(
Id = acct.Id,
Territory__c = territory
));
}
}
if (!toUpdate.isEmpty()) {
List<Database.SaveResult> results = Database.update(toUpdate, false);
return countSuccesses(results);
}
return 0;
}
// ─── Convenience Overloads ───────────────────────────────────────────
/**
* Single-account territory assignment convenience method
* @param accountId The Account Id to assign a territory for
* @return 1 if updated, 0 if no change needed
*/
public static Integer assignTerritory(Id accountId) {
return assignTerritories(new Set<Id>{ accountId });
}
// ─── Private Helpers ─────────────────────────────────────────────────
/**
* Loads territory mappings from Custom Metadata
* @return Map of territory key (State:Country) to territory name
*/
private static Map<String, String> loadTerritoryMappings() {
Map<String, String> mappings = new Map<String, String>();
for (Territory_Mapping__mdt mapping : Territory_Mapping__mdt.getAll().values()) {
String key = buildTerritoryKey(mapping.State__c, mapping.Country__c);
mappings.put(key, mapping.Territory_Name__c);
}
return mappings;
}
/**
* Builds a consistent territory lookup key
* @param state The billing state
* @param country The billing country
* @return A normalized key string
*/
private static String buildTerritoryKey(String state, String country) {
return (state ?? '').toUpperCase() + ':' + (country ?? '').toUpperCase();
}
/**
* Chunks a list of Accounts into sublists of the given size
* @param accounts The accounts to chunk
* @param chunkSize Maximum chunk size
* @return List of account sublists
*/
private static List<List<Account>> chunkAccounts(List<Account> accounts, Integer chunkSize) {
List<List<Account>> chunks = new List<List<Account>>();
List<Account> current = new List<Account>();
for (Account acct : accounts) {
current.add(acct);
if (current.size() == chunkSize) {
chunks.add(current);
current = new List<Account>();
}
}
if (!current.isEmpty()) {
chunks.add(current);
}
return chunks;
}
/**
* Counts successful results from a DML operation
* @param results List of Database.SaveResult
* @return Count of successful operations
*/
private static Integer countSuccesses(List<Database.SaveResult> results) {
Integer count = 0;
for (Database.SaveResult sr : results) {
if (sr.isSuccess()) {
count++;
} else {
for (Database.Error err : sr.getErrors()) {
System.debug(LoggingLevel.WARN,
'Update failed for ' + sr.getId() + ': ' + err.getMessage()
);
}
}
}
return count;
}
// ─── Exception ───────────────────────────────────────────────────────
/**
* Custom exception for AccountService errors
*/
public class AccountServiceException extends Exception {}
}
Related skills
How it compares
Choose this when you need opinionated Apex abstract-class scaffolds for integrations rather than generic Apex snippet generators.
FAQ
What does generating-apex produce?
generating-apex produces consistent Salesforce Apex classes and abstract base classes with shared behavior, documented override methods, and callout examples such as named credential endpoints for integration services.
When should developers use generating-apex?
Developers should use generating-apex when building Salesforce integration or backend service families that need reusable abstract classes instead of inconsistent one-off Apex implementations.
Is Generating Apex safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.