Platform Developer I Interview Questions

25 expert-curated Platform Developer I interview questions — covering Apex, SOQL, triggers, LWC, test classes, governor limits, and security enforcement.

Platform Developer I Interview Questions Content

Apex primitive types: Integer, Long, Decimal, Double, Boolean, String, Id, Date, Datetime, Time, Blob. Collections: (1) List: Ordered, index-accessible, allows duplicates. List<Account> accts = new List<Account>(); (2) Set: Unordered, no duplicates, used for uniqueness checks and deduplication. Set<Id> ids = new Set<Id>(); (3) Map: Key-value pairs with unique keys. Map<Id, Account> accMap = new Map<Id, Account>([SELECT Id, Name FROM Account]); Collections are essential for bulkification — collect IDs in Sets, query once into Maps, and batch DML into Lists to avoid governor limits.
SOQL (Salesforce Object Query Language) queries data from the Salesforce database. Child-to-Parent (traverse up): SELECT Name, Account.Name FROM Contact — reference parent fields using the relationship name. Parent-to-Child (subquery): SELECT Name, (SELECT LastName FROM Contacts) FROM Account — use the child relationship name in parentheses. Aggregate functions: COUNT(), SUM(), AVG(), MIN(), MAX() — used with GROUP BY. SOQL injection prevention: Never concatenate user input directly into SOQL strings. Use bind variables (WHERE Name = :userInput) or String.escapeSingleQuotes() for dynamic SOQL to prevent injection attacks.
DML operations: insert (creates new records), update (modifies existing records), upsert (inserts if no match found, updates if match found), delete (moves to Recycle Bin), undelete (restores from Recycle Bin), merge (merges up to 3 records of the same type, deleting duplicates). Upsert: Uses a specified external ID field (or the record Id) to determine if a record exists. If a match is found, the record is updated; otherwise it is inserted. Syntax: upsert records ExternalId__c; Use Database.upsert() with allOrNone=false for partial success. Upsert is ideal for data integration scenarios where you cannot guarantee whether a record already exists.
Trigger context variables: Trigger.new — List of new record versions (available in insert, update, undelete). Trigger.old — List of old record versions (available in update, delete). Trigger.newMap — Map of Id to new record (available in before update, after insert, after update, after undelete). Trigger.oldMap — Map of Id to old record (available in update, delete). Trigger.isInsert / isUpdate / isDelete / isUndelete — Boolean flags for the operation type. Trigger.isBefore / isAfter — Boolean flags for timing. Trigger.size — Number of records in the current batch. In before triggers, Trigger.new records are mutable (you can set field values directly without DML). In after triggers, they are read-only.
Bulkification means writing code that handles up to 200 records in a trigger batch efficiently. Anti-patterns: SOQL inside a for loop (hits the 100 SOQL limit), DML inside a for loop (hits the 150 DML limit). Correct approach: (1) Collect all relevant IDs into a Set before the loop. (2) Query once outside the loop: Map<Id, Account> accMap = new Map<Id, Account>([SELECT Id, Name FROM Account WHERE Id IN :ids]); (3) Process records in the loop using the Map for O(1) lookups. (4) Collect updated records in a List. (5) Perform a single DML statement after the loop. This pattern works for any batch size and stays well within governor limits. Always test triggers with large data volumes (200+ records).
Key synchronous governor limits: (1) SOQL queries: 100 per transaction. (2) SOQL rows returned: 50,000. (3) DML statements: 150. (4) DML rows: 10,000. (5) Heap size: 6 MB. (6) CPU time: 10,000 ms. (7) Callouts: 100 (max 120 seconds per callout). (8) @future calls: 50 per transaction. Asynchronous limits are higher (e.g., Heap 12 MB, 200 SOQL). Limits reset per transaction, not per user or org. Use Limits.getQueries() and similar Limits class methods to programmatically check usage. Design patterns to avoid limits: bulkification, SOQL for loops for large data sets, asynchronous processing (Batch, Queueable).
Requirements: (1) Class annotated with @isTest. (2) Methods annotated with @isTest (or testMethod keyword). (3) Minimum 75% code coverage across all Apex before production deployment. Test data must be created in the test (not relying on org data) unless @isTest(seeAllData=true) is used (avoid this). Key patterns: @testSetup method runs once and creates data shared across all test methods in the class. Test.startTest() and Test.stopTest() create a new set of governor limits and force async operations to complete. Assertions: System.assertEquals(expected, actual, message), System.assertNotEquals(), System.assert(condition, message). Test for bulk (200 records), single record, and negative cases.
@future methods run asynchronously in a separate thread, useful for: (1) Callouts from triggers: Triggers cannot make synchronous callouts. Annotate with @future(callout=true). (2) Mixed DML workaround: Mixing DML on setup and non-setup objects in one transaction causes a Mixed DML error. Moving one DML operation to a @future method resolves this. Limitations: (1) Only primitive parameters (no sObjects or custom types — pass IDs and re-query inside). (2) Cannot be called from Batch Apex. (3) Maximum 50 @future calls per transaction. (4) Cannot chain @future methods (call another @future from within @future). For complex async scenarios, use Queueable Apex instead.
Batch Apex (implementing Database.Batchable<sObject>) processes large data sets by breaking them into chunks. Three required methods: (1) start(): Returns a QueryLocator (SOQL query up to 50M rows) or an Iterable. Called once at the beginning. (2) execute(): Processes one batch chunk (default 200 records, configurable 1–2000). Called once per chunk. Each execute() runs in its own transaction with fresh governor limits. (3) finish(): Called once after all batches complete. Used for post-processing (email notifications, chaining another job). Database.Stateful: Implement this interface to maintain instance variable state across execute() chunks (e.g., running totals). Without it, variables are reset between chunks. Execution: Database.executeBatch(new MyBatch(), 200);
Visualforce is Salesforce's tag-based markup language for building custom UIs. Key components: apex:page (root element, defines controller), apex:form (wraps interactive elements), apex:commandButton (triggers controller action), apex:inputField (renders a field with FLS enforcement), apex:outputField, apex:pageBlock, apex:repeat (iteration), apex:messages (display errors). View state: Stores the page's data between requests (max 170 KB) — keep it small by marking transient fields. Visualforce is still used for: PDF generation, legacy integrations, embedding in flows/layouts, and cases where full LWC is not yet feasible. New UI development should use LWC.
@api: Marks a property or method as public — accessible by parent components or via Lightning App Builder. Public properties are reactive (parent changes propagate to child). @wire: Connects a component property or function to a wire adapter (e.g., a Salesforce data service or Apex method). Data is fetched reactively. Example: @wire(getRecord, { recordId: '$recordId', fields }) wiredRecord; The $ prefix makes the parameter reactive. @track: Originally required for reactive nested object/array properties. In API version 39+, all component properties are reactive by default — @track is still valid but rarely needed. Primitive properties and object references are reactive; only internal mutations to objects/arrays need @track or explicit reassignment to trigger re-render.
Lightning Data Service (LDS) is a centralised data layer in LWC that handles record CRUD operations while enforcing FLS/CRUD security, caching data on the client, and refreshing multiple components when a record changes. Key wire adapters: getRecord: Reads record data. getRecordCreateDefaults: Gets default values for record creation. updateRecord: Updates a record (imported from lightning/uiRecordApi). createRecord / deleteRecord: Create or delete records imperatively. Example: import { getRecord, updateRecord } from 'lightning/uiRecordApi'; LDS caches data browser-side and notifies all components using the same record when it is updated — providing automatic UI consistency without manual refresh. Always prefer LDS over direct Apex calls for standard CRUD operations.
SLDS is available automatically in LWC without any import — just use the CSS class names directly in your HTML template. Key SLDS concepts: (1) Utility Classes: slds-m-around_medium, slds-p-horizontal_large for spacing. (2) Grid System: slds-grid, slds-col for layouts. (3) Forms: slds-form-element, slds-form-element__label. (4) Components: Use base Lightning components (lightning-input, lightning-button, lightning-card) which are pre-styled with SLDS. (5) Icons: lightning-icon with SLDS icon names. Best practice: use base Lightning components wherever possible — they handle SLDS, accessibility, and mobile responsiveness automatically. Custom styling should use CSS custom properties (design tokens) rather than overriding SLDS classes.
LWC lifecycle hooks: constructor(): Called when the component is created. Do not access the DOM or child components here. Call super() first. connectedCallback(): Called when the component is inserted into the DOM. Safe to access the component's own DOM (not children yet). Good for data fetching, event subscription, and initialisation. renderedCallback(): Called after every render. Use for post-render DOM manipulation. Be careful of infinite loops (avoid state changes that trigger re-renders here). disconnectedCallback(): Called when the component is removed from the DOM. Use for cleanup (unsubscribing from message channels, clearing timers). errorCallback(error, stack): Error boundary — catches errors from child components. Only available on components acting as error boundaries.
Debugging methods: (1) System.debug(): Writes output to debug logs. Use LoggingLevel enum (e.g., System.debug(LoggingLevel.ERROR, 'message');). (2) Debug Logs: Set up via Setup > Debug Logs. Capture logs for a specific user with configurable log levels (Apex, Database, System, etc.). View in Developer Console or VS Code. (3) Developer Console: Built-in IDE with query editor, log viewer, and code editor. (4) Anonymous Apex: Execute code snippets instantly via Developer Console or VS Code for quick testing. (5) Apex Replay Debugger (VS Code extension): Replay a debug log as a step-through debugger, setting breakpoints and inspecting variable values. (6) Checkpoints: Add heap dump checkpoints to capture full variable state at a specific line during execution.
By default, Apex runs in system context — it bypasses CRUD (object-level) and FLS (field-level) security. To enforce security: CRUD checks: Use Schema methods — Schema.sObjectType.Account.isCreateable(), isReadable(), isUpdateable(), isDeletable() before DML/queries. FLS checks: Schema.sObjectType.Account.fields.Name.isAccessible(), isUpdateable(). Simplified approach: Use Security.stripInaccessible() to automatically strip fields the user cannot access from query results or DML inputs — cleaner than manual field-by-field checks. with sharing: Enforces record-level sharing rules (OWD/role hierarchy) but NOT FLS/CRUD. Always pair sharing enforcement with explicit FLS checks for full security compliance.
with sharing: The class enforces the running user's record-level sharing (OWD, role hierarchy, sharing rules). SOQL queries and DML operations only affect records the user has access to. without sharing: The class ignores sharing — all records of the object are accessible regardless of the user's sharing access. Use for system-level operations where full data access is explicitly required. inherited sharing: The class inherits the sharing mode of its caller. If called from a with sharing class, it runs with sharing; if called from a without sharing context, it runs without sharing. This is the safest default for utility/service classes that should respect the caller's context. If no keyword is specified, the class defaults to inherited sharing (in most contexts) — but explicitly declaring is a best practice.
Custom Labels are reusable text values defined in Setup that can be used across Apex, Visualforce, and LWC. They support translation — you can define different values per language, enabling multi-language applications. In Apex: System.Label.MyLabel. In Visualforce: {!$Label.MyLabel}. In LWC: Import with import LABEL_NAME from '@salesforce/label/c.MyLabel'; and reference as a property. Use custom labels for: UI text strings, error messages, success messages, and any text that may need to be translated or updated without a code deployment. Labels can also be used in Flows, formula fields, and validation rules. Maximum 5,000 custom labels per org.
Custom Settings: Store configuration data in an object with a hierarchy (org, profile, user level). Two types: Hierarchy (the most common — user/profile values override org-level) and List (no hierarchy). Accessible via MySettings__c.getInstance(). Data is not deployable as metadata — records are org-specific. Custom Metadata Types (CMT): Configuration data stored as metadata records. Deployable via change sets, packages, and CLI. Records can be packaged and version-controlled. Accessible via SOQL: SELECT Value__c FROM MyConfig__mdt WHERE DeveloperName = 'Setting1'. CMTs are preferred for configuration that needs to travel between sandboxes and production as part of deployment. Custom Settings are preferred for user/profile-specific runtime overrides. CMTs cannot be created/updated via DML in Apex triggers (only in tests or Setup).
The order of execution on record save: (1) Load original record values from database. (2) Override with new values from the save request. (3) Execute system validations (data type, required fields). (4) Execute all before triggers. (5) Run custom validation rules. (6) Check duplicate rules. (7) Save record to database (not yet committed). (8) Execute all after triggers. (9) Execute assignment rules. (10) Execute auto-response rules. (11) Execute workflow rules (field updates trigger re-evaluation of before/after triggers). (12) Execute processes (Process Builder). (13) Execute escalation rules. (14) Roll-up summary recalculation (if applicable). (15) Recalculate criteria-based sharing. (16) Commit to database. (17) Execute post-commit logic (send email, async enqueue). Understanding this order is critical for debugging unexpected behaviour in multi-automation orgs.
Apex callouts send HTTP requests to external systems. Steps: (1) Add the endpoint to Remote Site Settings or use a Named Credential (recommended for authentication). (2) Use HttpRequest, HttpResponse, and Http classes. (3) Callouts cannot be made after DML in the same transaction — use @future(callout=true) or Queueable/Batch to separate them. (4) In test classes, use HttpCalloutMock interface and Test.setMock() to mock HTTP responses. (5) Maximum callout timeout is 120 seconds; 100 callouts per transaction. Named Credentials handle authentication (Basic, OAuth, JWT) declaratively, preventing credentials from being hardcoded in Apex. Always handle HTTP status codes and exceptions (callout timeout, network errors).
Queueable Apex (implementing Queueable interface) runs asynchronously. Advantages over @future: (1) Accepts non-primitive parameters (sObjects, custom types, collections). (2) Returns a job ID for monitoring via AsyncApexJob. (3) Can be chained — enqueue another Queueable within execute() (max chain depth of 5 in dev/sandbox, unlimited in production, but limited to avoid infinite chains). (4) Can make callouts if Database.AllowsCallouts is implemented. (5) Can be called from Batch Apex. Usage: System.enqueueJob(new MyQueueable()); Testing: wrap in Test.startTest() / Test.stopTest() to force synchronous execution. Ideal for jobs that need object parameters or chaining patterns.
Scheduled Apex runs at a specified time by implementing the Schedulable interface and the execute(SchedulableContext ctx) method. Scheduling options: (1) UI: Setup > Apex Classes > Schedule Apex — define start/end dates and frequency. (2) Programmatic: System.schedule('JobName', cronExpression, new MySchedulable()); Cron format: '0 0 8 * * ?' (8 AM daily). Limits: 100 scheduled jobs per org simultaneously. Scheduled Apex itself has a limited context — it is common to call Database.executeBatch() from within the Scheduled Apex execute method to do the actual data processing. Monitor scheduled jobs via Setup > Scheduled Jobs or SOQL on CronTrigger and CronJobDetail.
DML statements (insert records;): If any record in the batch fails, the entire operation is rolled back and an exception is thrown. All or nothing. Database methods (Database.insert(records, false);): The second parameter allOrNone — when set to false, allows partial success. Records that pass validation are committed; failed records are captured in Database.SaveResult. Returns a list of SaveResult objects, each with isSuccess(), getErrors(), and the record Id. Use partial DML when processing a batch where some records may legitimately fail and you want to continue processing the rest. Use allOrNone DML when data integrity requires all records to succeed together (transactional requirement).
Parent to Child: Use @api properties on the child component. The parent sets them as HTML attributes: <c-child record-id={recordId}></c-child>. The child declares: @api recordId;. You can also call child @api methods from the parent using template refs. Child to Parent: The child dispatches a custom event: this.dispatchEvent(new CustomEvent('myevent', { detail: payload })); The parent listens: <c-child onmyevent={handleEvent}></c-child>. For unrelated components (siblings or different trees), use the Lightning Message Service (LMS) — publish/subscribe via a Message Channel. This is the recommended alternative to Aura custom events between unrelated components.