Showing posts with label Apex. Show all posts
Showing posts with label Apex. Show all posts

Sunday, August 16, 2026

Zero-Touch Case Documents: Automating Dynamic PDF Generation and Delivery in Salesforce

 

The Business Problem

Every organization running Salesforce eventually hits the same operational bottleneck: a business process that depends on a human generating a document, filling it with record data, and emailing it out. It's slow, error-prone, and doesn't scale.

In this case, the requirement was precise: the moment a Case is created, a formatted PDF — built against a structure the business team had already defined — needed to be generated and delivered to the customer's inbox automatically. No manual document prep. No copying case details into a form by hand. No separate step to draft and send the email.

The complexity sat in two places. First, the PDF itself had to be dynamic — Case-specific data such as the case number, account, customer name, and address needed to populate directly into predefined blank fields on a business-supplied form. Second, the email carrying that PDF needed its own dynamic template, merging Case data with related Account and Customer details, following a structure the business had already signed off on.

The goal of this proof-of-concept was to solve both problems declaratively wherever possible, keep the architecture maintainable by non-developers, and make failure visible rather than silent. Here's how it came together — the diagram below gives the high-level shape before we walk through each stage.

Solution Overview

At a high level, the architecture has four moving parts, shown end to end in the diagram above:



  1. A Record-Triggered Flow on the Case object, fired on record creation, acting as the orchestrator.
  2. A Prompt Template that accepts the Case ID as input and returns the populated document structure, using the business-provided file as the base format with the blank fields resolved dynamically.
  3. An Apex class that converts this formatted content into an actual PDF, retrieves the additional Account and Customer details required, drafts the email through Salesforce's Messaging classes, attaches the PDF, and sends it to the customer.
  4. Error handling and logging, so failures — a missing or invalid customer email, for instance — are captured for review rather than failing silently.

Step-by-Step Breakdown

1. Trigger: record-triggered flow on Case creation

The entry point is intentionally simple: a Record-Triggered Flow configured to fire on Case creation. Its role is orchestration only — it does not construct the PDF or send the email directly, but coordinates the downstream calls to the Prompt Template and the Apex class. Keeping this layer thin makes the automation easier to hand off and modify without touching code.

2. Formatting the document through a Prompt Template

Rather than hardcoding the PDF layout inside Apex, the design uses a Prompt Template that takes the Case ID as its sole input parameter. The template is grounded in the business-supplied file, which defines the exact structure and blank fields the final document requires — case number, account, customer name, address, and similar fields.

The Prompt Template resolves these fields against live Case data and returns the fully populated document content, which the Flow captures into a variable. This keeps the formatting logic declarative and business-owned: when the form layout changes, the business team can adjust it without a developer touching Apex.

3. PDF conversion and email dispatch through Apex

The Flow hands the populated content to an Apex class, which performs the core execution:

  • Converts the formatted content into an actual PDF using Salesforce's native PDF generation capability (Blob.toPDF()).
  • Queries the Case for its related Account and Contact details — customer name, email address, and any additional fields needed for personalization.
  • Builds the email body from a separate email template, merging in both Case data and Account/Customer data.
  • Uses the Messaging class to construct and send the email, with the generated PDF attached.
  • Delivers the email to the customer's address as retrieved from the record.

Consolidating PDF generation, data retrieval, and email dispatch into one well-tested Apex class made the logic considerably easier to unit test and reason about, compared to distributing the same responsibilities across multiple Flow elements.

4. Error handling and logging

Automation of this kind fails quietly if you're not deliberate about it — a missing customer email, a null Account lookup, or a malformed PDF payload can break the process without anyone noticing. To guard against that, the design includes:

  • Validation checks before attempting to send, such as confirming a valid customer email exists.
  • A dedicated logging mechanism that captures failures with enough context — Case ID, failure reason, timestamp — for a support or error-logging user to investigate.
  • Isolated failure handling, so an issue on one Case does not affect processing for any other.

Lessons learned

A few takeaways stood out from building this proof-of-concept:

  • Separating "what to generate" from "how to send it" — using the Prompt Template purely for content and formatting, and Apex purely for delivery — made the solution significantly easier to maintain and hand off to business stakeholders.
  • Error logging is not optional. Any automation that touches a customer's inbox carries a higher cost for silent failure than a purely internal process, so visibility into failures has to be designed in from the start, not added afterward.
  • Declarative-first, code-second is the right default here. The Flow owns orchestration, the Prompt Template owns dynamic formatting against a business-owned structure, and Apex is reserved for the pieces that genuinely require code: PDF binary conversion and the Messaging API calls.

Closing thoughts

This proof-of-concept demonstrates how a common but operationally expensive requirement — generate a document, populate it with live data, and deliver it automatically — can be solved cleanly with a combination of Flow, Prompt Templates, and Apex, without hardcoding business logic that should stay flexible. The pattern generalizes well beyond Cases, to invoices, confirmations, onboarding packets, and any other scenario where a document needs to be assembled and delivered the moment a record is created.

If you're solving something similar, I'd welcome hearing how your approach differs — particularly around error handling strategy or how much of the template logic you keep declarative versus code-driven.

Wednesday, August 5, 2026

Apex Changes in Summer '26: The Biggest Security Shift in Year

 A practical guide for Salesforce architects, developers, and admins preparing for API version 67.0

If you've been putting off reading the Summer '26 release notes because "it's probably just the usual stuff," this is the release to make time for. Summer '26 ships API version 67.0, and buried inside that version bump is what Salesforce itself is calling one of the most significant shifts to the Apex security model in years.

This isn't a UI refresh or a nice-to-have feature. It's a change to how your Apex code behaves at a fundamental level — and if you're not ready for it, upgrading your API version could silently change what data your users can see, or break your deployment outright.

Let's walk through exactly what's changing, why it matters, and how to prepare your orgs (and your clients') before this hits production.

Quick facts: Summer '26 release timeline

  • Sandbox preview: May 8–9, 2026
  • Production rollout weekends: May 15, June 5, and June 12–13, 2026 (varies by instance — check your Maintenance Calendar)
  • New API version: 67.0
  • Biggest theme for developers: Secure-by-default Apex

The three changes that define this release

Three related changes land together at API version 67.0. Individually, each one is significant. Together, they represent a full reversal of Apex's historical security posture — from "open by default, lock it down if you remember" to "locked down by default, open it up if you mean to."



1. Database operations now default to user mode

Before 67.0, SOQL, SOSL, DML, and Database class methods ran in system mode by default. That meant your queries and DML statements quietly ignored the running user's field-level security (FLS), object permissions, and sharing rules — unless you went out of your way to enforce them.

From API 67.0 onward, that default flips. The same operations now run in user mode, meaning they automatically respect whatever the logged-in user is allowed to see and do.

This is good news for security, but it's a real behavior change for existing code. Any class that assumed implicit "God mode" access — pulling back every field and every record regardless of who's running it — will now filter results based on the actual user's permissions.

2. with sharing is the new default for Apex classes

Previously, an Apex class with no sharing keyword defaulted to without sharing in most contexts, silently bypassing org-wide defaults, sharing rules, and role hierarchy.

At API 67.0, a class with no explicit declaration now defaults to with sharing. If you genuinely need a class to ignore sharing rules going forward, you now have to say so explicitly with without sharing — which is exactly the kind of intentional, auditable decision this change is designed to encourage.

3. WITH SECURITY_ENFORCED is gone — replaced by WITH USER_MODE

This one isn't a behavior change, it's a hard compile error. Any SOQL query still using the WITH SECURITY_ENFORCED clause will fail to compile once a class is on API 67.0.

The replacement, WITH USER_MODE, isn't just a rename with better branding. It's meaningfully more thorough:

  • It checks field-level security across the entire query, including the WHERE clause — not just the SELECT list.
  • It correctly handles polymorphic fields like Owner or Task.WhatId.
  • It reports every inaccessible field it finds, not just the first one it hits, via QueryException.getInaccessibleFields().
apex
// This fails to compile at API 67.0:
// "WITH SECURITY_ENFORCED is no longer supported, use WITH USER_MODE instead."
List<Account> accts = [SELECT Id FROM Account WITH SECURITY_ENFORCED LIMIT 1];

// The replacement:
try {
    List<Account> accts = [
        SELECT Id, Name, AnnualRevenue
        FROM Account
        WITH USER_MODE
        LIMIT 1
    ];
} catch (QueryException e) {
    Map<String, Set<String>> blockedFields = e.getInaccessibleFields();
    // Handle or surface the blocked fields gracefully
}

If a query genuinely needs elevated access, be explicit about it with WITH SYSTEM_MODE rather than relying on an implicit default.

The one deliberate exception: triggers

Apex triggers are carved out of this change entirely. Triggers always run in system mode, and that isn't changing — you can't declare a sharing or access mode on a trigger itself. What is affected is the handler class your trigger delegates to. If your trigger-handler pattern relies on a handler class with no explicit sharing declaration, that handler now defaults to with sharing at 67.0. Audit those handler classes specifically.

Why this matters more than a typical governor-limit tweak

Here's the part that should get every architect's attention: your existing test coverage was written against the old defaults. A test class that passes cleanly on API 66.0 can pass just as cleanly on 67.0 and still hide a real production problem, because the assertions were never written to check field- or record-level visibility for a restricted user.

Before bumping any class to API 67.0, it's worth adding test scenarios that specifically run as a constrained user (System.runAs()) and assert on what that user can and cannot see. This is especially important for managed packages, where you don't get a second chance once it's in a subscriber org.

The diagram above sums up the shift at a glance: three quiet defaults that used to favor access are now three defaults that favor restriction — and each one has an explicit escape hatch if you truly need the old behavior.

Your Apex security migration checklist

  1. Search your codebase for WITH SECURITY_ENFORCED. Every instance needs to become WITH USER_MODE before you touch API 67.0.
  2. Audit classes with no sharing declaration, especially trigger handlers. Decide deliberately: should this be with sharing (the new default, no change needed) or does it genuinely need without sharing?
  3. Review Database class calls (Database.query, Database.insert, etc.) for explicit AccessLevel parameters, and add them where system-mode access is actually required.
  4. Add restricted-user test scenarios. Don't just re-run your existing suite — write new assertions that run as a limited-permission user and check data visibility.
  5. Stage the API version bump. Don't flip every class to 67.0 in one deployment. Move incrementally, class by class or module by module, and test as you go.
  6. Pay special attention to managed packages. Once a package version is released with these defaults, there's no quiet fix later — get this right before you ship.

Beyond security: other Apex updates worth knowing

The security overhaul is the headline, but a handful of other Apex changes in Summer '26 are genuinely useful for day-to-day development.

Multiline strings and String.template()

Building JSON payloads or email bodies with a chain of + '\n' + concatenations has always been painful. Triple single-quotes now give you real multiline string literals, and String.template() adds named placeholder interpolation:

apex
String payload = '''
{
    "Account": "${accountName}",
    "Last Updated": "${date}"
}'''.template(new Map<String, Object>{
    'accountName' => 'My Account',
    'date' => Datetime.newInstance(2026, 6, 15, 8, 0, 0)
});

Two small gotchas worth remembering: the newline right after the opening ''' is trimmed automatically, and String.template() renders Datetime values in GMT using yyyy-MM-dd HH:mm:ss — not the running user's local time zone — so format it yourself if you need something else.

Elastic limits for async jobs (Beta)

Orgs that regularly hit their daily asynchronous job ceiling get some breathing room. Queueable and @future jobs can now be enqueued up to twice the licensed daily limit, with anything above the standard threshold processed at a throttled rate instead of failing outright.

The architectural catch: throttling is quiet. A hard limit fails loudly and shows up in your monitoring immediately; a throttled org just gets slower, which can go unnoticed if your alerting only watches for errors. If you plan to lean on this buffer, track the new DailyAsyncApexElasticExecutions and DailyAsyncApexProcessed entries in System.OrgLimits.getMap() and build monitoring around queue depth and latency, not just failure counts.

No-argument constructors required for invocable action parameters

Any custom Apex class used as an input parameter for an invocable action now needs a visible no-argument constructor (public, or global if it's in a managed package). This applies from API 67.0 onward — a good one to check if you're building custom actions for Flow or Agentforce.

@IntegrationTest for real Agentforce and Data 360 callouts (Developer Preview)

Standard Apex unit tests mock callouts and roll back data, which makes it impossible to properly assert on live Agentforce or Data 360 behavior. The new @IntegrationTest annotation allows real, uncommitted-by-default callouts with a controlled way to commit test data mid-transaction using IntegrationTest.commitTestOnly(), plus a @TearDown method for cleanup. Currently scratch-org only, and worth watching as it matures.

The bigger picture

Summer '26 makes one thing clear: Salesforce is done treating security as something you opt into. Between the Apex defaults, the retirement of the OAuth username-password flow, changes to SOAP login(), and the mandatory SAML framework migration, this release is pushing every org toward explicit, auditable access decisions instead of implicit trust.

For architects, that's a genuinely good direction — but it means this is not a release to skip past on your way to the Agentforce headlines. Block time this sprint to search your codebase for WITH SECURITY_ENFORCED, audit your sharing declarations, and get ahead of the API version bump before it becomes a scramble.


Sources: Salesforce Developers Blog — "The Salesforce Developer's Guide to the Summer '26 Release"; Salesforce Architecture Blog — "Summer '26 Release Architect Highlights"; official Summer '26 Release Notes (help.salesforce.com).

Saturday, August 1, 2026

Apex Interview Questions — Part 5 (Design Patterns, Integration, Performance, Scenario-Based)

 

1. What is the Trigger Handler design pattern, and why is it considered a best practice at scale?

It centralizes all trigger logic into a single handler class per object, with the trigger itself only routing to context-specific methods (beforeInsert, afterUpdate, etc.). At scale, this avoids logic scattered across multiple triggers, makes unit testing easier since handler methods can be tested independently of a live trigger context, and gives one place to control execution order and recursion.

2. What is the Selector pattern in Apex, and what problem does it solve?

The Selector pattern centralizes all SOQL queries for a given object into a dedicated “selector” class, instead of scattering the same or similar queries across multiple classes. This avoids duplicated query logic, makes it easy to enforce field-level security and sharing consistently, and simplifies future query changes to a single place.

public class AccountSelector {
    public static List<Account> getActiveAccounts() {
        return [SELECT Id, Name FROM Account WHERE Active__c = true WITH SECURITY_ENFORCED];
    }
}

3. What is the Service Layer pattern in Apex?

A Service Layer class groups related business logic for a specific domain (e.g., OpportunityService) behind clear, well-named public methods, so that triggers, LWCs, and batch jobs all call the same central logic rather than duplicating business rules across multiple entry points.

4. What is dependency injection, and is it commonly used in Apex?

Dependency injection means providing a class with its dependencies (like a selector or service instance) from the outside rather than instantiating them internally, which makes unit testing easier through mocking. It’s less common in vanilla Apex than in languages like Java, but frameworks like fflib and Apex frameworks using the Application factory pattern implement it for more testable, decoupled code.

5. What is the Unit of Work pattern, and why is it useful in Apex?

The Unit of Work pattern batches multiple related DML operations (inserts, updates across different objects) into a single, coordinated commit, tracking the relationships between records so they’re all registered before one final “commit” call executes the DML in the correct dependency order. It reduces the risk of hitting DML statement limits and keeps complex multi-object operations transactionally consistent.

6. What is the difference between a REST callout and a SOAP callout in Apex?

A REST callout uses the Http, HttpRequest, and HttpResponse classes to send/receive typically JSON-based requests over standard HTTP verbs (GET, POST, PUT, DELETE). A SOAP callout uses a WSDL-generated Apex class (via Salesforce’s “Generate from WSDL” tool) to call a SOAP-based web service using strongly-typed method calls that mirror the WSDL’s operations.

7. How do you make a basic REST callout in Apex?

You construct an HttpRequest with an endpoint and method, send it via an Http instance, and inspect the returned HttpResponse:

HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/data');
req.setMethod('GET');
req.setHeader('Authorization', 'Bearer ' + accessToken);

Http http = new Http();
HttpResponse res = http.send(req);

if (res.getStatusCode() == 200) {
    Map<String, Object> result = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
}

8. What is a Named Credential, and why is it preferred over hardcoding endpoint URLs and credentials?

A Named Credential stores an external system’s endpoint URL and authentication details securely at the org level, so Apex code references it by name instead of hardcoding sensitive values like API keys or passwords directly in code. This improves security, makes credentials easy to rotate without a code deployment, and centralizes authentication handling (including OAuth flows) outside of Apex logic.

9. What is a Remote Site Setting, and when is it required?

A Remote Site Setting whitelists an external domain that Apex is allowed to make callouts to; without registering the domain (or using a Named Credential, which handles this automatically), any callout to an unregistered endpoint throws an UnauthorizedEndpointException.

10. How do you handle a callout timeout gracefully in Apex?

You set a timeout explicitly on the HttpRequest (setTimeout(), in milliseconds, up to a max of 120,000ms) and wrap the callout in a try-catch to catch a CalloutException, allowing you to retry, log the failure, or notify the user instead of letting the transaction fail with an unhandled error.

req.setTimeout(10000);
try {
    HttpResponse res = http.send(req);
} catch (CalloutException e) {
    System.debug('Callout failed: ' + e.getMessage());
}

11. What causes a “Too many SOQL queries” error in a complex, multi-trigger scenario, and how do you debug it?

This typically happens when multiple triggers or workflow-driven updates on related objects cascade into each other within the same transaction, each independently issuing SOQL queries that accumulate against the shared 100-query limit. Debugging usually involves checking debug logs for the query count at various points (via the Limits class) and consolidating queries into selector methods called once and reused across the transaction.

12. A batch job is processing millions of records but keeps timing out or hitting heap limits — what would you check first?

First, check whether the execute() method is doing unnecessary work per batch, like re-querying related data inside a loop instead of bulk-querying it once per batch. Also check the batch size (Database.executeBatch()’s second parameter) — reducing it can lower heap usage per execution, and verify the start() method’s QueryLocator isn’t selecting more fields than needed.

13. How would you design a system where multiple integrations update the same Salesforce object without triggers overwriting each other’s changes?

A common approach is to centralize all incoming update logic through a single Apex service layer (rather than separate trigger paths per integration), using the trigger handler pattern to control execution order, along with careful use of Trigger.isUpdate checks and field-level comparisons to avoid unnecessary re-processing or overwriting fields set by a different integration in the same transaction.

14. What would you do if a trigger update in a Batch Apex job causes a “field is not writeable” security error for some users?

This usually indicates the batch is running in a system context without appropriate field-level security enforcement, or the running user in a scheduled context lacks edit access to a specific field. You’d check whether Security.stripInaccessible() or explicit FLS checks are needed, or whether the batch class should run under a more appropriate user context via System.runAs() in tests, and review the actual running user’s permission set in production.

15. How do you optimize a SOQL query that’s slow on a large object with millions of records?

Ensure the WHERE clause filters on indexed fields (standard indexed fields, external IDs, or custom fields marked for indexing via a support case), avoid LIKE '%value%' patterns which can’t use an index efficiently, use selective filters to keep the query below the “non-selective query” threshold, and consider using LIMIT combined with pagination if only a subset of records is actually needed.

16. What is the difference between a selective and non-selective SOQL query?

A selective query filters using indexed fields and returns a small percentage of the total records in the object, allowing Salesforce’s query optimizer to use an index efficiently. A non-selective query — often one filtering on unindexed fields or using leading wildcards — forces a full table scan internally, which becomes increasingly slow as the object’s record count grows into the millions.

17. How would you design Apex code to safely handle a scenario where an external system sends duplicate webhook callouts?

You’d implement idempotency — for example, by having the external system pass a unique transaction/request Id, storing recently processed Ids (in a custom object or platform cache), and checking for that Id before processing a new payload, so a duplicate callout doesn’t create duplicate records or apply the same update twice.

18. What is Platform Cache, and how might it help in a high-volume integration scenario?

Platform Cache stores frequently accessed data in a fast, temporary, org-wide or session-based cache, reducing repeated SOQL queries for data that doesn’t change often (like configuration settings or reference data). In a high-volume integration, it can reduce query counts and improve performance by avoiding redundant lookups within and across transactions.

19. In an interview, how would you answer “How do you handle governor limit exceptions gracefully in production”?

A strong answer covers proactive prevention (bulkifying code, using selectors, monitoring query/DML counts via the Limits class during development) combined with reactive handling — wrapping risky operations in try-catch where feasible, logging detailed context when a LimitException does occur (since it can’t always be caught cleanly), and using Batch Apex or Queueable chaining to break up large operations before they approach a limit in the first place.

20. A hiring manager asks you to describe a real production issue you solved involving Apex — what should a strong answer include?

A strong answer names the specific symptom (e.g., an intermittent UNABLE_TO_LOCK_ROW error or a SOQL 101 limit failure under bulk load), explains the root cause you identified (e.g., DML inside a loop, or two processes updating the same parent record concurrently), describes the concrete fix (bulkifying the trigger, adding FOR UPDATE locking, or restructuring the batch size), and mentions how you verified the fix held up under a realistic data volume test — not just that it passed a single test case.

Apex Interview Questions — Part 4 (Async Apex, Exception Handling, Sharing Rules)

 

1. What is asynchronous Apex, and why would you use it?

Asynchronous Apex runs in a separate thread from the user’s transaction, at a later time, rather than blocking the user’s immediate interaction. It’s used for long-running operations, callouts, bulk processing, or scheduled jobs — anything that shouldn’t hold up the user interface or that needs higher governor limits than synchronous execution allows.

2. What are the four main types of asynchronous Apex?

Future methods, Queueable Apex, Batch Apex, and Scheduled Apex. Each solves a different problem: Future methods handle simple fire-and-forget tasks, Queueable adds chaining and complex parameter support, Batch Apex processes very large data volumes in chunks, and Scheduled Apex runs code at a specific time or on a recurring basis.

3. What is a Future method, and what are its limitations?

A Future method is a static method annotated with @future that runs asynchronously. Its main limitations are that it can only accept primitive data types or collections of primitives as parameters (no sObjects), it can’t be chained to call another future method, and you can’t track its execution status directly.

public class AsyncService {
    @future
    public static void updateAccounts(Set<Id> accountIds) {
        List<Account> accs = [SELECT Id, Description FROM Account WHERE Id IN :accountIds];
        for (Account a : accs) {
            a.Description = 'Updated asynchronously';
        }
        update accs;
    }
}

4. What is Queueable Apex, and how is it different from a Future method?

Queueable Apex implements the Queueable interface and offers advantages Future methods lack: it accepts complex, non-primitive parameters (including sObjects), supports job chaining (one queueable job enqueuing another), and returns a job Id you can use to monitor status via AsyncApexJob.

public class UpdateAccountsQueueable implements Queueable {
    private List<Account> accounts;

    public UpdateAccountsQueueable(List<Account> accounts) {
        this.accounts = accounts;
    }

    public void execute(QueueableContext context) {
        update accounts;
    }
}

5. How do you chain Queueable jobs together?

Inside the execute() method of one Queueable class, you call System.enqueueJob() with a new instance of the next Queueable class. Salesforce allows chaining, but be mindful of the chain depth limits enforced per context (5 in most contexts, unlimited in some Enterprise/Unlimited editions, though best practice is to avoid excessively long chains).

public void execute(QueueableContext context) {
    // do work
    System.enqueueJob(new NextStepQueueable());
}

6. What is Batch Apex, and when should you use it?

Batch Apex implements the Database.Batchable interface and processes large numbers of records (up to 50 million) by splitting them into smaller batches (default 200 records), each executed in its own governor-limit context. It’s the right choice when a Future or Queueable job would exceed limits due to sheer data volume.

public class UpdateAllAccountsBatch implements Database.Batchable<sObject> {
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator('SELECT Id, Description FROM Account');
    }
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        for (Account a : scope) {
            a.Description = 'Batch updated';
        }
        update scope;
    }
    public void finish(Database.BatchableContext bc) {
        // post-processing, e.g., send a summary email
    }
}

7. What are the three methods every Batch Apex class must implement?

start(), which defines the scope of records to process (via a QueryLocator or Iterable); execute(), which runs once per batch of records (called multiple times); and finish(), which runs once after all batches complete, often used for follow-up actions like sending a summary email or chaining another batch job.

8. How do you control the batch size in Batch Apex?

You pass a second parameter to Database.executeBatch() specifying the number of records per batch (default is 200, maximum is 2000):

Database.executeBatch(new UpdateAllAccountsBatch(), 100);

9. What is Scheduled Apex, and how do you implement it?

Scheduled Apex runs code at specified times using the Schedulable interface and a CRON expression. It’s commonly used to trigger a Batch Apex job on a recurring schedule (e.g., nightly data cleanup).

public class NightlyCleanupScheduler implements Schedulable {
    public void execute(SchedulableContext sc) {
        Database.executeBatch(new UpdateAllAccountsBatch());
    }
}
String cronExp = '0 0 2 * * ?'; // every day at 2 AM
System.schedule('Nightly Cleanup', cronExp, new NightlyCleanupScheduler());

10. What is the maximum number of batch jobs that can be queued or active at once?

An org can have up to 5 batch jobs queued or actively processing at the same time. Attempting to queue a 6th throws a LimitException, which is a common real-world gotcha when multiple automated processes try to launch batches simultaneously.

11. Why can’t you make a callout directly inside a trigger?

Triggers execute within the same transaction as the DML that fired them, and Salesforce doesn’t allow synchronous callouts within that context because it would leave the database transaction open and waiting on an external system for an unpredictable amount of time. The workaround is to make the callout asynchronously, typically via a Future method or Queueable class.

12. What is the difference between a checked and unchecked exception in Apex?

Apex doesn’t formally distinguish checked vs. unchecked exceptions the way Java does — all Apex exceptions are effectively unchecked, meaning the compiler doesn’t force you to catch them. However, custom exceptions (extending the built-in Exception class) are commonly used to represent expected, catchable business logic errors.

public class InvalidDiscountException extends Exception {}

if (discount > 100) {
    throw new InvalidDiscountException('Discount cannot exceed 100%');
}

13. What is the difference between DmlException and QueryException?

DmlException is thrown when a DML operation (insert/update/delete) fails, typically due to validation rules, required fields, or triggers throwing errors. QueryException is thrown when a SOQL query fails or returns unexpected results, such as calling .get(0) on an empty list or assigning a multi-row query result to a single sObject variable.

14. What is a custom exception class in Apex, and why create one?

A custom exception class extends the built-in Exception class, letting you represent specific, meaningful error conditions in your application (like InsufficientInventoryException) rather than relying on generic exception types. This makes error handling in calling code more precise and readable.

15. What happens if you don’t catch an exception in Apex?

If an exception isn’t caught, it propagates up the call stack, and if nothing catches it, the entire transaction is rolled back and an unhandled exception error is returned to the user or calling process (which can look like a generic, unhelpful error in an LWC or integration if not handled properly).

16. What is the purpose of a finally block in Apex exception handling?

Code inside a finally block always executes after the try (and catch, if triggered) block, regardless of whether an exception occurred — commonly used for cleanup logic that must run no matter what, like closing a resource or logging that an operation completed.

try {
    processRecords();
} catch (Exception e) {
    System.debug('Error: ' + e.getMessage());
} finally {
    System.debug('Processing attempt finished');
}

17. What is the difference between with sharing, without sharing, and inherited sharing?

with sharing enforces the running user’s record-level sharing rules within that class. without sharing explicitly ignores sharing rules, running with full object access regardless of the user (Apex’s default behavior if unspecified, which is why explicitly declaring it matters). inherited sharing makes the class adopt whatever sharing context it was called from, giving more flexibility for utility classes reused across different contexts.

18. Why does sharing enforcement matter in Apex, and where is it commonly tested in interviews?

Sharing rules only restrict record-level visibility, not object or field-level permissions — those come from profiles and permission sets separately. Interviewers often test whether you know that leaving a class as the Apex default (without sharing behavior) can unintentionally expose or modify records a user shouldn’t have access to, which is a real security review finding in many orgs.

19. What is Security.stripInaccessible() used for?

It’s a method that removes field values a running user doesn’t have access to from a list of sObjects before they’re returned to the client, helping enforce field-level security (FLS) explicitly in Apex — since SOQL queries themselves don’t automatically respect FLS by default.

SObjectAccessDecision decision = Security.stripInaccessible(
    AccessType.READABLE, accountList
);
List<Account> safeAccounts = decision.getRecords();

20. What is the difference between enforcing sharing rules and enforcing field-level security (FLS) in Apex?

Sharing rules (with sharing) control which records a user can see or edit based on ownership, role hierarchy, and sharing settings. FLS controls which fields on a record a user is allowed to view or edit, and must be checked separately (using Security.stripInaccessible(), WITH SECURITY_ENFORCED in SOQL, or manual Schema.sObjectField checks) since sharing rules alone don’t restrict field visibility.

Apex Interview Questions — Part 3 (Triggers Deep-Dive, Trigger Frameworks, Order of Execution, Test Classes)

1. Why is it a best practice to have only one trigger per object?

Salesforce doesn’t guarantee the execution order of multiple triggers on the same object, which can cause unpredictable behavior when logic depends on a specific sequence. A single trigger per object (delegating to a handler class) gives you full control over execution order.

2. What is a trigger handler pattern, and why use one?

A trigger handler pattern moves all business logic out of the trigger itself and into a separate Apex class, with the trigger simply calling handler methods based on the context (before insert, after update, etc.). This keeps triggers thin, makes logic testable in isolation, and avoids duplicated logic across multiple triggers.

trigger AccountTrigger on Account (before insert, after update) {
    AccountTriggerHandler handler = new AccountTriggerHandler();
    if (Trigger.isBefore && Trigger.isInsert) {
        handler.beforeInsert(Trigger.new);
    }
}

Common open-source frameworks include the Apex Trigger Actions Framework, Kevin O’Hara’s SObject Trigger Framework, and Andrew Fawcett’s fflib-based Enterprise Patterns. Most solve the same core problem: centralizing dispatch logic and preventing recursive or duplicate execution.

4. What is the correct order of execution when a record is saved in Salesforce?

At a high level: system validation rules run first, then before triggers, then custom validation rules, then duplicate rules, then the save to the database (but not committed yet), then after triggers, then assignment rules, auto-response rules, workflow rules, escalation rules, entitlement rules, and finally the commit to the database, followed by post-commit logic like sending emails.

5. Why does order of execution matter for interview scenarios?

Interviewers often test whether you understand that workflow field updates can re-trigger the save process, or that before-triggers happen before validation rules run — misunderstanding this can lead to unexpected infinite loops or values being overwritten unexpectedly.

6. How do you prevent a trigger from running recursively?

A common pattern is a static Boolean flag in a handler or utility class that’s set to true the first time the trigger logic runs in a transaction, checked at the start, and used to skip re-execution during any recursive invocation within that same transaction.

public class TriggerControl {
    public static Boolean hasAlreadyRun = false;
}

7. What is the difference between Trigger.isBefore and Trigger.isAfter?

Trigger.isBefore is true when the trigger is executing in a before-context (record not yet saved, so field changes are cheap), while Trigger.isAfter is true in an after-context (record already has its Id, but changing fields here requires a separate DML update).

8. When would you use an after-trigger instead of a before-trigger?

After-triggers are needed when you require the record’s Id (for example, to create related child records) or need to update related records rather than the triggering record itself. Before-triggers are preferred for simply modifying fields on the triggering record, since they avoid an extra DML statement.

9. What is a Test Class in Apex, and why is it required?

A test class contains methods annotated with @isTest that verify Apex code behaves correctly. Salesforce requires at least 75% code coverage across an org’s Apex code (with meaningful assertions, not just coverage) before code can be deployed to production.

@isTest
private class AccountTriggerHandlerTest {
    @isTest
    static void testAccountInsert() {
        Account acc = new Account(Name = 'Test Account');
        insert acc;
        System.assertNotEquals(null, acc.Id);
    }
}

10. What is Test.startTest() and Test.stopTest() used for?

They mark a boundary around the code you actually want to measure and test — code inside this block gets a fresh set of governor limits, and any asynchronous Apex called inside it (Future, Queueable, Batch) executes synchronously when Test.stopTest() is reached, letting you assert on its results immediately.

Test.startTest();
MyQueueableClass.enqueueJob();
Test.stopTest();
System.assertEquals(1, [SELECT COUNT() FROM Task]);

11. Why shouldn’t test classes rely on existing org data?

Tests that query existing records assume specific data exists, which may not be true in other environments (like a fresh sandbox or another org), causing tests to fail unpredictably. Best practice is to create all necessary test data within the test method itself, or through a @testSetup method.

12. What is @testSetup used for?

A method annotated with @testSetup runs once before all test methods in a class and creates common test data that each test method can then use, reducing duplicated setup code and improving test run performance.

@testSetup
static void setup() {
    insert new Account(Name = 'Test Co');
}

13. What is SeeAllData=true, and why is it generally discouraged?

It’s an annotation that allows a test method to access real org data instead of being isolated in its own data sandbox. It’s discouraged because it makes tests dependent on the state of the org’s data, which can change over time and cause tests to fail inconsistently across environments.

14. How do you test that an exception is thrown correctly?

Wrap the code that should throw the exception in a try-catch within the test method, and assert that the catch block was actually reached (or that no exception was silently swallowed):

try {
    insert new Account(); // missing required field
    System.assert(false, 'Expected an exception');
} catch (DmlException e) {
    System.assert(e.getMessage().contains('REQUIRED_FIELD_MISSING'));
}

15. What is code coverage, and is 75% coverage alone considered good practice?

Code coverage measures the percentage of Apex lines executed by test methods. While Salesforce enforces a minimum of 75% to deploy to production, coverage alone doesn’t guarantee correctness — tests should include meaningful assertions that verify actual behavior, not just execute lines without checking results.

16. What is the difference between unit tests and integration-style tests in Apex?

Unit tests isolate a single method or class’s logic, often using mock callouts or minimal data. Integration-style tests verify that multiple components (trigger, handler, related objects) work correctly together, closer to a real end-to-end scenario, though still run entirely within Salesforce’s test framework.

17. How do you test a class that makes an HTTP callout?

You implement the HttpCalloutMock interface to simulate the external response, then register it with Test.setMock() before the callout code runs, since real HTTP callouts aren’t allowed during test execution.

Test.setMock(HttpCalloutMock.class, new MyMockResponseGenerator());

18. What is System.runAs() used for in test classes?

System.runAs() lets you execute test code in the context of a specific user, which is essential for testing sharing rules, field-level security, and profile-based permission behavior, since test methods otherwise run as the user executing the test (often an admin with full access).

User testUser = [SELECT Id FROM User WHERE Profile.Name = 'Standard User' LIMIT 1];
System.runAs(testUser) {
    // logic tested with restricted permissions
}

19. Why should test classes cover both positive and negative scenarios?

Testing only the “happy path” (valid data, expected flow) misses bugs that occur with invalid input, missing data, or edge cases like bulk operations or null values — these negative and boundary scenarios are often exactly what breaks in production and what interviewers expect you to mention.

20. What is a trigger’s context variable, Trigger.newMap, used for?

Trigger.newMap provides a map of record Id to the new version of the sObject, available in update, before/after contexts where an Id already exists. It’s useful for efficiently looking up a specific record’s new state by Id without looping through Trigger.new manually.

Account updatedAcc = Trigger.newMap.get(someAccountId);

Apex Interview Questions — Part 2 (Governor Limits, SOQL/SOSL, Bulkification)

 

1. What are the key governor limits every Apex developer should know?

The most commonly tested ones are: 100 SOQL queries per transaction (200 for async), 150 DML statements per transaction, 10,000 DML rows per transaction, 50,000 SOQL query rows retrieved, and 6MB heap size (12MB for async). These exist because Apex runs on shared, multi-tenant infrastructure.

2. What is the difference between synchronous and asynchronous governor limits?

Asynchronous Apex (Future, Queueable, Batch, Scheduled) generally gets higher limits than synchronous Apex — for example, 200 SOQL queries instead of 100 — because async transactions run separately from the user's request and don't need to complete within the same response window.

3. What happens when a governor limit is exceeded?

Salesforce throws a System.LimitException, which — unlike most other exceptions — cannot always be caught and safely continued from, since it typically indicates the current transaction has already used up a shared platform resource. The entire transaction is rolled back.

4. How can you check how close you are to a limit at runtime?

The Limits class provides methods like Limits.getQueries(), Limits.getLimitQueries(), Limits.getDmlStatements(), and Limits.getLimitDmlStatements(), which let you inspect current usage against the maximum allowed within a transaction.

apex
System.debug('SOQL used: ' + Limits.getQueries() + ' / ' + Limits.getLimitQueries());

5. Why should SOQL queries never be placed inside a for loop?

Each iteration of the loop would count as a separate query against the 100-query-per-transaction limit, so processing a batch of 200 records could exceed the limit almost immediately. The fix is to query once outside the loop and iterate over the results in memory.

apex
// Bad
for (Account a : accounts) {
    List<Contact> cons = [SELECT Id FROM Contact WHERE AccountId = :a.Id];
}

// Good
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
    // group in memory
}

6. What is SOQL used for, and what are its main clauses?

SOQL retrieves records from Salesforce's database. It supports SELECT, FROM, WHERE, ORDER BY, LIMIT, GROUP BY, and relationship queries, but doesn't support arbitrary joins the way SQL does — relationships are traversed instead through parent-child or child-parent syntax.

7. How do you write a parent-to-child SOQL query?

You use a subquery with the child relationship name (usually the plural of the child object, or a custom relationship name ending in __r for custom objects):

apex
List<Account> accs = [
    SELECT Name, (SELECT LastName FROM Contacts)
    FROM Account
];

8. How do you write a child-to-parent SOQL query?

You traverse upward using dot notation on the relationship field:

apex
List<Contact> cons = [
    SELECT LastName, Account.Name, Account.Industry
    FROM Contact
];

9. What is SOSL, and when would you use it over SOQL?

SOSL performs a text search across multiple objects and fields at once, useful when you're searching for a term (like a name or keyword) but don't know which object or field it lives in. SOQL is better when you already know the specific object and filtering criteria.

apex
List<List<sObject>> results = [FIND 'Acme' IN ALL FIELDS RETURNING Account(Name), Contact(LastName)];

10. Can SOSL return records from multiple different objects in one query?

Yes — that's one of its main advantages over SOQL. The result is a list of lists, where each inner list corresponds to one of the object types specified in the RETURNING clause.

11. What is bulkification and why is it a best practice?

Bulkification means designing Apex logic to correctly and efficiently process multiple records in a single execution context — for example, a trigger firing on 200 records from a data import — rather than assuming only one record will ever pass through at a time.

12. What's a common real-world bulkification mistake in trigger code?

Placing a SOQL query or DML statement directly inside a trigger's for loop over Trigger.new. This works fine with one test record but fails in production once more than roughly 100-150 records are processed in a single batch.

13. How do you avoid DML statements inside a loop?

Collect the records that need to be inserted, updated, or deleted into a list first, then perform a single DML call after the loop completes:

apex
List<Contact> toUpdate = new List<Contact>();
for (Contact c : contacts) {
    c.Description = 'Reviewed';
    toUpdate.add(c);
}
update toUpdate;

14. What is the difference between upsert and using insert/update separately?

upsert automatically determines whether each record already exists (based on its Id or a specified external Id field) and either inserts or updates it accordingly, in a single DML statement — simplifying logic that would otherwise require separate insert and update handling.

15. What is an external ID field, and how is it used with upsert?

An external ID field is a custom field marked as an External ID in its field-level settings, typically used to match records against an external system's identifier during data integration. upsert can use this field instead of the Salesforce record Id to determine whether a match exists.

apex
upsert accountList External_Id__c;

16. What is the maximum number of records that can be processed by a single DML statement?

Apex allows up to 10,000 records per DML operation within a single transaction, which is why very large data loads are often handled through Batch Apex rather than a single synchronous DML call.

17. What is the difference between Database.insert() and a plain insert statement?

A plain insert statement fails the entire operation if even one record throws an error. Database.insert(records, false) allows partial success — records that fail don't stop the others from being inserted, and you can inspect a Database.SaveResult list to see which succeeded or failed.

apex
Database.SaveResult[] results = Database.insert(accountList, false);

18. What is a Database.SaveResult, and what information does it provide?

It's the object returned by Database.insert/update/delete calls made with partial-success mode enabled. Each result indicates whether that specific record succeeded (isSuccess()) and, if not, provides error details through getErrors().

19. Why is querying only the fields you need considered a best practice?

Retrieving unnecessary fields increases heap usage and can slow down query performance, especially with large result sets. It also avoids hitting the 50,000-row query limit unnecessarily and keeps code more maintainable by making dependencies explicit.

20. What is LIMIT in SOQL and why should you use it deliberately?

LIMIT restricts the number of records a SOQL query returns. Using it deliberately (rather than relying on default behavior) helps control performance, avoid exceeding row limits, and makes intent clear — for example, LIMIT 1 when you only expect a single matching record.

apex
Account acc = [SELECT Id FROM Account WHERE Name = 'Acme Corp' LIMIT 1];

Apex Interview Questions — Part 1 (Beginner Basics)

 

1. What is Apex and how is it different from Java?

Apex is Salesforce's proprietary, strongly-typed, object-oriented programming language used to execute flow and transaction control statements on the Salesforce platform. It's syntactically similar to Java, but it runs on Salesforce's multi-tenant servers, is tightly coupled with the database (native DML and SOQL support), and is governed by strict limits (governor limits) that don't exist in standard Java since Salesforce's infrastructure is shared across all customers.

2. What are the primitive data types supported in Apex?

Apex supports primitive types including Integer, Long, Double, Decimal, String, Boolean, Date, Datetime, Time, ID, and Blob. These behave similarly to primitives in other languages but ID is Salesforce-specific and represents an 18-character record identifier.

3. What is the difference between a List, Set, and Map in Apex?

  • List: an ordered collection that allows duplicate values, accessed by index.
  • Set: an unordered collection of unique values — no duplicates allowed.
  • Map: a collection of key-value pairs where each key is unique, used heavily for quick lookups (e.g., mapping an Id to an sObject).
apex
List<String> names = new List<String>{'Alice', 'Bob'};
Set<Id> accountIds = new Set<Id>();
Map<Id, Account> idToAccount = new Map<Id, Account>();

4. What is an sObject in Apex?

An sObject is a generic representation of any Salesforce object (standard or custom) — like Account, Contact, or MyCustomObject__c. Every Salesforce record you query, insert, update, or delete in Apex is handled as an sObject or a specific subtype of it.

5. How do you declare and use a variable in Apex?

Variables are declared with a type followed by a name, optionally initialized with a value:

apex
Integer counter = 0;
String greeting = 'Hello';
Account acc = new Account(Name = 'Acme Corp');

6. What is the difference between == and .equals() in Apex?

In Apex, == compares value equality for primitives (unlike Java, where == compares object references for non-primitives). For strings and primitive types, == and .equals() behave the same way in Apex — this is a key difference from Java that trips up developers coming from a Java background.

7. What are Apex collections and why are they important?

Collections (List, Set, Map) let you work with multiple records or values efficiently in memory, instead of processing them one at a time. They're essential for bulkification — processing many records in a single operation instead of looping DML or SOQL calls, which helps avoid hitting governor limits.

8. What is a Trigger in Apex?

A trigger is a piece of Apex code that executes automatically before or after specific DML events (insert, update, delete, undelete) occur on a Salesforce object. Triggers are commonly used to enforce business logic, validate data, or update related records automatically.

apex
trigger AccountTrigger on Account (before insert, after update) {
    // logic here
}

9. What are the different Trigger events (before insert, after update, etc.)?

Apex triggers support: before insert, after insert, before update, after update, before delete, after delete, and after undelete. "Before" events are used to modify field values on the record before it's saved; "after" events are used when you need the record's Id or need to affect related records.

10. What is the difference between trigger.new and trigger.old?

Trigger.new contains the new versions of the records (available in insert/update/undelete triggers), while Trigger.old contains the old versions of the records before the change (available in update/delete triggers). Comparing them lets you detect exactly which fields changed.

11. What is SOQL and how is it different from SQL?

SOQL (Salesforce Object Query Language) is used to query data from Salesforce's database. Unlike standard SQL, SOQL only supports querying (not full DDL/DML), and it automatically understands relationships between Salesforce objects — but it doesn't support arbitrary joins the way SQL does.

apex
List<Account> accs = [SELECT Id, Name FROM Account WHERE Industry = 'Technology'];

12. What is the difference between SOQL and SOSL?

SOQL queries one object (and its related objects) at a time and is best when you know which object(s) to search. SOSL (Salesforce Object Search Language) searches text across multiple objects simultaneously, similar to a full-text search — useful when you don't know which object contains the match.

13. What are governor limits in Salesforce, and why do they exist?

Governor limits are runtime limits enforced by Salesforce (e.g., max 100 SOQL queries or 150 DML statements per transaction) to ensure no single customer's code monopolizes shared, multi-tenant server resources. Exceeding a limit throws a runtime exception that can't be caught with normal try-catch for certain limit types.

14. What is bulkification and why does it matter in Apex?

Bulkification means writing Apex so it correctly handles multiple records at once (e.g., 200 records triggered by a bulk data load), instead of assuming only one record will ever be processed. This typically means moving SOQL/DML operations outside of loops.

apex
// Bad: DML inside a loop
for (Account a : accounts) {
    update a;
}

// Good: bulkified
update accounts;

15. What is a static variable in Apex, and when would you use one?

A static variable belongs to the class itself rather than any specific instance, and its value persists for the duration of a single transaction. It's commonly used to prevent recursive trigger execution by acting as a "flag" that's checked and set once per transaction.

apex
public class TriggerHandler {
    public static Boolean hasRun = false;
}

16. What is the difference between a class variable and an instance variable?

A class (static) variable is shared across all instances of a class and tied to the transaction, while an instance variable belongs to a specific object instance created with new and can hold different values for each instance.

17. What is an Apex constructor, and how do you define one?

A constructor is a special method that runs when an object is instantiated with new, used to set up initial state. It shares the class's name and has no return type.

apex
public class Employee {
    String name;
    public Employee(String name) {
        this.name = name;
    }
}

18. What is the difference between public, private, and global access modifiers?

  • private: accessible only within the same class (default if unspecified).
  • public: accessible within the same application/namespace.
  • global: accessible from anywhere, including other namespaces and via API — required for Apex used in managed packages or exposed externally.

19. What is a DML statement, and what are the common DML operations in Apex?

DML (Data Manipulation Language) statements modify records in the database. The common Apex DML operations are insert, update, delete, undelete, and upsert (which inserts or updates depending on whether the record already exists).

20. What is a try-catch block, and why is exception handling important in Apex?

A try-catch block lets you gracefully handle runtime errors instead of letting them crash the transaction with an unhandled exception. It's especially important in Apex because unhandled exceptions in triggers or Apex-called-from-LWC can surface confusing errors to end users if not caught and re-thrown with a clear message.

apex
try {
    insert new Account(Name = 'Test');
} catch (DmlException e) {
    System.debug('Insert failed: ' + e.getMessage());
}