The scaffold has to follow the trigger-handler pattern (triggers are dumb, logic lives in the handler class), the service must use the Unit of Work pattern, the LWC must separate HTML, JS, CSS, and meta, and the test class must cover at least 75% of lines or Salesforce won't let you deploy.
Archiet generates the full scaffold correctly from an architectural genome. The shape of every output file matches what a Salesforce-certified developer would write — because the templates were built from that standard.
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.