Module 3 — Development
Move into logic, custom code, and front-end development for Salesforce apps.
What you will learn
- ✓Understand when to use declarative configuration and when custom code is necessary.
- ✓Write maintainable Apex classes using types, methods, collections, and error handling.
- ✓Query and change Salesforce data safely with bulkified SOQL and DML.
- ✓Design trigger handlers and asynchronous jobs that respect platform limits.
- ✓Build Lightning Web Components that communicate with Apex and respond to user actions.
- ✓Write meaningful tests that protect behavior and make deployments safer.
Study material
1. What Salesforce development means
Salesforce development is the creation of custom business behavior, user interfaces, and integrations on the Salesforce platform. A developer works with metadata, Apex, SOQL, Lightning Web Components, APIs, and automated tests. The goal is not simply to write code; it is to solve a business problem while respecting Salesforce's multi-tenant architecture, security model, and governor limits.
Practical example
A company needs an approval request to be created whenever a high-value Opportunity is submitted. An admin may handle the basic path with Flow, while a developer may add Apex when the logic involves complex calculations or an external service.
2. Declarative tools versus programmatic code
Salesforce provides declarative tools such as fields, validation rules, Flow, and approval processes. These should be considered first because they are easier to maintain. Apex and LWC are appropriate when the requirement needs complex logic, transaction control, reusable services, custom UI behavior, or an integration. Good developers understand both options and avoid code that configuration can solve reliably.
Practical example
A simple field update belongs in a before-save Flow. A transaction that calls several reusable services, handles complex pricing rules, and must be tested as code may belong in Apex.
3. Apex fundamentals
Apex is Salesforce's strongly typed, object-oriented programming language. It looks similar to Java and is used for server-side business logic. Apex supports classes, interfaces, methods, variables, collections, exceptions, access modifiers, and annotations. Code runs in the context of a Salesforce transaction and must follow platform limits.
Practical example
Apex can calculate a renewal amount, enforce a complex business rule, or expose a method that an LWC calls to retrieve records.
Code example
public with sharing class GreetingService {
public static String greet(String name) {
if (String.isBlank(name)) {
return 'Hello, Salesforce developer';
}
return 'Hello, ' + name.trim();
}
}4. Classes, methods, and access modifiers
A class groups related data and behavior. A method performs an operation and can accept parameters or return a value. Use private members for implementation details and public or global members only when another part of the application must call them. `with sharing` helps enforce record-level sharing for code that reads or changes business data.
Practical example
A public service class can expose `createRenewalTask`, while helper methods that format a message remain private so callers depend only on the stable service API.
Code example
public with sharing class AccountService {
public static List<Account> findByIndustry(String industry) {
return [
SELECT Id, Name, Industry
FROM Account
WHERE Industry = :industry
ORDER BY Name
];
}
}5. Variables, data types, and null handling
Apex supports primitive types such as String, Integer, Decimal, Boolean, Date, and Datetime, along with sObjects and collections. Choose a type that represents the business value accurately. Always consider null or blank values before calling methods or performing calculations, especially when data comes from users or integrations.
Practical example
A discount field may be null for a new Opportunity. Code should treat that case deliberately instead of assuming a number is always present.
Code example
Decimal discount = opportunity.Discount__c == null
? 0
: opportunity.Discount__c;
Boolean isLargeDeal = opportunity.Amount != null
&& opportunity.Amount >= 100000;6. Collections: List, Set, and Map
Lists keep an ordered collection and can contain duplicates. Sets store unique values. Maps associate a key with a value and are essential for bulk processing. Use collections to process many records together and to avoid querying or updating one record at a time.
Practical example
When processing Opportunities, collect all Account IDs in a Set, query the Accounts once, and store them in a Map for fast lookup.
Code example
Set<Id> accountIds = new Set<Id>();
for (Opportunity opportunity : opportunities) {
if (opportunity.AccountId != null) {
accountIds.add(opportunity.AccountId);
}
}
Map<Id, Account> accountsById = new Map<Id, Account>([
SELECT Id, Name FROM Account WHERE Id IN :accountIds
]);7. SOQL: querying Salesforce data
SOQL retrieves records from Salesforce objects. A query selects fields, filters records with WHERE, orders results, and can traverse relationships. Use bind variables instead of concatenating user input. Query only the fields and records you need, and remember that a query returns no rows without throwing an exception.
Practical example
A service class can retrieve open Opportunities closing this month and show them in an LWC dashboard.
Code example
Date today = Date.today();
Date monthEnd = today.toStartOfMonth().addMonths(1).addDays(-1);
List<Opportunity> openDeals = [
SELECT Id, Name, Amount, CloseDate, StageName
FROM Opportunity
WHERE IsClosed = false
AND CloseDate >= :today
AND CloseDate <= :monthEnd
ORDER BY CloseDate
];8. SOSL: searching across objects
SOSL searches text across multiple objects and fields. It is useful when a user provides one search term and expects matching Leads, Contacts, Accounts, or other searchable records. SOQL is usually better when you know the object and precise filters; SOSL is better for broad text search.
Practical example
A support console can search a customer name or email and return matching Accounts and Contacts in one search operation.
Code example
List<List<SObject>> matches = [
FIND :searchTerm IN ALL FIELDS
RETURNING
Account(Id, Name LIMIT 10),
Contact(Id, Name, Email LIMIT 10)
];9. DML and database operations
DML statements insert, update, upsert, delete, undelete, and merge records. DML is transactional: an unhandled failure can roll back the transaction. The Database class provides methods such as `Database.insert(records, false)` for partial success, where each result must be inspected and logged or returned to the caller.
Practical example
A data import service may allow valid Contacts to save while returning row-level errors for records with invalid email values.
Code example
Database.SaveResult[] results = Database.insert(contacts, false);
for (Integer index = 0; index < results.size(); index++) {
if (!results[index].isSuccess()) {
for (Database.Error error : results[index].getErrors()) {
System.debug('Contact ' + index + ': ' + error.getMessage());
}
}
}10. Bulkification and governor limits
Salesforce serves many customers on shared infrastructure, so each transaction has governor limits for queries, DML statements, CPU time, heap, and more. Bulkified code works for one record and for a batch of records. Never place SOQL or DML inside a loop when the operation can be collected and performed once outside it.
Practical example
A trigger may receive 200 Opportunities in one transaction. A bulkified handler queries related Accounts once and updates all required records in one DML operation.
Code example
List<Task> tasks = new List<Task>();
for (Opportunity opportunity : Trigger.new) {
if (opportunity.StageName == 'Closed Won') {
tasks.add(new Task(
WhatId = opportunity.Id,
Subject = 'Schedule customer handoff'
));
}
}
if (!tasks.isEmpty()) {
insert tasks;
}11. Triggers and trigger context
A trigger runs automatically before or after records are inserted, updated, deleted, or undeleted. Context variables such as `Trigger.new`, `Trigger.old`, `Trigger.isInsert`, and `Trigger.isUpdate` describe the transaction. Before triggers are useful for changing the incoming record without DML; after triggers are useful when record IDs or related record actions are required.
Practical example
When an Account is updated, a trigger can identify whether its customer tier changed and create a follow-up task for the account team.
Code example
trigger AccountTrigger on Account (before update) {
for (Account currentAccount : Trigger.new) {
Account previousAccount = Trigger.oldMap.get(currentAccount.Id);
if (currentAccount.Customer_Tier__c != previousAccount.Customer_Tier__c) {
currentAccount.Tier_Changed_Date__c = Date.today();
}
}
}12. Trigger handlers and one-trigger pattern
Keep a trigger thin and move business logic into a handler or service class. A single trigger per object gives the team predictable control over execution order and makes logic easier to test. The handler should be bulkified, focused, and aware of recursion risks when it updates records that can invoke automation again.
Practical example
Instead of placing 150 lines in an Opportunity trigger, the trigger delegates to `OpportunityTriggerHandler`, which coordinates validation, related records, and notifications.
Code example
trigger OpportunityTrigger on Opportunity (after update) {
OpportunityTriggerHandler.handleAfterUpdate(
Trigger.new,
Trigger.oldMap
);
}13. Asynchronous Apex
Asynchronous Apex moves work outside the immediate user transaction or handles larger workloads. Future methods are simple but have restrictions. Queueable Apex supports chaining and complex state. Batch Apex processes very large data sets in chunks. Scheduled Apex runs code at a defined time. Select the simplest async tool that meets the requirement.
Practical example
After an Opportunity closes, a Queueable job can send a large set of related records to a downstream system without making the sales user wait for the callout.
Code example
public class RenewalQueueable implements Queueable {
private Set<Id> accountIds;
public RenewalQueueable(Set<Id> accountIds) {
this.accountIds = accountIds;
}
public void execute(QueueableContext context) {
// Process renewal work for accountIds.
}
}
System.enqueueJob(new RenewalQueueable(accountIds));14. Apex error handling and custom exceptions
Use try-catch when a failure can be handled meaningfully, such as a recoverable callout or user-facing validation. Custom exceptions communicate a business-specific problem. Do not silently swallow errors. Log useful diagnostic context and return a clear message to the caller when appropriate.
Practical example
A pricing service catches an unavailable configuration error and tells the user to contact the pricing team instead of displaying a generic null-pointer failure.
Code example
public class PricingException extends Exception {}
try {
if (pricebookId == null) {
throw new PricingException('A price book is required.');
}
} catch (PricingException error) {
throw new AuraHandledException(error.getMessage());
}15. Apex security and sharing
Apex runs in system context by default, so developers must deliberately protect data. Use `with sharing` when record sharing should be enforced, and enforce object and field permissions where the use case requires it. Use safe query and database patterns, avoid exposing sensitive fields unnecessarily, and validate every value received from a client.
Practical example
An LWC that lists Cases should return only records the current user is allowed to access and should not expose internal escalation notes to a customer-facing user.
16. Lightning Web Components fundamentals
LWC is Salesforce's modern web component model. A component commonly contains an HTML template, a JavaScript controller, and a configuration XML file. The JavaScript defines reactive state and behavior; the template renders that state; the metadata file exposes the component to supported pages. Components should be small, reusable, and focused on one UI responsibility.
Practical example
A `renewalSummary` component can display a heading, a total amount, and a list of upcoming renewals on an Account record page.
Code example
<!-- renewalSummary.html -->
<template>
<lightning-card title="Renewals">
<p class="slds-p-horizontal_small">
Upcoming renewals: {renewalCount}
</p>
</lightning-card>
</template>17. LWC properties, events, and reactive state
LWC uses properties to pass data into child components and custom events to send information from a child to its parent. A property used by the template is reactive, so assigning a new value causes the relevant UI to update. Keep state local when possible and communicate through clear, documented inputs and events.
Practical example
A child product selector dispatches a `productselected` event. The parent listens for it and updates the Opportunity line item summary.
Code example
// productSelector.js
handleSelect(event) {
this.dispatchEvent(
new CustomEvent('productselected', {
detail: { productId: event.target.value }
})
);
}18. Calling Apex from LWC
An LWC can call an Apex method imperatively when it needs precise control, or use the `@wire` service for reactive, cacheable reads. Apex methods exposed to LWC must be static and annotated with `@AuraEnabled`. Use cacheable methods only for read-only operations, and handle loading, success, empty, and error states in the UI.
Practical example
An Account page wires a read-only Apex method to show open Cases. A button imperatively calls another method to close a selected Case and then refreshes the list.
Code example
// accountCases.js
import getOpenCases from '@salesforce/apex/AccountCaseService.getOpenCases';
import { wire } from 'lwc';
@wire(getOpenCases, { accountId: '$recordId' })
cases;
get hasError() {
return Boolean(this.cases.error);
}19. LWC lifecycle and user experience
LWC lifecycle hooks such as `connectedCallback`, `renderedCallback`, and `disconnectedCallback` let a component respond to creation, rendering, and removal. Avoid unnecessary work in `renderedCallback` because it can run repeatedly. Always provide loading indicators, empty states, accessible labels, useful error messages, and responsive layouts.
Practical example
A component uses `connectedCallback` to initialize local state, displays a spinner while records load, and shows 'No open cases' when the query returns an empty list.
20. Apex testing and test data
Apex tests verify behavior in an isolated test context. Create your own test data instead of depending on org records. `Test.startTest()` and `Test.stopTest()` reset relevant limits and execute asynchronous work during tests. Test success paths, validation failures, bulk input, permissions-sensitive behavior, and meaningful edge cases rather than testing only code coverage.
Practical example
A test for an Opportunity handler verifies one Opportunity and a list of 200 Opportunities, proving the logic works for normal and bulk transactions.
Code example
@IsTest
private class AccountServiceTest {
@IsTest
static void findsAccountsByIndustry() {
Account account = new Account(
Name = 'Example Manufacturing',
Industry = 'Manufacturing'
);
insert account;
Test.startTest();
List<Account> results = AccountService.findByIndustry('Manufacturing');
Test.stopTest();
System.assertEquals(1, results.size());
System.assertEquals(account.Id, results[0].Id);
}
}21. Testing triggers, async jobs, and callouts
Tests should exercise the public behavior of a trigger handler or service, not private implementation details. Use `Test.startTest()` and `Test.stopTest()` for Queueable, Batch, Scheduled, or Future work. Use `HttpCalloutMock` to test callout responses without calling a real external system, and test both successful and failed responses.
Practical example
An integration test supplies a mock 200 response and a mock 500 response, proving the service handles both a successful sync and a recoverable external failure.
22. Deployment, source control, and metadata
Salesforce development is metadata-driven. Source control should contain Apex classes, triggers, LWC files, object metadata, permissions, and tests. Developers work in a scratch org or sandbox, review changes, run tests, and deploy through Salesforce CLI or an approved CI/CD process. Never treat production as the first place to test a change.
Practical example
A developer creates a feature branch, builds an Apex service and LWC, runs targeted tests, opens a pull request, and deploys the reviewed metadata to a sandbox before production.
Code example
sf project deploy start \
--source-dir force-app/main/default \
--target-org my-sandbox \
--test-level RunLocalTests23. Development checklist
Before considering a feature complete, confirm that the requirement is understood, the simplest suitable tool was chosen, code is bulkified and secure, errors are handled, the UI supports loading and empty states, tests cover behavior, and deployment metadata is complete. Document decisions that the next developer or administrator will need to maintain the feature.
Practical example
For a new Case escalation feature, the team checks permissions, bulk updates, duplicate notifications, Flow and trigger interaction, test coverage, deployment dependencies, and rollback steps before release.