Platform Developer II Interview Questions
25 expert-curated Platform Developer II interview questions — covering enterprise patterns, Platform Events, CDC, Apex REST, SOSL, large data volumes, and advanced LWC.
Platform Developer II Interview Questions Content
Apex Enterprise Patterns (from the fflib open-source framework) provide a structured, scalable architecture: (1) Selector Layer: Centralises all SOQL queries for an object. Each selector class queries the object and enforces consistent field sets and FLS. Prevents repeated, inconsistent queries across the codebase. (2) Domain Layer: Encapsulates business logic for a specific object (the "domain"). Trigger handlers call the domain class, which contains methods like
onBeforeInsert(). (3) Service Layer: Coordinates cross-object business logic and orchestrates calls between domain and selector layers. Called from triggers, LWC, REST endpoints. (4) Application Factory: A factory class (using the Selector/Domain/Service interfaces) that allows dependency injection and mocking in tests. This pattern makes large codebases maintainable, testable, and consistent.
A Trigger Framework separates trigger logic from the trigger itself via a handler class. Pattern: one trigger per object, all logic in a handler class. The trigger only calls handler methods:
handler.beforeInsert(Trigger.new); Benefits: testability, maintainability, single entry point. Preventing recursion: When a trigger updates a field, which fires another trigger, creating an infinite loop. Solutions: (1) Static Boolean flag: A static variable (TriggerHandler.firstRun = false) set to false after the first execution, preventing re-entry. Check it at the start of the handler. (2) Static Set of IDs: Track which record IDs have been processed in this transaction and skip them if seen again. (3) Use ISCHANGED() in flow criteria to prevent re-firing. Frameworks like fflib and Dan Appleman's pattern provide this natively.
The Singleton pattern ensures a class has only one instance per transaction. In Apex, this is implemented using a static variable:
private static MyClass instance; public static MyClass getInstance() { if (instance == null) instance = new MyClass(); return instance; }. Common uses: (1) Configuration caches: Load Custom Metadata or Custom Settings once and cache in the singleton instance to avoid repeated SOQL queries across multiple trigger invocations in the same transaction. (2) Trigger context flags: The recursion-prevention flag is effectively a singleton state. (3) Service locators: The Application Factory in enterprise patterns uses singletons. Important: in Apex, static variables are reset per transaction — the singleton lives only within one transaction context.
Queueable chaining: within the
execute() method of a Queueable, you can enqueue another Queueable using System.enqueueJob(new NextJob()); This creates a chain of async jobs that execute sequentially. Limits: (1) In Developer/Sandbox orgs, chains are limited to 5 depth levels. (2) In Production, chains can be unlimited depth but are subject to the 250,000 async jobs per 24h org limit. (3) Only one child job can be enqueued from within a single execute() call. Error handling: if a job in the chain fails, subsequent jobs do not execute. Implement try-catch with error logging and conditional chaining. Use Queueable chaining for sequential processing (e.g., paginated API calls where each job processes one page and enqueues the next).
By default, Batch Apex re-instantiates the class between each
execute() call, resetting all instance variables. Database.Stateful is an interface that, when implemented, preserves instance variables across all execute() chunks. Use cases: (1) Accumulating running totals (total records processed, total errors). (2) Building a summary to include in the finish() notification email. (3) Tracking which records were processed for a final reconciliation. Trade-off: Stateful batches consume more heap memory as state is serialised between chunks. Keep state variables lightweight (primitives, small collections). Example: public class MyBatch implements Database.Batchable<sObject>, Database.Stateful { Integer errorCount = 0; ... } — errorCount persists across all execute() calls.
Platform Events are Salesforce's publish-subscribe messaging system. Events are published and consumed by triggers, flows, processes, or external subscribers. Publish Immediately: The event is published as soon as
EventBus.publish() is called, regardless of whether the publishing transaction commits or rolls back. Use when the event must fire even if the publisher's transaction fails (e.g., sending error notifications). Publish After Commit: The event is only published after the publishing transaction successfully commits. This is the default. Ensures subscribers don't react to events from rolled-back transactions. Replay ID: Each published event has a Replay ID. Subscribers can specify a replay ID to re-subscribe from a past event (up to 72 hours retention) — useful for recovering from subscriber downtime. External subscribers use CometD streaming to receive events.
Change Data Capture (CDC) automatically publishes change events when Salesforce records are created, updated, deleted, or undelete — no custom code needed. CDC events contain: the changed fields and their new values, the ChangeEventHeader (operation type, record IDs, entity name, changed fields list), and gap events (indicating missed events due to subscriber downtime). Differences from Platform Events: (1) CDC is automatically generated from record changes; Platform Events require explicit publishing. (2) CDC captures the actual changed data; Platform Events carry custom payloads. (3) CDC is primarily for integration/replication use cases; Platform Events are for general event-driven architectures. CDC events are available for standard objects (enable in Setup) and custom objects. Replay ID and 72-hour retention apply to CDC as well.
NavigationMixin: Mix into LWC to navigate programmatically.
this[NavigationMixin.Navigate]({ type: 'standard__recordPage', attributes: { recordId, objectApiName, actionName: 'view' } }); Supports record pages, object home, named pages, external URLs. Lightning Message Service (LMS): Publish-subscribe communication between unrelated LWC components (and Aura/VF). Define a Message Channel XML file, import it, and use publish() / subscribe(). Decouples components without parent-child hierarchy requirements. Jest Testing: LWC unit tests use Jest with @salesforce/sfdx-lwc-jest. Key patterns: describe/it/expect structure, createElement for mounting components, registerTestWireAdapter or registerApexTestWireAdapter for mocking wire adapters, jest.mock() for Apex imports. Tests verify DOM output and event dispatch.
Aura Custom Events: Component events (bubble up the component tree) and Application events (broadcast to all registered listeners in the page). Application events in Aura are global broadcasts — any component on the page listening for that event receives it, regardless of hierarchy. This creates tight coupling and potential performance issues. Lightning Message Service (LMS): The modern standard for cross-component communication in both LWC and Aura. Uses a declarative Message Channel definition. Components subscribe to specific channels, reducing global pollution. LMS works across all UI frameworks (LWC, Aura, Visualforce in Lightning). Preferred over Aura Application Events for new development. LMS supports scoping (active tab only) and is more performant. Migrate Aura Application Event-based communication to LMS.
Apex REST resources are created using the
@RestResource(urlMapping='/myresource/*') class annotation. Methods use HTTP verb annotations: @HttpGet (read), @HttpPost (create), @HttpPut (upsert), @HttpPatch (partial update), @HttpDelete. Access request/response via RestContext.request and RestContext.response. The request body is in RestContext.request.requestBody (a Blob — deserialise with JSON.deserialize()). Path parameters: access via RestContext.request.requestURI. Return values from @HttpGet methods are automatically serialised to JSON. Authentication: external systems call via OAuth 2.0 (Connected App). URL: https://instance.salesforce.com/services/apexrest/myresource/. Versioned resources: use URL mapping to version: /v1/myresource/*.
The
Continuation class enables long-running callouts from Visualforce (and LWC via Apex) without holding a server thread. Instead of a synchronous callout (max 120s, blocks thread), a Continuation action: (1) Creates a Continuation object with a timeout (max 120s) and adds callout requests to it. (2) Returns the Continuation from the action method — Salesforce suspends the transaction and releases the server thread. (3) When the external response arrives, Salesforce resumes the transaction and calls the callback method with the response. (4) The callback method processes the response and updates the page. Benefits: does not consume the synchronous callout timeout limit against a server thread. Supports up to 3 parallel callout requests per Continuation. Used for integrations with slow external APIs that would otherwise exceed the callout timeout.
Named Credentials store the URL and authentication settings for an external endpoint, preventing credentials from being hardcoded in Apex. In Apex callouts, reference:
req.setEndpoint('callout:MyNamedCredential/path'); Authentication protocols supported: Password (Basic Auth), OAuth 2.0 (client credentials, JWT bearer, web server flow), AWS Signature V4, JWT, No Auth. Per-User Named Credentials: Each Salesforce user authenticates individually to the external system. The named credential stores the URL; each user must complete an OAuth flow to store their own token. Ideal for user-context integrations. Per-Org: One shared credential for all users. Authentication secrets are stored securely in Salesforce infrastructure and never exposed to Apex code. Named Credentials automatically handle token refresh for OAuth.
SOSL (Salesforce Object Search Language) performs full-text searches across multiple objects simultaneously. Syntax:
FIND {search term} IN ALL FIELDS RETURNING Account(Name, Id), Contact(FirstName, LastName). Clauses: IN: Scope — ALL FIELDS, NAME FIELDS, EMAIL FIELDS, PHONE FIELDS. RETURNING: Specify objects and fields to return (can include WHERE and ORDER BY per object). WITH: Additional filters — WITH DATA CATEGORY, WITH NETWORK, WITH DIVISION, WITH SNIPPET. Results are a List<List<sObject>>. Governor limit: 20 SOSL queries per transaction; 2,000 rows returned per query. Use SOSL for search-box style queries; use SOQL for precise, filtered data retrieval on known objects. SOSL leverages the Salesforce search index (faster for text searches, but not guaranteed real-time).
Standard SOQL:
List<Account> accts = [SELECT Id FROM Account]; — all results are loaded into heap at once. 50,000 row limit. Risk: heap overflow with large result sets. SOQL For Loop: for (Account a : [SELECT Id FROM Account]) {...} — processes records one at a time; Salesforce fetches them in batches internally. Reduces heap usage. Still subject to the 50,000 row limit. Batched SOQL For Loop: for (List<Account> batch : [SELECT Id FROM Account]) {...} — processes 200 records per iteration (the most efficient pattern for Batch Apex context). For more than 50,000 records, use Batch Apex with a QueryLocator in start() — this can handle up to 50 million rows. QueryLocator bypasses the standard 50,000 row SOQL limit.
Skinny Tables: Salesforce-maintained internal copies of an object with a subset of frequently queried fields, avoiding expensive joins. Must be requested from Salesforce support. Dramatically improves query performance for large objects. Compound Indexes: Indexes spanning multiple fields. Custom indexes on single fields can be requested; compound indexes are available for specific performance optimisation. Selective Queries: A SOQL query is selective when its WHERE clause filters via an indexed field and reduces the result set to less than: 10% of records (if total < 100,000), or 333,333 records (if total > 1 million). Non-selective queries against large objects cause full table scans and may be blocked or timeout. Indexed fields: Id, Name, OwnerId, fields marked as External ID, custom indexed fields (requested via support), and standard indexed fields (IsDeleted, CreatedDate, SystemModstamp, RecordType).
LDV patterns for orgs with millions of records: (1) Defer sharing calculations: During bulk data loads, defer sharing rule recalculation to avoid lock contention. (2) Avoid record locking: Minimise the number of records being locked simultaneously. (3) Use External IDs: For upsert operations in integrations. (4) Batch Apex with skinny tables: Avoid queries with complex JOINs. (5) Selective queries: Always filter on indexed fields. (6) Avoid lookup cascade: Large numbers of related records can slow down parent record saves. (7) Archive old data: Use BigObjects for long-term storage; query current data stays performant. (8) Avoid cross-object formula fields on large objects — they prevent indexing of related fields. (9) Parallel data loads: Split large imports into parallel Data Loader jobs.
Trigger firing order by operation: Insert: before insert → (validation rules, duplicates, save) → after insert. Update: before update → (validation rules, duplicates, save) → after update. If a workflow field update occurs: before update → after update fires again (once more). Delete: before delete → (save/remove from DB) → after delete. Undelete: after undelete (no before undelete). Multiple triggers on the same object for the same event fire in an undefined order — do not rely on order between multiple triggers. Use a single trigger per object with a dispatcher pattern. When a workflow rule updates a field, it re-fires before/after update triggers one more time (not recursively beyond that single re-fire). Process Builder fires after the trigger cycle.
Mixed-save processing occurs when a single save request affects multiple related records (e.g., saving an Account with a child Contact in one request, or a master-detail cascade). Processing order for a single save: (1) The record initiating the save is processed through the full order of execution (before triggers, validation, save to DB, after triggers). (2) Child records (if part of the same save) are processed next. (3) Cross-object updates triggered by workflow/flow fire in a subsequent pass. This matters because: a before trigger on Account fires before any Contact in the same request. After triggers see already-committed parent records. Cascade deletes on master-detail also follow an order — detail records fire delete triggers before the master's after trigger. Understanding this prevents bugs where developers assume all records in a request are in the same trigger context.
Strategy Pattern: Define a family of algorithms behind a common interface. A context class delegates to the appropriate strategy at runtime. Example: different discount calculation strategies (percentage, fixed amount, tiered) all implement a
IDiscountStrategy interface with a calculate(Decimal price) method. The caller passes the strategy it wants. In Apex, commonly used in email dispatch logic, notification strategies, or complex calculation variations. Factory Pattern: A factory class creates instances of classes without the caller needing to know the concrete type. Example: TriggerHandlerFactory.getHandler('Account') returns the AccountTriggerHandler. In enterprise patterns (fflib), the Application class acts as the factory for Selectors, Domains, and Services. Factories enable dependency injection and allow tests to swap in mock implementations.
Example structure:
@RestResource(urlMapping='/orders/*') global with sharing class OrderAPI { @HttpGet global static OrderResponse doGet() { String orderId = RestContext.request.requestURI.substringAfterLast('/'); Order__c order = [SELECT Id, Name FROM Order__c WHERE Id = :orderId WITH SECURITY_ENFORCED]; return new OrderResponse(order); } @HttpPost global static Id doPost() { String body = RestContext.request.requestBody.toString(); OrderRequest req = (OrderRequest) JSON.deserialize(body, OrderRequest.class); Order__c o = new Order__c(Name__c = req.name); insert o; RestContext.response.statusCode = 201; return o.Id; } } Best practices: use WITH SECURITY_ENFORCED in queries, return structured wrapper classes, set appropriate HTTP status codes (200, 201, 400, 404), handle exceptions and set RestContext.response.statusCode to error codes with error body.
LWC components declare where they can be placed using the
targets property in their .js-meta.xml configuration file. Common targets: lightning__RecordPage: Appears in Lightning App Builder on record detail pages. lightning__AppPage: Custom app pages. lightning__HomePage: The Home tab. lightning__FlowScreen: Can be used inside a Flow screen. lightningCommunity__Page: Experience Cloud pages. Target-specific properties (configurable in App Builder) are declared using <targetConfigs> and <targetConfig> tags. The property element within targetConfig exposes component properties as configurable attributes in App Builder without needing code changes. The objects attribute restricts a RecordPage component to specific object types. supportedFormFactors restricts to desktop or phone.
@wire with Apex: The Apex method must be annotated
@AuraEnabled(cacheable=true) for wire use. The LWC imports it: import getAccounts from '@salesforce/apex/AccountController.getAccounts'; Then: @wire(getAccounts, { searchTerm: '$searchTerm' }) wiredAccounts; The $ prefix makes parameters reactive — changing searchTerm re-calls the method. Wire adapters: Platform-provided adapters (getRecord, getPicklistValues, etc.) from lightning/uiRecordApi, lightning/uiObjectInfoApi. They handle caching, network requests, and reactive updates automatically. Imperative Apex: Call Apex as a Promise: getAccounts({ searchTerm: this.searchTerm }).then(result => { this.data = result; }).catch(error => { ... }); Use imperative when: the call is event-triggered (not on load), the Apex method has side effects (DML), or the method is not cacheable.
Key Database namespace methods: Database.insert(records, allOrNone): Partial success when allOrNone=false. Returns
List<Database.SaveResult>. Database.upsert(records, externalIdField, allOrNone): Returns List<Database.UpsertResult> with isCreated() to distinguish insert vs update. Database.delete(records, allOrNone). Database.emptyRecycleBin(records): Hard delete records without Data Loader. Database.convertLead(convertLeadRequest): Convert leads programmatically with full control over mapping. Database.merge(masterRecord, duplicates): Merge up to 3 records. Database.setSavepoint() / Database.rollback(savepoint): Manual transaction savepoints — roll back to a specific point in the transaction if subsequent operations fail. Savepoints do not work across async boundaries.
Dynamic SOQL using
Database.query(queryString) allows runtime-built queries but introduces SOQL injection risk. Safe practices: (1) Bind variables: Use ':variableName' in the string — variable values are not interpreted as SOQL. However, bind variables in dynamic SOQL reference the variable by name in scope at the query call — be careful. (2) String.escapeSingleQuotes(): Escapes user-supplied strings before inserting into the query. (3) Whitelisting: Only allow known field/object names from a validated list — never allow raw user input to define field names or object names. (4) Schema methods: Validate object/field existence using Schema.getGlobalDescribe() before including them in queries. (5) Type-safe patterns: Use strongly-typed selector classes rather than building raw dynamic queries throughout the codebase.
The
@AuraEnabled annotation exposes an Apex method to be called from LWC or Aura components. Parameters: cacheable=true: The method's results can be cached by the browser (Lightning Data Service cache). Required for using the method with @wire. Cacheable methods must be read-only (no DML or side effects — enforced by the platform). cacheable=false (default): Used for methods with DML or side effects. Cannot be used with @wire — only imperative calls. The method must be public or global static. Return types must be JSON-serialisable (primitives, sObjects, Lists, Maps with String keys, custom Apex classes with public/global fields). Exceptions thrown in @AuraEnabled methods are surfaced as AuraHandledExceptions in the component — always throw new AuraHandledException(e.getMessage()) for clean error handling.