Total Pageviews

AI, Apex, Integration, Interview,

Advanced Apex Programming: Build Reusable, Production-Ready Code

Advanced Salesforce Development Series | Article 1

Introduction

Have you ever worked on a Salesforce project where the same business logic was written in three or four different Apex classes?

Imagine that your project has a customer registration process, an integration that creates customers from an external application, and a scheduled job that updates customer records. All three processes need to validate customer information before saving it.

One developer writes the validation logic in a trigger. Another writes similar logic in an API class. A third copies that logic into a batch class.

Initially, everything works. But after a few months, the business team changes the validation rules. Now, the development team must find every copy of the logic and update it. If one implementation is missed, the application starts producing inconsistent results.

This is where reusable Apex programming becomes important.

Writing Apex that works today is one thing. Designing Apex that remains maintainable, testable, secure, and reliable as the application grows is a different challenge.

In this article, we will build a reusable customer-processing service and explore the Apex concepts and design decisions that make it suitable for a production application.

1. What Does Production-Ready Apex Mean?

Production-ready Apex is not simply code that compiles successfully or passes a basic unit test. It is code designed to handle real business requirements, unexpected inputs, large record volumes, security restrictions, and future changes.

A well-designed Apex implementation should generally have these characteristics:

  • Reusable: Business rules can be shared across triggers, APIs, batch jobs, and other entry points.
  • Bulkified: It processes multiple records without exceeding Salesforce governor limits.
  • Maintainable: Each class has a clear responsibility.
  • Testable: Business rules can be tested without unnecessarily depending on database operations.
  • Secure: Appropriate object permissions, field permissions, and record-sharing rules are enforced.
  • Reliable: Failures are handled deliberately, and database changes follow a defined transaction strategy.
  • Extensible: New business requirements can be introduced without rewriting the entire application.
Important: Reusable code does not mean creating a separate class for every small operation. It means identifying responsibilities and creating abstractions where they make the application easier to understand, test, and maintain.

2. Understand the Business Requirement

Before writing code, let us define our example project.

A company wants to process customer records in Salesforce. A customer is represented by an Account record.

The business team provides these requirements:

  1. The Account name is mandatory.
  2. The customer email must be valid when provided.
  3. The customer phone number must be provided.
  4. Accounts must be validated before processing.
  5. The processing logic must be reusable from different Apex entry points.
  6. Invalid records must not be processed.
  7. The implementation must support multiple records in one transaction.
  8. The solution must be covered by Apex unit tests.

We will implement the example using a service class, a validation utility, a custom exception, and a unit test class.

The example uses standard Account fields, so you can experiment with it in a Salesforce Developer Edition or another suitable sandbox without creating a custom object.

3. Organize Apex Classes by Responsibility

A common mistake is to put every operation into one large Apex class. That class may validate records, execute SOQL queries, update records, send emails, and process integrations.

As requirements increase, the class becomes difficult to maintain and test.

For our example, we will use the following structure:

CustomerProcessing/
|
|-- CustomerValidationException.cls
|
|-- CustomerValidator.cls
|
|-- CustomerService.cls
|
|-- CustomerServiceTest.cls
Class Responsibility
CustomerValidationException Represents a business validation failure.
CustomerValidator Validates customer data.
CustomerService Coordinates validation and database operations.
CustomerServiceTest Verifies expected behavior and failure scenarios.

This is a small service-oriented design. Larger applications may also benefit from selector classes for queries, domain classes for object-specific behavior, and a dependency injection strategy.

4. Create a Custom Exception

Apex provides built-in exceptions for different types of failures. However, business validation failures often deserve a meaningful exception type.

For example, an invalid customer email is different from a database failure. Using a custom exception makes that distinction clearer.

Create a class named CustomerValidationException.

public class CustomerValidationException
    extends Exception {
}

This class gives our application a specific exception type that callers can catch when customer validation fails.

Custom exceptions are particularly useful when the application needs to distinguish expected business failures from unexpected technical failures.

5. Build a Reusable Customer Validator

The next class will contain our customer validation rules.

Notice that this class does not insert or update records. Its only responsibility is to determine whether the supplied Account data meets the business requirements.

Create CustomerValidator.cls.

public class CustomerValidator {

    public static void validate(Account customer) {

        if (customer == null) {
            throw new CustomerValidationException(
                'Customer record cannot be null.'
            );
        }

        if (String.isBlank(customer.Name)) {
            throw new CustomerValidationException(
                'Customer name is required.'
            );
        }

        if (String.isBlank(customer.Phone)) {
            throw new CustomerValidationException(
                'Customer phone number is required.'
            );
        }

        if (!String.isBlank(customer.Email__c)
            && !Pattern.matches(
                '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$',
                customer.Email__c
            )) {

            throw new CustomerValidationException(
                'Enter a valid customer email address.'
            );
        }
    }
}
Configuration required: The example references Account.Email__c. Create a custom Email field named Email__c on Account before compiling the complete example. Alternatively, replace it with an appropriate existing email field in your org and update the tests accordingly.

Why separate validation from database operations?

Suppose the email validation rule changes next month. If the validation logic is centralized in this class, we can update the rule in one place.

The validator can also be called from different Apex entry points without copying its implementation.

There is another benefit: because validation does not perform DML, it can be tested independently without inserting records into the database.

Production consideration: This example uses a lightweight email-format check to demonstrate reusable validation. It does not prove that an email address exists or that its owner can receive messages. Real business validation may require additional rules.

6. Build the Customer Service Layer

Now we need a class that coordinates validation and database operations.

The service layer receives Account records, validates them, and inserts the valid records.

We will also provide a separate method that allows an existing Account to be updated after validation.

Create CustomerService.cls.

public with sharing class CustomerService {

    public static List<Id> createCustomers(
        List<Account> customers
    ) {

        if (customers == null || customers.isEmpty()) {
            return new List<Id>();
        }

        // Validate all records before performing DML.
        for (Account customer : customers) {
            CustomerValidator.validate(customer);
        }

        // Insert records in one DML operation.
        insert customers;

        List<Id> customerIds = new List<Id>();

        for (Account customer : customers) {
            customerIds.add(customer.Id);
        }

        return customerIds;
    }

    public static void updateCustomer(Account customer) {

        if (customer == null || customer.Id == null) {
            throw new CustomerValidationException(
                'An existing customer record is required.'
            );
        }

        CustomerValidator.validate(customer);

        update customer;
    }
}

What makes this implementation reusable?

The service does not care whether the request originated from a Lightning Web Component, an integration, a scheduled job, or another Apex class.

Any authorized Apex caller can use the same service instead of implementing its own copy of the customer-processing logic.

Why validate before inserting?

The service validates every Account before executing the insert statement. If validation throws an exception, the insert statement is never reached.

For this synchronous operation, that gives us a clear all-or-nothing validation approach: either all submitted records pass the validation rules, or none are inserted by this service method.

The database operation itself is still subject to Salesforce transaction behavior. If this method is called inside a larger transaction, the caller must consider how exceptions are caught and how earlier database operations are handled.

Why use one DML statement?

Salesforce imposes governor limits on database operations. Performing DML inside a loop is a common way to waste those limits.

Our service collects the records and executes one insert statement for the entire list.

// Avoid this pattern.
for (Account customer : customers) {
    insert customer;
}

// Prefer bulk DML.
insert customers;

Bulkification also means thinking about the entire execution path. A service that performs one DML operation can still be inefficient if its validator executes a SOQL query for every record.

7. Understand Interfaces and Abstract Classes

The service we have built works for our current requirements. But what happens when another application needs a different customer-processing implementation?

This is where interfaces and abstract classes can help.

7.1 What is an interface?

An interface defines a contract. It tells implementing classes which methods they must provide, without specifying the implementation of those methods.

For example, imagine that one implementation processes individual customers while another sends customer information to an external system.

Create ICustomerProcessor.cls.

public interface ICustomerProcessor {

    List<Id> processCustomers(
        List<Account> customers
    );
}

Now create a class that implements the interface.

public with sharing class LocalCustomerProcessor
    implements ICustomerProcessor {

    public List<Id> processCustomers(
        List<Account> customers
    ) {

        return CustomerService.createCustomers(
            customers
        );
    }
}

The class must implement the method declared by the interface.

A caller that depends on ICustomerProcessor can work with any implementation that satisfies the contract, provided that the implementation is supplied correctly.

This makes interfaces valuable for integrations, alternative processing strategies, and test doubles.

7.2 What is an abstract class?

An abstract class is a base class that cannot be instantiated directly. It can contain shared implementation as well as abstract methods that subclasses must implement.

Consider a simple abstract processor.

public abstract class AbstractCustomerProcessor {

    public void logStart() {
        System.debug(
            'Customer processing started.'
        );
    }

    public abstract void processOne(
        Account customer
    );
}

A concrete subclass can inherit the shared logging method and implement the required processing method.

public class BasicCustomerProcessor
    extends AbstractCustomerProcessor {

    public override void processOne(
        Account customer
    ) {

        CustomerValidator.validate(customer);
    }
}

In this example, the subclass inherits logStart() and supplies its own implementation of processOne().

7.3 Interface versus abstract class

Feature Interface Abstract class
Main purpose Define a contract Provide a shared base implementation
Implementation Implementing classes supply the required methods Can contain implemented and abstract methods
Instantiation Cannot be instantiated directly Cannot be instantiated directly
Typical use Alternative implementations and testability Common behavior shared by related classes
Practical advice: Do not create interfaces and abstract classes simply to make a project appear advanced. Introduce them when you have a real need for multiple implementations, a stable contract, or shared behavior.

8. Understand Inheritance and Method Overriding

Inheritance allows one class to extend another class and reuse its accessible behavior.

In the previous example, BasicCustomerProcessor extends AbstractCustomerProcessor.

Method overriding allows a subclass to provide its own implementation of an inherited virtual or abstract method.

In Apex, a method generally needs to be declared virtual or abstract in the base class before a subclass can override it.

For example:

public virtual class CustomerProcessorBase {

    public virtual String getProcessorName() {
        return 'Default Customer Processor';
    }
}

A subclass can override the method:

public class PremiumCustomerProcessor
    extends CustomerProcessorBase {

    public override String getProcessorName() {
        return 'Premium Customer Processor';
    }
}

Inheritance is useful when classes share a genuine relationship and common behavior. If two classes only happen to contain similar code, composition or a shared utility may be a better design.

9. Use Collections Effectively

Apex collections are essential for processing Salesforce records efficiently.

The three primary collection types are:

  • List: An ordered collection that can contain duplicate values.
  • Set: A collection of unique values.
  • Map: A collection of key-value pairs.

9.1 Use a List to process multiple Accounts

List<Account> customers = new List<Account>();

customers.add(new Account(
    Name = 'ABC Technologies',
    Phone = '9876543210',
    Email__c = 'contact@abc.example'
));

customers.add(new Account(
    Name = 'XYZ Solutions',
    Phone = '9876501234',
    Email__c = 'info@xyz.example'
));

List<Id> customerIds =
    CustomerService.createCustomers(customers);

The example creates two Account records and passes them to the same service method. The method does not need a separate implementation for each record.

9.2 Use a Set to eliminate duplicate identifiers

Set<Id> accountIds = new Set<Id>();

accountIds.add('001000000000001AAA');
accountIds.add('001000000000001AAA');

System.debug(accountIds.size()); // 1

The example illustrates how a Set prevents duplicate values. The sample IDs are illustrative; use real record IDs when querying Salesforce data.

Sets are particularly useful when collecting IDs before a SOQL query.

9.3 Use a Map for efficient record lookup

Map<Id, Account> accountById =
    new Map<Id, Account>([
        SELECT Id, Name, Phone
        FROM Account
        WHERE Phone != null
        LIMIT 200
    ]);

for (Id accountId : accountById.keySet()) {

    Account customer = accountById.get(accountId);

    System.debug(customer.Name);
}

A Map allows the application to retrieve an Account by its ID without repeatedly searching through a List.

When processing related records, maps often help avoid nested loops and improve readability.

10. Separate Business Logic from Database Operations

The service layer is a good start, but a larger application may need a separate database-access layer.

Consider an application that must find customer Accounts by ID. We can put that query into a dedicated selector class.

public with sharing class AccountSelector {

    public static List<Account> selectByIds(
        Set<Id> accountIds
    ) {

        if (accountIds == null || accountIds.isEmpty()) {
            return new List<Account>();
        }

        return [
            SELECT Id, Name, Phone
            FROM Account
            WHERE Id IN :accountIds
            WITH USER_MODE
        ];
    }
}

The selector owns the query. The service can focus on coordinating business operations.

Notice the use of WITH USER_MODE. It instructs Salesforce to enforce the running user's applicable data access permissions for the query.

The selector does not automatically solve every security concern in the application. The service must also use appropriate access controls for any database writes and other operations.

Recommended responsibility separation

Trigger / LWC / REST API / Batch
                 |
                 v
          CustomerService
                 |
        +--------+--------+
        |                 |
        v                 v
 CustomerValidator   AccountSelector
        |                 |
        v                 v
 Business Rules       SOQL Queries
                 |
                 v
          Database DML

This is one possible architecture, not a mandatory structure for every Salesforce project. For a small application, a separate selector class may add unnecessary complexity.

11. Handle Exceptions Properly

Exception handling is another area where production Apex requires careful thought.

A common mistake is to catch every exception and return a success-looking response.

For example, avoid this pattern:

try {
    insert customers;
} catch (Exception e) {
    System.debug(e.getMessage());
}

If the insert fails, the calling code may continue as though the operation succeeded. Logging the error is not the same as handling it.

Catch only what you can handle

A caller may want to distinguish validation failures from database failures.

try {

    List<Id> customerIds =
        CustomerService.createCustomers(customers);

    System.debug(customerIds);

} catch (CustomerValidationException e) {

    System.debug(
        'Validation failed: ' + e.getMessage()
    );

} catch (DmlException e) {

    System.debug(
        'Database operation failed: ' + e.getMessage()
    );
}

This code demonstrates exception handling. In a real application, replace debug statements with an appropriate response, logging mechanism, or user-facing error handling.

A caught exception does not automatically roll back earlier successful DML operations in the transaction. If all changes must be rolled back after a failure, design the transaction strategy explicitly, potentially using a savepoint and rollback or allowing the exception to propagate.

Use partial success only when the business requirement supports it

Sometimes a business process should accept valid records and report errors for invalid records. In other cases, the entire operation must succeed or fail together.

These are different transaction requirements.

Requirement Possible approach
All submitted records must be valid Validate all records before performing DML.
Valid records should succeed independently Use an appropriate partial-success DML strategy and inspect each result.
Failure must undo earlier changes Use a deliberate rollback strategy or allow the exception to propagate.
Failures must be reported to an external application Return a structured response through the relevant API or integration layer.

12. Write a Proper Apex Test Class

Apex testing should verify business behavior, including successful operations and failure scenarios.

The following test class covers:

  • Successful creation of a customer.
  • Bulk processing of multiple customers.
  • Rejection of an Account without a name.
  • Rejection of an Account without a phone number.
  • Rejection of an invalid email address.
  • Handling of an empty input list.

Create CustomerServiceTest.cls.

@IsTest
private class CustomerServiceTest {

    @IsTest
    static void shouldCreateCustomer() {

        Account customer = new Account(
            Name = 'ABC Technologies',
            Phone = '9876543210',
            Email__c = 'contact@abc.example'
        );

        Test.startTest();

        List<Id> customerIds =
            CustomerService.createCustomers(
                new List<Account>{ customer }
            );

        Test.stopTest();

        System.assertEquals(
            1,
            customerIds.size(),
            'One customer ID should be returned.'
        );

        Account savedCustomer = [
            SELECT Id, Name, Phone, Email__c
            FROM Account
            WHERE Id = :customerIds[0]
        ];

        System.assertEquals(
            'ABC Technologies',
            savedCustomer.Name
        );

        System.assertEquals(
            '9876543210',
            savedCustomer.Phone
        );
    }

    @IsTest
    static void shouldProcessMultipleCustomers() {

        List<Account> customers = new List<Account>{
            new Account(
                Name = 'Customer One',
                Phone = '9876543210',
                Email__c = 'one@example.com'
            ),
            new Account(
                Name = 'Customer Two',
                Phone = '9876543211',
                Email__c = 'two@example.com'
            ),
            new Account(
                Name = 'Customer Three',
                Phone = '9876543212',
                Email__c = 'three@example.com'
            )
        };

        Test.startTest();

        List<Id> customerIds =
            CustomerService.createCustomers(customers);

        Test.stopTest();

        System.assertEquals(3, customerIds.size());

        System.assertEquals(
            3,
            [SELECT COUNT()
             FROM Account
             WHERE Id IN :customerIds]
        );
    }

    @IsTest
    static void shouldRejectMissingName() {

        Account customer = new Account(
            Phone = '9876543210',
            Email__c = 'contact@example.com'
        );

        Test.startTest();

        try {

            CustomerService.createCustomers(
                new List<Account>{ customer }
            );

            System.assert(
                false,
                'Expected a validation exception.'
            );

        } catch (CustomerValidationException e) {

            System.assertEquals(
                'Customer name is required.',
                e.getMessage()
            );
        }

        Test.stopTest();
    }

    @IsTest
    static void shouldRejectMissingPhone() {

        Account customer = new Account(
            Name = 'ABC Technologies',
            Email__c = 'contact@example.com'
        );

        try {

            CustomerService.createCustomers(
                new List<Account>{ customer }
            );

            System.assert(
                false,
                'Expected a validation exception.'
            );

        } catch (CustomerValidationException e) {

            System.assertEquals(
                'Customer phone number is required.',
                e.getMessage()
            );
        }
    }

    @IsTest
    static void shouldRejectInvalidEmail() {

        Account customer = new Account(
            Name = 'ABC Technologies',
            Phone = '9876543210',
            Email__c = 'invalid-email'
        );

        try {

            CustomerService.createCustomers(
                new List<Account>{ customer }
            );

            System.assert(
                false,
                'Expected a validation exception.'
            );

        } catch (CustomerValidationException e) {

            System.assertEquals(
                'Enter a valid customer email address.',
                e.getMessage()
            );
        }
    }

    @IsTest
    static void shouldHandleEmptyInput() {

        Test.startTest();

        List<Id> customerIds =
            CustomerService.createCustomers(
                new List<Account>()
            );

        Test.stopTest();

        System.assertEquals(0, customerIds.size());
    }
}
Testing tip: Run the test class in your Salesforce org after creating the required custom email field. Additional tests should cover update behavior, null input, selector security, and any Account validation rules or triggers that exist in your org.

13. Common Apex Mistakes to Avoid

Mistake 1: Writing SOQL inside a loop

A query inside a loop can consume governor limits quickly. Collect record IDs first, then query the required records in one operation wherever possible.

Mistake 2: Writing DML inside a loop

Build a List of records and perform bulk DML instead of inserting or updating each record separately.

Mistake 3: Putting every responsibility into one class

A single large class makes changes risky. Separate validation, business orchestration, and data access when that separation improves maintainability.

Mistake 4: Catching exceptions without handling them

Do not silently ignore failures. Return meaningful errors, log useful diagnostic information, or propagate exceptions when appropriate.

Mistake 5: Ignoring security

Apex normally runs with elevated system-level privileges unless the code and execution context establish different behavior. Use sharing declarations, enforce CRUD and field-level security where required, and select the appropriate user-mode or system-mode behavior for each operation.

Mistake 6: Assuming with sharing enforces field permissions

The with sharing keyword primarily affects record-level sharing. It does not, by itself, enforce object permissions or field-level security.

For database operations, consider user-mode SOQL and DML, or appropriate describe checks and security-enforcement techniques based on the application's requirements.

Mistake 7: Using a Boolean success flag without useful error information

A response such as success = false may not tell the caller what went wrong. A well-designed API response should distinguish validation errors, authorization failures, and unexpected processing failures without exposing sensitive internal details.

Mistake 8: Overengineering a simple requirement

Not every application needs a domain layer, repository abstraction, factory, interface, and dependency injection framework. Use the smallest architecture that meets the requirements and can evolve safely.

Mistake 9: Assuming passing tests proves production readiness

Tests should also examine bulk behavior, security, transaction boundaries, integration failures, and interactions with existing triggers and automation.

14. How to Improve This Design for a Large Salesforce Project

Our current implementation is suitable for learning the basics of reusable service design. A larger application would need additional decisions before deployment.

Consider these improvements:

  1. Define transaction behavior: Decide whether the service should support all-or-nothing processing or partial success.
  2. Strengthen input validation: Add country-specific phone rules, required fields, and business-specific duplicate checks where necessary.
  3. Enforce authorization: Ensure that the caller is permitted to create or update Accounts and that field access follows the intended security model.
  4. Add structured logging: Record correlation IDs, processing outcomes, and safe diagnostic details for integration failures.
  5. Introduce a selector layer: Move complex or shared queries into dedicated classes.
  6. Use asynchronous processing when justified: For large jobs or slow external operations, consider Batch Apex, Queueable Apex, or an appropriate event-driven design.
  7. Plan for retries: Integration operations should account for timeouts, duplicate requests, and idempotency.
  8. Automate quality checks: Use code reviews, Salesforce Code Analyzer, and CI/CD validation as part of the development workflow.

A particularly important point is that a reusable service should have a clearly documented contract. Callers should know whether the method throws exceptions, performs DML, accepts partial success, and requires system or user permissions.

15. Hands-On Assignment for Salesforce Developers

Now it is your turn to extend the customer-processing service.

Project: Advanced Customer Management Service
Difficulty: Intermediate to Advanced
Objective: Build a reusable service that validates, creates, updates, and retrieves customer records.

Assignment requirements

  1. Create a reusable CustomerValidator class.
  2. Create a CustomerService class for customer processing.
  3. Create a custom exception for validation errors.
  4. Create an interface named ICustomerProcessor.
  5. Implement the interface using a concrete customer processor.
  6. Add a method that retrieves customers by a Set of Account IDs.
  7. Ensure SOQL and DML operations are bulkified.
  8. Enforce the required record, object, and field access rules.
  9. Write tests for valid records, invalid records, and bulk operations.
  10. Document how the service behaves when one record fails validation.

Advanced challenge

Extend the service to process customer information received from an external REST API.

Your implementation should:

  • Accept a JSON request.
  • Deserialize the request into a wrapper class.
  • Validate every customer before database processing.
  • Prevent duplicate customers using an appropriate business key.
  • Return a structured response with record IDs and useful error messages.
  • Handle repeated requests safely where the business operation requires idempotency.
  • Include REST API tests and bulk-processing tests.

Do not put the entire integration implementation into one REST resource class. Keep request handling, business validation, database operations, and response construction appropriately separated.

16. Key Takeaways

Writing production-ready Apex requires more than knowing the syntax. You must understand how to organize business logic, handle database operations, enforce security, and test the behavior of the application.

Remember these principles:

  • Keep each class focused on a clear responsibility.
  • Centralize reusable business validation.
  • Use interfaces when multiple implementations or a stable contract are valuable.
  • Use inheritance when classes share genuine behavior.
  • Choose Lists, Sets, and Maps based on the operation you need to perform.
  • Bulkify SOQL and DML operations.
  • Handle exceptions deliberately.
  • Enforce security at the appropriate level.
  • Write meaningful tests for positive, negative, and bulk scenarios.
  • Choose an architecture that fits the complexity of the business requirement.

Conclusion

Apex developers often begin by writing code that solves the immediate requirement. As they gain experience, the focus shifts toward writing code that other developers can understand, test, reuse, and extend.

That is the difference between implementing a feature and designing a maintainable Salesforce solution.

In this tutorial, we built a customer-processing service, separated validation from database operations, explored interfaces and inheritance, used collections, handled exceptions, and wrote Apex tests.

Try implementing the assignment in your own Salesforce Developer Edition. Start with the basic service and then add security enforcement, selector classes, and REST API support.

Follow KhumedSFDC for more practical Salesforce development tutorials, Apex examples, Lightning Web Components, integrations, and advanced development concepts.


Frequently Asked Questions

What is reusable Apex code?

Reusable Apex code is written so that multiple parts of an application can use the same business logic without duplicating its implementation. Service classes, utility classes, and interfaces can help achieve this.

Why should business logic be separated from database operations?

Separating these responsibilities makes code easier to maintain and test. Business rules can be updated independently of queries and database writes.

When should I use an interface in Apex?

Use an interface when you need a defined contract that multiple classes can implement. This is useful for alternative processing strategies, integrations, and testable application designs.

What is the difference between with sharing and user mode?

The with sharing declaration controls record-level sharing behavior for a class. User-mode database operations enforce the running user's applicable data access permissions for those operations. They address related but different aspects of security.

How do I make Apex code bulkified?

Process collections of records, avoid SOQL and DML inside loops, and use Lists, Sets, and Maps to organize records efficiently. Review the entire execution path, including helper classes and triggers.

What should I learn after advanced Apex programming?

Continue with Apex security, asynchronous Apex, advanced testing, integration patterns, platform events, and Lightning Web Components. These topics build on the reusable service patterns introduced in this article.