Choosing Between Lightning Data Service and Apex Controllers for CRUD in Lightning Components
Compare Lightning Data Service and Apex controllers for CRUD in Lightning components. Trade‑offs, decision criteria, and a sample implementation to validate the choice.
07 Oct 2025, 18:07 UTC

Problem & Decision
When building a Lightning component that performs Create, Read, Update, or Delete (CRUD) on Salesforce records, you can either rely on Lightning Data Service (LDS) or write an Apex controller with explicit DML and SOQL. The decision hinges on the complexity of the business logic, governor‑limit exposure, and maintenance effort.
Decision Criteria
- CRUD Simplicity – Does the component only need basic field reads/writes?
- Business Logic – Are there cross‑object validations, triggers, or custom actions that must run?
- Security & Governance – Does the solution need fine‑grained field‑level security checks or bulk‑safe code?
- Performance – Will the component load or update many records in a single transaction?
- Environment Support – Must the component run in Lightning Experience, Flow, or LWC only?
Supported Options
| Feature | Lightning Data Service (LDS) | Apex Controller |
|---|---|---|
| CRUD Operations | Automatic – @wire or lightning-record-form |
Explicit DML & SOQL |
| Field‑Level Security (FLS) | Automatically enforced | Must be checked manually or via @ReadOnly/@WriteOnly |
| Bulk Safety | Built‑in bulk‑safe handling | Requires explicit bulk‑safe coding (e.g., Database.update(..., false)) |
| Custom Actions & Cross‑Object Logic | Not supported | Fully supported |
| Performance (Batch Loading) | Can load multiple records in one component | Multiple round‑trips unless optimized |
| Environment | Lightning Experience, Flow, LWC | Platform‑wide (Aura, LWC, Visualforce) |
Trade‑Offs Explained
Maintainability vs. Control
LDS removes boilerplate code, reduces the risk of governor‑limit violations, and automatically caches records. This makes it ideal for simple forms or dashboards where the only requirement is to display and edit a few fields.
Apex controllers give you full control over the transaction. You can apply complex validation, orchestrate multiple DML statements, and log audit data. However, every SOQL and DML call consumes governor limits, and the code must be carefully bulk‑safe.
Security
Because LDS respects field‑level security out of the box, you don’t need to add extra permission checks. With Apex you must either rely on the platform’s automatic record‑type and field checks (when using @AuraEnabled with readOnly or writeOnly) or implement explicit Schema.sObjectType checks.
Performance
When a component needs to load many records at once, LDS can batch the requests internally. An Apex method that calls Database.query for each record will hit the SOQL limit sooner unless you write a single bulk query.
Feature Coverage
Any requirement that involves custom Apex actions (e.g., invoking a custom REST endpoint, performing a cross‑object calculation) forces you to use Apex. LDS cannot trigger custom actions or execute arbitrary logic.
Concrete Implementation & Validation
1. LDS‑Based Lightning Web Component (LWC)
import { LightningElement, api, wire } from 'lwc';
import { getRecord, updateRecord } from 'lightning/uiRecordApi';
import NAME_FIELD from '@salesforce/schema/Contact.LastName';
export default class ContactEditor extends LightningElement {
@api recordId;
@wire(getRecord, { recordId: '$recordId', fields: [NAME_FIELD] })
contact;
get name() {
return this.contact.data.fields.LastName.value;
}
handleNameChange(event) {
const fields = { Id: this.recordId, LastName: event.target.value };
updateRecord({ fields })
.then(() => console.log('Updated'))
.catch(error => console.error(error));
}
}
Run this component in Lightning Experience. Verify that no SOQL appears in the Developer Console when the component loads. After editing the contact via the UI, refresh the page and confirm the new value is displayed without a new query.
2. Apex‑Based Lightning Component
public with sharing class ContactController {
@AuraEnabled(cacheable=true)
public static Contact getContact(Id recordId) {
return [SELECT Id, LastName FROM Contact WHERE Id = :recordId LIMIT 1];
}
@AuraEnabled
public static void updateContact(String recordId, String lastName) {
Contact c = new Contact(Id = recordId, LastName = lastName);
update c; // Bulk‑safe if called in bulk context
}
}
import { LightningElement, api, wire, track } from 'lwc';
import getContact from '@salesforce/apex/ContactController.getContact';
import updateContact from '@salesforce/apex/ContactController.updateContact';
export default class ContactEditorApex extends LightningElement {
@api recordId;
@track contact;
@wire(getContact, { recordId: '$recordId' })
wiredContact({ error, data }) {
if (data) this.contact = data;
else console.error(error);
}
handleNameChange(event) {
updateContact({ recordId: this.recordId, lastName: event.target.value })
.then(() => console.log('Updated'))
.catch(error => console.error(error));
}
}
Deploy both components to a scratch org. Update a contact record via the UI, then reload both components. The LDS component should reflect the change immediately due to its caching mechanism; the Apex component will also show the updated value after the Apex method completes.
Validation Checklist
- Confirm that the LDS component performs no SOQL in the debug log on page load.
- Check that the Apex component logs the SOQL query and DML operation.
- Verify that updating a record in one component propagates to the other within the same session.
- Ensure no governor‑limit exceptions appear when bulk‑updating multiple records via the Apex method (use
Database.updatewithfalsefor partial success).
Limitations & Caveats
- LDS cannot replace Apex for complex business logic such as cross‑object validation, custom actions, or batch jobs.
- Using LDS in Salesforce Classic or Visualforce is unsupported; the component must run in Lightning Experience or LWC.
- Apex methods that perform DML without
@AuraEnabledannotations may bypass the platform’s automatic security checks – always include@AuraEnabled(readOnly=false)and perform explicit permission checks if necessary. - When writing bulk Apex, always use
Database.update(..., false)orDatabase.insert(..., false)to avoid partial failures.
Conclusion
If your component only needs to display and edit a handful of fields with minimal custom logic, LDS is the lighter, safer choice. When you require custom validation, cross‑object operations, or explicit control over DML, an Apex controller is necessary despite its higher maintenance cost.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.