Apex Trigger Interview Questions: Bulkification, Recursion, and Test Cases

Practice Apex trigger interviews with bulk Account updates, collection-based SOQL and DML, repeat-update reasoning, and precise Salesforce test cases.

Author: PracHub

Published: 9/9/2026

Apex Trigger Interview Questions: Bulkification, Recursion, and Test Cases

September 9, 2026

Quick Overview

Work through an Account-to-Contact trigger exercise, explain why a transaction-wide Boolean can skip valid work, and design assertions for bulk and repeat updates.

Software EngineerFree

A trigger that works for one Account can still skip records in a bulk update, repeat unnecessary writes, or suppress a legitimate second change. Strong Apex trigger interview answers explain the business transition and the transaction boundary before reaching for a recursion flag.

This guide develops an original Account-to-Contact exercise. Official facts describe Salesforce behavior; practice reasoning explains our proposed implementation and tests. No candidate report is used to claim that these are questions from a particular employer. This is Salesforce platform development preparation, not a guide to interviewing at Salesforce as a company.

Verification boundary: the policy cases below were executed in a local Python model. The Apex trigger and test class were reviewed as examples but were not compiled or run in a Salesforce org. Expected Apex outcomes are labeled accordingly; model results are not platform execution evidence.

Start with Implement an Idempotent Versioned Database Update to practice distinguishing duplicate work from a new valid transition, then apply that distinction to the trigger exercise.

Collect changed Accounts, query related Contacts in bulk, and update only mismatched descriptions.

What should the trigger actually do?

Our fictional requirement is narrow: when an existing Account’s Rating changes, synchronize each related Contact’s Description to Account rating: <Rating>. Clearing Rating clears the Description. An Account update that leaves Rating unchanged must not touch Contacts. If a Contact already has the desired Description, avoid updating it again.

This exercise assumes that this automation owns Contact.Description and runs as system-mode business automation in an isolated practice org. Real Contact descriptions often contain user-entered notes, so this is not a deployment recommendation for an existing organization. A production design would normally choose a dedicated field and agree on ownership, permissions, and competing automation first.

Ask the interviewer whether synchronization also applies to newly created Contacts, Account insertion, and reparented Contacts. Those events are outside this after-update example. Saying so matters: the handler cannot guarantee a permanent cross-object invariant when it only runs on one specific event.

Account transitionExisting Contact descriptionDesired result
Cold → HotoldAccount rating: Hot
Hot → HotoldLeave unchanged: no Rating transition
Hot → WarmAccount rating: HotAccount rating: Warm
Warm → nullAccount rating: WarmClear Description
Cold → HotAccount rating: HotNo Contact write needed
Cold → Hot, no ContactsNoneNo Contact write needed

The apparently surprising second row is intentional. This trigger responds to a Rating transition; it is not a background repair job for historical inconsistencies. If the requirement is ongoing reconciliation, design and test that separately.

Before or after, and which context values matter?

Official facts: before triggers can modify the records being saved without a separate update to those same records. After triggers are useful for affecting related records, and the triggering records are read-only in that context. Trigger.new provides current records; an update’s Trigger.oldMap supports comparison with prior values. Apex trigger fundamentals

We choose after update because the exercise writes related Contacts, not the Account rows in Trigger.new. The trigger delegates to a handler:

trigger AccountRatingSync on Account (after update) {
    AccountRatingSyncHandler.apply(Trigger.new, Trigger.oldMap);
}

The handler’s contract is specifically an update context with an old record for every current ID. Do not silently reuse it for before insert, where that assumption is wrong. A thin trigger makes the boundary visible, but moving bad logic into a class does not make it bulk-safe.

In an interview, explain why each context value exists rather than listing every trigger event from memory. Here the important comparison is old Rating versus new Rating, and the important output is a collection of Contact updates.

Why does the single-record solution fail?

Consider querying Contacts inside a loop over Accounts and updating each Contact immediately. With one Account and one Contact, that can look correct. With many Accounts, query count grows with parent count and DML calls grow with child count. The code’s resource use follows the data volume in the wrong way.

Official facts: Salesforce’s bulk-trigger guidance recommends processing collections. It documents a synchronous SOQL query limit of 100, a DML statement limit of 150 per transaction, and trigger execution in batches of up to 200 records. Queries and DML should operate on collections where possible. Bulk Apex Triggers

Do not claim that the 101st Account always produces the first failure: other automation may already have consumed resources. Nor does one query make a trigger safe for unlimited child records. Query rows, DML rows, CPU, heap, and the rest of the transaction still matter.

Our proposed handler changes the shape of the work. It first builds an Account-ID-to-desired-description map. If that map is empty, it returns before querying. It then queries related Contacts once for that invocation and accumulates only the necessary updates.

A collection-based handler you can explain

public class AccountRatingSyncHandler {
    public static void apply(List<Account> rows,
                             Map<Id, Account> oldRows) {
        Map<Id, String> desired = new Map<Id, String>();
        for (Account a : rows) {
            if (a.Rating != oldRows.get(a.Id).Rating) {
                desired.put(a.Id, a.Rating == null
                    ? null : 'Account rating: ' + a.Rating);
            }
        }
        if (desired.isEmpty()) return;
        Set<Id> accountIds = desired.keySet();
        List<Contact> changes = new List<Contact>();
        for (Contact c : [SELECT Id, AccountId, Description
                         FROM Contact
                         WHERE AccountId IN :accountIds]) {
            String target = desired.get(c.AccountId);
            if (c.Description != target) {
                changes.add(new Contact(Id=c.Id,
                                        Description=target));
            }
        }
        if (!changes.isEmpty()) update changes;
    }
}

The map serves two purposes: selecting eligible parents and looking up each child’s target value. The null target is meaningful, so membership comes from the query’s Account-ID filter rather than treating a null map value as “nothing to do.”

There is one query when eligible Accounts exist, and at most one Contact DML statement per handler invocation. The final list contains sparse updates: only the Contact ID and intended Description. That keeps the write’s intent clear and avoids accidentally copying unrelated fields into the update.

These counts are predictions for the isolated handler, not measurements from Salesforce. For 205 changed Accounts processed as 200 plus five in one synchronous transaction, expect two handler invocations. “One query” therefore means one per eligible invocation, not one for the entire transaction.

Why not use a transaction-wide static Boolean?

Official context: Salesforce documents a helper-class static Boolean that skips later invocations within a transaction. That mechanism deliberately means “run only once.” It is not automatically equivalent to “process every eligible record correctly.” Salesforce recursion example

Our counterexample: a synchronous update contains 205 Accounts. The first trigger batch processes 200 and marks the transaction as already processed. The later batch contains five different Accounts, but the flag skips them. Those five records are valid work, not recursive duplicates.

A static set of processed IDs is more selective, but still needs a business definition. Suppose one Account changes Cold → Hot and later Hot → Warm in the same transaction. A permanent “already saw this ID” guard can suppress the second valid transition. Tracking an operation, transition, or active execution path may be more appropriate, depending on the automation graph.

Our example avoids a global guard. It checks the source-field transition and the child’s desired value. Re-entering with no Rating change returns early; re-entering when children already match avoids another Contact update. This is useful idempotent behavior within the stated assumptions, not proof that every possible Flow and trigger interaction terminates.

If another automation continually reverses the same fields, map that cycle and resolve ownership. Adding an ever-broader skip condition may hide the conflict while leaving inconsistent records. Salesforce’s architecture guidance is a useful starting point for reviewing the wider record-triggered automation design. Record-Triggered Automation

A transaction-wide Boolean skips the second five-record batch; per-record eligibility processes all 205.

What should the bulk test assert?

Build 205 Accounts with Rating Cold and one Contact per Account. Give every Contact Description old. Update all Ratings to Hot in one operation. The primary assertion is that all 205 related Contacts now have the expected Description; checking only the first record would miss the exact defect we are testing.

Then update the same Account list again without changing Rating. In the isolated example, expect no handler query and no Contact update. The caller’s Account update is still a DML statement, so an assertion about total DML must account for it.

A useful excerpt from the proposed Apex test is:

Integer beforeQueries = Limits.getQueries();
Integer beforeDml = Limits.getDmlStatements();
update accounts; // unchanged Rating
System.assertEquals(beforeQueries, Limits.getQueries());
System.assertEquals(beforeDml + 1, Limits.getDmlStatements());

Those exact count assertions assume no other Account automation. In a real org, first identify the additional work rather than weakening the assertion until it happens to pass. The behavior assertion remains essential even when an org-level resource budget needs a broader bound.

Next, change the first Account from Hot to Warm in the same test transaction and re-query its Contact. Expect Account rating: Warm. This catches guards that allow each ID only once. Add separate cases for clearing Rating, no Contacts, multiple Contacts, and mixed changed/unchanged Accounts.

Official testing guidance: Salesforce’s trigger-testing module demonstrates creating test records, performing the DML operation, and checking the result with assertions. Coverage alone does not describe whether the required behavior was checked. Testing Apex Triggers

What was verified, and what remains an org test?

The local Python model executed the transition and child-value rules. Its 200-record batch changed 200 descriptions; the next batch changed five. Repeating the same Ratings produced zero modeled queries and writes. A second valid transition changed one child, clearing Rating produced null, and an already-correct child required no write.

The model also simulated the global-Boolean shortcut: processing only the first batch left five descriptions unchanged. These results validate the counterexample’s logic. They do not validate Apex syntax, governor-limit accounting, Salesforce batching, sharing, or order of execution.

The accompanying proposed Apex test class sets up its own data, uses a Test.startTest/Test.stopTest boundary, re-queries results, and checks the repeated update. Running that class in an isolated Salesforce environment is the next verification step. No passing Apex execution, deployment, or coverage percentage is claimed here.

For failure handling, ask whether one invalid Contact should reject the whole synchronization or whether partial success is allowed. The example uses ordinary list DML and does not implement a partial-success recovery policy. Introducing partial success would require explicit error handling and a reconciliation strategy; it is a different contract, not a free optimization.

Practice the reasoning beyond Apex syntax

The verified PracHub prompts below exercise transferable skills. They are not labeled Apex-specific questions because the available question set covers broader engineering scenarios.

PracHub questionConnection to this exercise
Implement an Idempotent Versioned Database UpdateDistinguish repeated work from a newer valid transition.
Design autocomplete and merchant bulk editsReason about collection updates and partial failure.
Write good tests and define integration testsSeparate policy-model confidence from platform execution.
In-Memory Key-Value Store with Nested TransactionsExplain transaction boundaries and rollback expectations.
Generate Account Email NotificationsIdentify which transitions should cause side effects.

Rehearse Implement an Idempotent Versioned Database Update after reviewing the 205-record case. Your answer should identify eligible work, bound resource use, and name the assertion that would expose skipped records. That is more convincing than presenting a recursion flag as a universal solution.

Sources and Further Reading


Comments (0)