Salesforce Platform Developer Practice Questions: 25 Free PD1 Practice Questions (2026)

Hey friends,

If you're studying for the Salesforce Platform Developer exam (still called PD1 by almost everyone), you've probably noticed that most free "practice tests" are either way too easy or a little too familiar. So I wrote my own.

Here are 25 original, scenario-based PD1 practice questions, weighted like the real exam, with an explanation for every answer option. I wrote each one from the official exam outline and checked every answer against the current Salesforce docs (Apex Developer Guide, LWC Developer Guide, governor limits and the CLI reference).

These aren't exam dumps, and they aren't trying to look like the real questions word for word. They're built to practice the reasoning the exam rewards, which is what still works when Salesforce words a question differently than you expected.

How to use these questions

  • Answer all 25 before you open any answers. Each answer is hidden under a Show the answer and explanation toggle right below its question.
  • Read the explanation even when you got it right. Knowing why the other three options are wrong is most of the skill.
  • Count your correct answers and score yourself with the calculator link at the end.

The questions follow the current official weights from the Platform Developer exam guide: Developer Fundamentals 27% (7 questions), Process Automation and Logic 28% (7), User Interface 25% (6), and Testing, Debugging, and Deployment 20% (5). If a topic trips you up, the matching section of my Platform Developer (PD1) study guide goes deeper.

Developer Fundamentals (27%): 7 questions

Question 1. A property management company tracks Inspection__c records for each Property__c. An inspection can't exist without a property, deleting a property should delete its inspections, anyone who can see a property should see its inspections, and each property needs a count of failed inspections. Which relationship should Inspection__c have to Property__c?

  • A. A lookup relationship
  • B. A hierarchical relationship
  • C. An external lookup relationship
  • D. A master-detail relationship
Show the answer and explanation

Answer: D. Master-detail checks every box. The parent field is required, deleting the master deletes its details, the detail inherits sharing from the master (it has no owner of its own), and the master can have a roll-up summary field such as a COUNT of inspections filtered to Result = Failed.

A lookup is optional by default, gives the child its own owner and sharing, and doesn't support roll-up summary fields on custom objects. A hierarchical relationship is only available on the User object. An external lookup links to a parent that is an external object, which Property__c isn't.

Question 2. A training company has Student__c and Course__c objects. A student can take many courses, a course has many students, and each enrollment needs its own Grade__c. Reports must show courses per student and students per course. What should the developer build?

  • A. A multi-select picklist of course names on Student__c
  • B. A lookup from Student__c to Course__c
  • C. A junction object, Enrollment__c, with one master-detail relationship to Student__c and another to Course__c
  • D. A lookup from Student__c to Course__c plus a lookup from Course__c to Student__c
Show the answer and explanation

Answer: C. A many-to-many relationship needs a junction object. Each Enrollment__c record links one student to one course and is the natural home for Grade__c. With two master-detail relationships, you can report from either side and add roll-up summaries to both parents. The first master-detail you create becomes the primary relationship.

A multi-select picklist stores text, so there's nowhere to keep a grade per enrollment and reporting is weak. A single lookup lets a student point to only one course. Two lookups in opposite directions still give each record one value, which is not many-to-many.

Question 3. Sales reps want every Contact to display its Account's Annual Revenue. The value must always match the Account, with no extra data storage and nothing to maintain. What should the developer use?

  • A. A roll-up summary field on Contact
  • B. A before-save record-triggered flow on Contact that copies the value
  • C. A cross-object formula field on Contact that references Account.AnnualRevenue
  • D. An Apex trigger on Contact that copies the value
Show the answer and explanation

Answer: C. A cross-object formula is calculated when the record is read, so it always shows the Account's current value and stores nothing. It's the classic declarative-first answer.

Roll-up summary fields live on the master record and aggregate its details, so they work in the opposite direction. The flow and the trigger copy the value only when the Contact is saved, so it goes stale as soon as someone edits the Account (unless you build more automation on Account), and both store a duplicate value.

Question 4. A new developer asks how the Model-View-Controller (MVC) pattern maps to the Salesforce Platform. Which mapping is correct?

  • A. Model: Visualforce pages and LWC templates. View: Apex classes. Controller: standard and custom objects.
  • B. Model: Apex triggers. View: page layouts only. Controller: flows.
  • C. Model: the LWC JavaScript class. View: custom objects. Controller: Visualforce pages.
  • D. Model: standard and custom objects and their fields. View: Visualforce pages and LWC HTML templates. Controller: standard controllers, Apex controllers and the LWC JavaScript class.
Show the answer and explanation

Answer: D. The model is the data and its structure (objects, fields and relationships). The view is what the user sees (Visualforce markup or an LWC template). The controller holds the logic that connects them, such as a standard controller, an Apex controller or extension, or the JavaScript class behind an LWC.

The other options swap the layers. Triggers and flows are logic rather than the data model, and page layouts are only one piece of the view.

Question 5. An integration inserts Order__c records. Each order arrives with the customer's ERP number, which is stored on Account in ERP_Number__c, an External ID field. Order__c has a lookup field, Account__c (relationship name Account__r). How can the developer link each order to its Account without querying for Account Ids first?

  • A. Set order.Account__c = 'ERP-1001'; and insert the order
  • B. Set order.Account__r = new Account(ERP_Number__c = 'ERP-1001'); and insert the order
  • C. Rename the Account Name field to hold the ERP number
  • D. Add a formula field on Order__c that matches the ERP number to an Account
Show the answer and explanation

Answer: B. Apex can resolve a relationship through an external ID. You assign the relationship field an in-memory sObject that has only the external ID set, and the platform finds the matching parent during the insert. It saves a SOQL query and works in bulk.

Option A fails because a lookup field expects a record Id, not an ERP number. Renaming the Name field doesn't create a relationship. A formula field is read-only and can't set a lookup.

Question 6. Invoice__c has a lookup to Account. Some invoices intentionally have no Account, and the business wants to keep it that way. The Account needs an Open Invoices count that stays current as invoices are created, edited and deleted. What should the developer do?

  • A. Create a roll-up summary field on Account that counts open invoices
  • B. Convert the lookup to master-detail, then create a roll-up summary field
  • C. Create a formula field on Account that counts related invoices
  • D. Build a record-triggered flow or Apex trigger on Invoice__c that recalculates the count on the related Account
Show the answer and explanation

Answer: D. Roll-up summary fields need a master-detail relationship, and this one has to stay a lookup, so the count must be maintained by automation. A record-triggered flow can run on create, update and delete. An Apex trigger can also cover undelete. Either way, recalculate for the old and new Account when an invoice moves between Accounts.

A roll-up summary isn't available on this custom lookup. Converting to master-detail fails while any invoice has a blank Account, and it would make the field required, which the business doesn't want. Formula fields can't aggregate child records.

Question 7. Your team lead asks what the exam means by "Agentforce for Developers", the AI coding assistant that Salesforce now ships as the Agentforce Vibes extension. Which two statements are accurate? (Choose two.)

  • A. It can generate Apex and Lightning Web Component code from natural-language prompts in VS Code or in the browser-based IDE that used to be called Code Builder.
  • B. Code it generates is exempt from the 75% code coverage requirement for production deployments.
  • C. Its output can be wrong or miss your org's context, so generated code gets the same review, testing and deployment process as code you write yourself.
  • D. Governor limits don't apply to code it generates.
Show the answer and explanation

Answer: A and C. The assistant works inside VS Code (through the Salesforce Extension Pack) and the browser-based Agentforce Vibes IDE, formerly Code Builder. It can generate code from a prompt, suggest completions and draft Apex tests. Like any AI tool, it can be confidently wrong, so you still review it, test it and deploy it normally.

The platform doesn't know or care who wrote the code. Coverage requirements and governor limits apply exactly the same to AI-generated code.

Process Automation and Logic (28%): 7 questions

Question 8. On Opportunity, a before-save record-triggered flow sets Discount__c to 10. A before update Apex trigger on the same object sets Discount__c to 15. A user edits an Opportunity and both automations run. What value is saved?

  • A. 10, because flows always run after Apex
  • B. It's unpredictable, because Salesforce doesn't define the order
  • C. 15, because before-save flows run before before triggers, so the trigger's value overwrites the flow's
  • D. Neither, because the save fails when two automations update the same field
Show the answer and explanation

Answer: C. In the save order of execution, record-triggered flows configured to run before the record is saved come first, and then all before triggers run. The trigger sees the flow's 10 and replaces it with 15. Custom validation rules run after both, so they see 15.

The order between before-save flows and before triggers is documented, so it isn't random. (What isn't guaranteed is the order of multiple triggers for the same event on one object.) Two automations updating the same field isn't an error. The last one to run wins.

Question 9. An after update trigger on Account updates the Account's child Contacts. A Contact trigger then updates the parent Account's Last_Contact_Change__c field, which fires the Account trigger again. Users now see "maximum trigger depth exceeded" errors. What's the best fix?

  • A. Declare a Boolean flag inside the trigger body and check it before running the logic
  • B. Store a "processing" checkbox on each Account and clear it with another update
  • C. Nothing, because Apex stops recursion automatically after the first run
  • D. Use a static variable in a helper class, such as a Set of processed Account Ids, and skip records already handled in this transaction
Show the answer and explanation

Answer: D. Static variables in an Apex class keep their value for the whole transaction, across every trigger invocation, which makes them the standard recursion guard. A Set of processed Ids is better than a single Boolean because it still lets new records in the same transaction get processed.

A variable declared in the trigger body is re-initialized each time the trigger runs, so it can't remember anything. A checkbox field adds extra DML and data that outlives the transaction. Apex doesn't stop recursion for you. It throws an error once the trigger stack depth passes 16.

Question 10. Line_Item__c has a master-detail relationship to Invoice__c, and the child relationship name is Line_Items. Which SOQL query returns each invoice with its line items in a single statement?

  • A. SELECT Name, (SELECT Amount__c FROM Line_Items__r) FROM Invoice__c
  • B. SELECT Name, (SELECT Amount__c FROM Line_Item__c) FROM Invoice__c
  • C. SELECT Name, Line_Items__r.Amount__c FROM Invoice__c
  • D. SELECT Name FROM Invoice__c JOIN Line_Item__c ON Invoice__c.Id = Line_Item__c.Invoice__c
Show the answer and explanation

Answer: A. A parent-to-child query uses a subquery on the child relationship name, and for custom relationships that name ends in __r. The result gives you each invoice with a list of its line items.

Option B uses the object name instead of the relationship name. Dot notation (option C) only goes from child to parent, for example Invoice__r.Name from Line_Item__c. SOQL has no JOIN keyword.

Question 11. A support component must find the text "Acme" in the names of Accounts, Contacts and Leads, all in one statement. Which Apex code does that?

  • A. List<List<SObject>> results = [FIND 'Acme' IN NAME FIELDS RETURNING Account(Name), Contact(Name), Lead(Name)];
  • B. List<SObject> results = [SELECT Name FROM Account, Contact, Lead WHERE Name LIKE '%Acme%'];
  • C. List<SObject> results = [FIND 'Acme' IN NAME FIELDS RETURNING Account(Name), Contact(Name), Lead(Name)];
  • D. Three separate SOQL queries with LIKE '%Acme%'
Show the answer and explanation

Answer: A. SOSL is built for searching text across several objects at once. An inline SOSL query returns a list of lists, with one inner list per object in the order of the RETURNING clause, so results[0] holds the Accounts.

SOQL queries one object per FROM clause, so option B doesn't compile. Option C has the wrong return type for SOSL. Three SOQL queries can work, but that isn't one statement, and leading-wildcard LIKE searches are slow on large objects.

Question 12. A developer wants a custom exception to throw when an order fails validation. Which class declaration is valid?

  • A. public class OrderValidationError extends Exception {}
  • B. public class OrderValidationException implements Exception {}
  • C. public class OrderValidationException extends Exception {}
  • D. public interface OrderValidationException extends Exception {}
Show the answer and explanation

Answer: C. A custom exception must extend the built-in Exception class, and its name must end with the word Exception. With just that one line, you inherit the standard constructors, so throw new OrderValidationException('Missing SKU'); works with no extra code.

OrderValidationError won't compile because the name doesn't end in Exception. Exception is a class, not an interface, so you extend it rather than implement it. An interface can't extend a class.

Question 13. An after insert trigger on Case contains this loop. What happens when Data Loader inserts 200 Cases in one batch?

for (Case c : Trigger.new) {
    insert new Task(WhatId = c.Id, Subject = 'Follow up');
}

  • A. All 200 Tasks are created successfully
  • B. The transaction fails with System.LimitException: Too many DML statements: 151, and nothing in it is saved
  • C. The platform automatically combines the inserts into one DML statement
  • D. The first 150 Tasks are saved and the rest are skipped
Show the answer and explanation

Answer: B. Each pass through the loop issues its own DML statement, and a transaction can issue only 150. The 151st insert throws a LimitException, which can't be caught, so the whole transaction rolls back. The fix is to add the Tasks to a List<Task> inside the loop and insert the list once after it. One DML statement can insert up to 10,000 records.

Apex doesn't bulkify your code for you, and a limit failure doesn't partially commit. That's why bulk patterns matter.

Question 14. Every night, a field must be recalculated on about 8 million Asset records. Which approach fits?

  • A. A Batch Apex class whose start method returns a Database.QueryLocator, scheduled with a Schedulable class
  • B. An @future method that queries and updates all the records
  • C. One Queueable job that processes every record in a single execute method
  • D. A schedule-triggered flow that runs on all Assets
Show the answer and explanation

Answer: A. Batch Apex is designed for this. A QueryLocator can return up to 50 million records, the platform splits them into chunks (200 by default), and each execute call gets a fresh set of governor limits. A Schedulable class kicks it off every night.

A single @future or Queueable transaction still has per-transaction limits, such as 50,000 rows retrieved by SOQL and 10,000 rows processed by DML, so neither can touch 8 million records in one run. Schedule-triggered flows have an org-wide cap of 250,000 interviews per 24 hours (or 200 times your user licenses, if that's higher), and 8 million records a night is far past that.

User Interface (25%): 6 questions

Question 15. A parent Lightning Web Component needs to pass a record Id to a child component, c-order-summary. Which combination works?

  • A. Child: @api recordId; Parent: <c-order-summary record-id={recordId}></c-order-summary>
  • B. Child: @api recordId; Parent: <c-order-summary recordId={recordId}></c-order-summary>
  • C. Child: @track recordId; Parent: <c-order-summary record-id={recordId}></c-order-summary>
  • D. Child: recordId; with no decorator. Parent: <c-order-summary record-id={recordId}></c-order-summary>
Show the answer and explanation

Answer: A. A public property needs the @api decorator. In the parent's HTML, the camelCase JavaScript name is written in kebab-case, so recordId becomes record-id.

Option B uses camelCase in the template, which isn't how LWC maps attributes to properties. @track makes a field reactive inside the component but doesn't make it public. A field with no decorator is private, so the parent can't set it.

Question 16. A Visualforce page on an Experience Cloud site shows comments typed by site users with <apex:outputText value="{!customerComment}" escape="false"/>. What's the risk, and what's the fix?

  • A. SOQL injection. Use bind variables in the controller.
  • B. No risk, because Visualforce sanitizes all output automatically
  • C. Cross-site scripting (XSS). Remove escape="false" so the value is HTML-encoded, which is the default.
  • D. Cross-site request forgery. Add a token to the page.
Show the answer and explanation

Answer: C. With escaping turned off, a user can submit a comment that contains a script, and it runs in other users' browsers. Leaving escape at its default (true) HTML-encodes the value. In other contexts, the HTMLENCODE and JSENCODE functions do the same job.

SOQL injection is about building queries from user input, which isn't happening here. Visualforce escapes output by default, but escape="false" turns that protection off. CSRF is a different attack, and Visualforce already protects standard form posts against it.

Question 17. This class backs an LWC that lists Contacts. Some users don't have field-level access to Salary__c, but the component still shows it to them. What should the developer change?

public with sharing class ContactController {
    @AuraEnabled(cacheable=true)
    public static List<Contact> getContacts(Id accountId) {
        return [SELECT Name, Phone, Salary__c FROM Contact WHERE AccountId = :accountId];
    }
}

  • A. Nothing. with sharing already enforces field-level security.
  • B. Change the class to without sharing
  • C. Hide the Salary column with CSS in the component
  • D. Run the query in user mode with WITH USER_MODE, or remove inaccessible fields with Security.stripInaccessible
Show the answer and explanation

Answer: D. with sharing controls which records a user sees, not which fields. Apex runs in system mode by default, so object and field permissions aren't checked unless you ask. WITH USER_MODE enforces the running user's object permissions, field-level security and sharing, and it throws an exception if the query includes a field the user can't access. Security.stripInaccessible removes those fields instead, so the rest of the data still comes back.

without sharing removes record-level protection, so it makes things worse. Hiding a column with CSS still sends the data to the browser, where anyone can read it.

Question 18. A "Close selected cases" button in an LWC calls an Apex method imperatively. The method is declared @AuraEnabled(cacheable=true) public static void closeCases(List<Id> caseIds), and it updates the Cases. The button throws an error. What's wrong?

  • A. Imperative calls require cacheable=true, so something else is the problem
  • B. A cacheable method can only read data. Remove cacheable=true, keep @AuraEnabled, and refresh any cached data after the update.
  • C. The method must be called with @wire instead
  • D. The method must be an instance method, not static
Show the answer and explanation

Answer: B. Marking a method cacheable tells the framework it only gets data and never changes it, so DML inside it fails. Methods that change data should be plain @AuraEnabled and called imperatively. Afterward, call refreshApex() for wired data, or notifyRecordUpdateAvailable() so Lightning Data Service refreshes those records.

cacheable=true is optional for imperative calls and required for @wire, which makes option C worse: wired methods must be cacheable, and they run when the component loads, not when someone clicks. Apex methods called from LWC must be static.

Question 19. An admin wants to drop your address-picker LWC onto a Flow screen and store the address the user picks in a flow variable. What does the component need?

  • A. An Aura wrapper component, because LWCs can't run in flows
  • B. The lightning__FlowScreen target in its js-meta.xml file, isExposed set to true, and an @api property declared in a targetConfig so the flow can map it
  • C. The lightning__RecordPage target only
  • D. An Apex class that starts the flow with Flow.Interview
Show the answer and explanation

Answer: B. The FlowScreen target makes the component available in Flow Builder. Each @api property listed in the targetConfig shows up as an input or output the admin can map to flow variables (use role="outputOnly" for output-only values). When the value changes, fire a FlowAttributeChangeEvent so the flow knows about it.

LWCs run in flows directly, so no Aura wrapper is needed. A record page target doesn't make a component available in Flow Builder. Starting a flow from Apex doesn't put a component on a screen.

Question 20. On the Account record page, users need a small component to edit five specific fields. It has no custom logic, and it must respect field-level security and sharing. What's the best choice?

  • A. lightning-record-edit-form with a lightning-input-field for each field
  • B. A custom Apex controller with an @AuraEnabled update method
  • C. A Visualforce page embedded in the Lightning page
  • D. A Queueable Apex class that saves the fields
Show the answer and explanation

Answer: A. The record form components use Lightning Data Service, so they need no Apex, respect the user's field-level security and sharing, show field labels and help text, and share a cache with the rest of the page. lightning-record-form also works if you don't need control over the layout.

Custom Apex means more code to write, test and secure (you'd have to enforce field-level security yourself). Visualforce is a heavier, older way to build this. Queueable Apex runs in the background and isn't a user interface.

Testing, Debugging, and Deployment (20%): 5 questions

Question 21. A test class has five test methods that all need the same 20 Accounts, created in a @testSetup method. One test method deletes ten of them. What do the other test methods see?

  • A. Only 10 Accounts if they run after the test that deleted them
  • B. 100 Accounts, because the setup method runs once before each test method and the records pile up
  • C. The original 20 Accounts, because changes made by one test method are rolled back before the next one runs
  • D. The Accounts stay in the org's database after the test run finishes
Show the answer and explanation

Answer: C. Records created in @testSetup are available to every test method in the class. If a test method changes or deletes them, those changes are rolled back when the method finishes, so the next method starts from the original data.

Test methods are isolated from each other, so deletions don't carry over. Rollback restores the original setup records for each method, so nothing piles up. Test data is never committed to the org.

Question 22. A with sharing class should return only the Opportunities the current user can see. How should the test prove it?

  • A. Use @isTest(SeeAllData=true) and compare the results with real org data
  • B. Call Test.setCurrentPage() before calling the method
  • C. Create a test user with a limited profile, create Opportunities owned by someone else, then call the method inside System.runAs(testUser) and assert on what comes back
  • D. Log in as a sales rep and run the test from their account
Show the answer and explanation

Answer: C. System.runAs switches the user context inside a test, so the class's sharing rules are enforced for that user. Creating your own user and records keeps the test repeatable in any org.

SeeAllData ties the test to whatever data happens to exist, so it's brittle and proves little. Test.setCurrentPage sets a Visualforce page context and has nothing to do with sharing. Logging in as someone else isn't part of the test, so it can't run in a deployment or a CI pipeline.

Question 23. A user in production gets an error when she saves a Case, but you can't reproduce it as an admin. What's the best way to see what happens in her transaction?

  • A. Add a trace flag for her user in Setup under Debug Logs, have her reproduce the error, then read the debug log
  • B. Ask her to open her browser's developer console to see the System.debug output
  • C. Check the Apex Jobs page in Setup
  • D. Open the Developer Console as yourself and wait for the error to show up
Show the answer and explanation

Answer: A. A user trace flag with a debug level captures debug logs for her transactions, including the error, the automation that ran, and limit usage. You can also download the log and step through it in VS Code with the Apex Replay Debugger.

System.debug writes to debug logs, not to the browser console. The Apex Jobs page shows only asynchronous jobs. The Developer Console captures logs for your own user, not hers.

Question 24. You deploy two classes, PricingService and InvoiceService, to production with the command below. Org-wide coverage is 88%. PricingServiceTest covers PricingService at 92% and InvoiceService at 40%. What happens?

sf project deploy start --test-level RunSpecifiedTests --tests PricingServiceTest

  • A. The deployment succeeds because org-wide coverage is above 75%
  • B. The deployment fails because, with RunSpecifiedTests, each class and trigger in the deployment needs at least 75% coverage from the tests you specified
  • C. The deployment succeeds because the average coverage of the two classes is 66%, which rounds up
  • D. The deployment succeeds because RunSpecifiedTests skips coverage checks
Show the answer and explanation

Answer: B. When you choose which tests to run, coverage is checked for each class and trigger in the deployment on its own, using only the tests you specified. InvoiceService is at 40%, so the deployment fails. Add its test class to the --tests list, or improve the tests, and deploy again.

Org-wide coverage is what counts under the default test level (RunLocalTests), not here. The average idea is wrong twice: 66% is below 75%, and nothing gets averaged. RunSpecifiedTests changes how coverage is checked. It doesn't skip the check.

Question 25. Your team is moving to source-driven development and wants a fresh, disposable org for each feature, created from a configuration file in the repository and thrown away when the work is merged. Which kind of org fits?

  • A. A Developer Pro sandbox, refreshed for each feature
  • B. A Full sandbox
  • C. A separate Developer Edition org for each developer
  • D. A scratch org created from a Dev Hub with a scratch org definition file
Show the answer and explanation

Answer: D. Scratch orgs are built for this. You create them from a Dev Hub using a definition file kept in source control (sf org create scratch), push your source in, and let them expire. They last from 1 to 30 days, and the default is 7.

Sandboxes are copies of production with refresh intervals (one day for Developer and Developer Pro, 29 days for Full), so they aren't defined by a file in your repo. Full sandboxes are meant for staging and performance testing. Developer Edition orgs are long-lived and aren't created from your project configuration.

Score yourself

Count your correct answers. For the two-answer question, count it only if you got both. Then enter your count in my Salesforce exam score calculator. That link opens it set to Platform Developer in questions-correct mode with the total already set to 25, so you only type your number.

The real exam's passing score is 68%, which is about 17 of these 25. Treat that as a rough signal, not a prediction. 25 questions is a small sample, and the real exam has 60 scored questions. If you miss more than one or two in a section, that's the section to study next.

Where to go next

Docs I checked these answers against:

If you spot something that's changed in a recent release, let me know and I'll update it.

Hope this helps, and good luck on the exam!

Best,

Nick