What gets generated
salesforce/
├── sfdx-project.json # SFDX project root: packageDirectories, sourceApiVersion
├── config/
│ └── project-scratch-def.json # Scratch org definition: features, orgPreferences
├── force-app/main/default/
│ ├── objects/
│ │ └── {Entity}__c/
│ │ └── {Entity}__c.object-meta.xml # Custom object: fields, relationships,
│ │ # listViews, searchLayouts
│ ├── classes/
│ │ ├── {Entity}Service.cls # Apex service: SOQL queries, DML, Unit of Work
│ │ ├── {Entity}Service.cls-meta.xml
│ │ ├── {Entity}TriggerHandler.cls # Trigger handler: beforeInsert, afterInsert,
│ │ │ # beforeUpdate, afterUpdate, beforeDelete
│ │ ├── {Entity}TriggerHandler.cls-meta.xml
│ │ ├── {Entity}Test.cls # Test class: ≥75% coverage, TestSetup,
│ │ │ # positive + negative + bulk test methods
│ │ ├── {Entity}Test.cls-meta.xml
│ │ └── AuditTrailService.cls # Emitted when audit_trail capability selected
│ ├── triggers/
│ │ ├── {Entity}Trigger.trigger # One trigger per object: delegates to handler
│ │ └── {Entity}Trigger.trigger-meta.xml
│ ├── lwc/
│ │ └── {entity}List/
│ │ ├── {entity}List.html # Wire adapter + template directives
│ │ ├── {entity}List.js # @wire, @track, @api, event handlers
│ │ ├── {entity}List.css
│ │ └── {entity}List.js-meta.xml # targets: lightning__AppPage, etc.
│ ├── permissionsets/
│ │ └── {AppName}_Admin.permissionset-meta.xml # Object + field permissions
│ └── namedCredentials/
│ └── {IntegrationName}.namedCredential-meta.xml # External callout auth
└── .github/workflows/
└── salesforce-ci.yml # GitHub Actions: validate → test → deploy to sandbox/prod
Apex trigger handler pattern
// triggers/ProductTrigger.trigger
trigger ProductTrigger on Product__c (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
ProductTriggerHandler handler = new ProductTriggerHandler();
if (Trigger.isBefore) {
if (Trigger.isInsert) handler.beforeInsert(Trigger.new);
if (Trigger.isUpdate) handler.beforeUpdate(Trigger.new, Trigger.oldMap);
}
if (Trigger.isAfter) {
if (Trigger.isInsert) handler.afterInsert(Trigger.new);
}
}
The trigger is dumb — one if per context, delegates immediately. All business logic lives in the handler class. This is the correct pattern; a trigger with inline DML is the single most common Salesforce tech debt smell.
Apex service with proper bulkification
public with sharing class ProductService {
public static List<Product__c> getByStatus(String status) {
return [
SELECT Id, Name, Price__c, Status__c, CreatedDate
FROM Product__c
WHERE Status__c = :status
WITH SECURITY_ENFORCED
ORDER BY CreatedDate DESC
LIMIT 200
];
}
public static void updateStatus(List<Product__c> products, String newStatus) {
for (Product__c p : products) {
p.Status__c = newStatus;
}
update products;
}
}
Every SOQL query uses WITH SECURITY_ENFORCED, every DML operates on lists (not individual records), and the governor limit at 200 is correct for SOQL row queries. These aren't style choices — they're deployment blockers when wrong.
LWC with @wire adapter
// lwc/productList/productList.js
import { LightningElement, wire } from 'lwc';
import getByStatus from '@salesforce/apex/ProductService.getByStatus';
export default class ProductList extends LightningElement {
@track products = [];
@track error;
@wire(getByStatus, { status: 'Active' })
wiredProducts({ data, error }) {
if (data) { this.products = data; this.error = undefined; }
if (error) { this.error = error; this.products = []; }
}
}
State machine transitions as Apex
When your genome declares state machine transitions, the handler emits a transition method with a from-state guard:
public static void transition(Product__c record, String trigger) {
Map<String, Map<String, String>> transitions = new Map<String, Map<String, String>>{
'Draft' => new Map<String, String>{ 'submit' => 'Pending Review' },
'Pending Review' => new Map<String, String>{ 'approve' => 'Active', 'reject' => 'Draft' }
};
String nextState = transitions.get(record.Status__c)?.get(trigger);
if (nextState == null) throw new IllegalArgumentException(
'No transition ' + trigger + ' from state ' + record.Status__c
);
record.Status__c = nextState;
}
CI/CD pipeline
# .github/workflows/salesforce-ci.yml
jobs:
validate:
steps:
- run: sf org login jwt --client-id $SF_CLIENT_ID --jwt-key-file server.key
- run: sf project deploy validate --target-org sandbox
deploy:
needs: validate
if: github.ref == 'refs/heads/main'
steps:
- run: sf project deploy start --target-org production
Uses the Salesforce CLI v2 (sf) — not the deprecated sfdx commands.
What ships in docs/
docs/decisions/ADR-salesforce-trigger-handler-pattern.md— why one trigger per object with a handler, with named alternativesdocs/compliance/salesforce-security-posture.md—WITH SECURITY_ENFORCED, permission set model, field-level securitydocs/runbooks/salesforce-deploy-scratch-to-sandbox.md— step-by-step: scratch org setup, push metadata, run tests, deploy to sandbox
Internal links
- Dynamics 365 integration for the Microsoft CRM/ERP platform generator
- SAP CAP integration for SAP BTP custom application generation
CTA
Try it — free plan, no credit card. archiet.com.
Generate a Salesforce project. Open the trigger, the handler, and the test class. Check whether it's the shape you'd submit for code review.