Series Note: This article is Part 5 of our hands-on engineering series: Building the Salesforce Appointment Scheduler. Follow along as we design and implement a multi-location scheduling platform on Salesforce using enterprise patterns and developer-first tooling.
The “God Trigger” Anti-Pattern in Salesforce
Apex triggers are one of the most powerful features on the Salesforce platform, enabling immediate server-side execution whenever records are created, updated, or deleted.
Unfortunately, triggers are also where code quality most frequently degenerates. Common anti-patterns include:
- The 800-Line “God Trigger”: Writing business logic, queries, and DML directly inside the
.triggerfile. This makes unit testing impossible without triggering full DML transactions and prevents logic reuse across batch jobs or APIs. - Multiple Triggers on a Single Object: Creating multiple triggers on the same object (e.g.
AppointmentBeforeTrigger.triggerandAppointmentAfterTrigger.trigger). Salesforce explicitly does not guarantee the execution order of multiple triggers on the same object, leading to non-deterministic behavior and intermittent bugs. - Trigger Boilerplate Fatigue: Writing the mandatory boilerplate for a clean trigger framework (trigger file, trigger handler class, metadata XMLs,
switch on Trigger.operationTypedispatchers) is repetitive, leading developers to cut corners and dump logic directly into the trigger.
In Part 5 of building the Salesforce Appointment Scheduler, we enforce Architecture by Default using Hob: The Salesforce House-Elf (hob create trigger).
Scaffolding the Trigger Framework with Hob
With one command, Hob scaffolds both the logic-less trigger and its companion handler class, pre-configured with complete separation of concerns and modern System.TriggerOperation dispatching:
hob create trigger Appointment__c
The Terminal Output
✨ Hob - The Salesforce House-Elf ✨
"Loyal, quiet, and tireless assistance for your Salesforce Development."
✔ Created Apex trigger 'AppointmentTrigger'
✔ Created TriggerHandler class 'AppointmentTriggerHandler'
🪄 Hob prepared your trigger and handler with clean Separation of Concerns:
Trigger: force-app/main/default/triggers/AppointmentTrigger.trigger
Handler: force-app/main/default/classes/AppointmentTriggerHandler.cls
SObject: Appointment__c
1. The Logic-less Trigger (AppointmentTrigger.trigger)
The generated trigger contains zero business logic. Its sole responsibility is delegating to the handler:
trigger AppointmentTrigger on Appointment__c (
before insert,
before update,
before delete,
after insert,
after update,
after delete,
after undelete
) {
AppointmentTriggerHandler.handleTrigger(
Trigger.operationType,
Trigger.new,
Trigger.oldMap
);
}
2. The Handler Dispatcher (AppointmentTriggerHandler.cls)
Hob provides clean routing using Salesforce’s System.TriggerOperation enum:
switch on operationType {
when BEFORE_INSERT { onBeforeInsert(newList); }
when AFTER_INSERT { onAfterInsert(newList); }
when BEFORE_UPDATE { onBeforeUpdate(newList, oldMap); }
when AFTER_UPDATE { onAfterUpdate(newList, oldMap); }
when BEFORE_DELETE { onBeforeDelete(oldMap); }
when AFTER_DELETE { onAfterDelete(oldMap); }
when AFTER_UNDELETE{ onAfterUndelete(newList); }
}
Domain Automation in Action
With the trigger framework scaffolded in seconds, we implemented the automated business workflows required by the appointment scheduler:
1. Standard Salesforce Task Generation (after insert)
In line with our architectural principle of maximizing native Salesforce platform features, scheduling an appointment automatically creates a standard Task for the assigned branch technician:
Task staffTask = new Task(
WhatId = apt.Id, // Linked directly to Appointment__c
WhoId = apt.Staff__c, // Assigned to Staff Contact
Subject = 'Service Appointment: ' + serviceName,
ActivityDate = appointmentDate,
Status = 'Not Started',
Priority = 'Normal',
Description = 'Upcoming appointment with ' + customerName + ' at ' + locationName
);
tasksToInsert.add(staffTask);
Because this is a standard Salesforce Task:
- It appears immediately on the technician’s Lightning Activity Timeline.
- It respects standard CRM notifications and reminders.
- It links the appointment transaction to the CRM activity history with zero custom UI required.
2. Automated Confirmation Emails
Upon insertion of an appointment with Status__c = 'Scheduled', the handler dispatches dual notifications:
- Customer Confirmation: Details the service name, scheduled time, branch address, and assigned specialist.
- Staff Alert: Informs the technician of the new booking and references their newly created follow-up task.
All email generation includes governor limit checks (Limits.getEmailInvocations() < Limits.getLimitEmailInvocations()) to ensure bulk safe execution.
3. Lifecycle Automation: Cancellation (after update)
When an operator cancels an appointment (Status__c changes to 'Cancelled'):
- Open follow-up
Taskrecords linked viaWhatIdare automatically marked asStatus = 'Completed'and prefixed with[CANCELLED]in their subject line. - Cancellation notices are dispatched to the customer.
4. Integrity Protection: Delete Guard (before delete)
To prevent historical data loss, attempting to delete an appointment with Status__c = 'Completed' is immediately blocked:
private static void onBeforeDelete(Map<Id, Appointment__c> oldMap) {
for (Appointment__c apt : oldMap.values()) {
if (apt.Status__c == 'Completed') {
apt.addError('Completed appointments cannot be deleted.');
}
}
}
Unit Testing the Automation
To ensure production-grade reliability, we implemented AppointmentTriggerTest.cls utilizing our Part 3 test factories:
testTaskCreatedOnAppointmentInsert: Verifies that inserting an appointment creates an associatedTaskwith correctWhatId,WhoId, andNot Startedstatus.testCancellationClosesOpenTask: Verifies that cancelling an appointment automatically closes pending tasks and updates subject lines.testDeleteCompletedAppointmentPrevented: Verifies that completed bookings cannot be deleted from the database.testDefaultStatusEnforced: Verifies that blank status fields automatically default to'Scheduled'.
DX Verdict & Tool Feedback
What Shined
- Separation of Concerns by Default:
hob create triggerprevents junior and senior developers alike from writing dirty triggers by providing the industry-standard pattern out of the box. - Deterministic File Placement: Triggers are routed to
force-app/main/default/triggers/and handlers toforce-app/main/default/classes/automatically.
Opportunities for Hob’s Backlog
- Companion Test Generation (
--with-test) for Triggers:hob create apexsupports--with-test. Bringing--with-testtohob create trigger(e.g. scaffoldingAppointmentTriggerTest.clswith@TestSetupand DML insertion tests) would complete the TDD loop for triggers.
The Appointment Scheduler Series
- Series Index: Building the Salesforce Appointment Scheduler: Architecture & Series Index
- ← Part 1: Eliminating Salesforce XML Fatigue — CLI Schema Scaffolding with Hob
- ← Part 2: Instant Least-Privilege Access — Generating Permission Sets with Hob
- ← Part 3: Standardizing Test Hygiene — In-Memory vs DML Factories with Hob
- ← Part 4: High-Velocity TDD — Companion Test Scaffolding with Hob
- Part 5: Architecture by Default — Separation of Concerns with Hob Triggers (Current Article)
- Part 6: Building Modern Lightning UIs Fast — Scaffolded LWCs with Hob →
- Part 7: One Command to Rule Them All — The Complete Dev Org Setup with Hob