Lightning Web Components & Transaction Design
An LWC is not a transaction coordinator
A promise chain is not a transaction
A user clicks Confirm and an LWC updates an Opportunity, creates two child records, calls an Apex method and finally shows a success toast. In a developer sandbox the sequence feels like one operation. In production, the third request fails after the first two commit, the user retries, and the page now contains duplicate children with an Opportunity that says the process completed. The component coordinated screens and requests; it never owned an atomic transaction.
Salesforce's LWC data guidance is explicit: createRecord, updateRecord and deleteRecord operations are independent transactions, even when JavaScript invokes several of them. For multiple records that must succeed or fail together, Salesforce recommends Apex. The JSBC Labs position is that an LWC should orchestrate user intent, not business consistency. The transaction boundary belongs where the platform can enforce it: on the server.
Use the smallest correct data boundary
For a single-record edit, start with Lightning Data Service and base components. lightning-record-form and lightning-record-edit-form provide metadata-aware fields, server validation, standard errors and UI API security without a custom controller. LDS also shares cached records across components, deduplicates requests and reflects supported changes to subscribers. That is usually safer and faster than rebuilding record editing in JavaScript and Apex.
Move to an imperative UI API function when the interface needs custom interaction but the business action still changes one record independently. Move to Apex when records must be queried by unsupported criteria, unsupported objects are involved, or several writes need one commit or rollback decision. The important choice is not declarative versus code; it is whether the selected boundary matches the consistency promise made to the user.
Expose one business command
If confirming an order means updating the header, reserving stock, creating lines and recording an audit entry, expose one Apex command such as confirmOrder(request). Do not make the component call updateHeader(), reserveStock(), createLines() and writeAudit() separately. One coarse business command gives Apex a single transaction, one authorization point and one structured outcome. Internal services can still remain modular behind that entry point.
Define the request as a versioned contract containing business inputs, not screen state. Return an outcome such as status, business identifier, record IDs, user-safe message and retry guidance. Avoid leaking raw exceptions or database vocabulary into the UI. Salesforce applies governor limits per Apex invocation; collapsing unrelated work into one enormous controller is not the goal. Group only the changes that share an atomic business outcome, and design the service to handle collections where realistic workloads require it.
Keep authority on the server
Client validation improves usability, but it is neither a security control nor the final statement of a business rule. Inputs can be stale, manipulated or submitted through another channel. Enforce required invariants with validation rules, Apex checks, database constraints and the same domain services used by other entry points. Salesforce recommends validation rules for record-edit forms because the server applies them consistently and returns field-level errors.
Treat every Apex parameter as untrusted. Re-query records required for the decision, verify their current state and enforce record, object and field access for the supported API version and execution mode. LDS and UI API respect CRUD, field-level security and sharing automatically; custom Apex makes the developer responsible for using the intended security context. The component may hide an action, but the server must decide whether the action is allowed.
Design for uncertain submissions
Disabling a button reduces double-clicks; it does not prove exactly-once execution. A browser can lose the response after the server commits, a mobile connection can retry, or a user can reopen the page. Generate or obtain a stable idempotency key for consequential commands and persist it with the business result. A repeated request with the same key should return the existing outcome or a clear in-progress state rather than repeat side effects.
Show progress immediately, prevent concurrent local submissions and keep the user on a recoverable screen when the outcome is unknown. Distinguish validation failure, authorization failure, conflict, transient dependency failure and unexpected error. Only advise Retry when the command is safe to repeat. Idempotency is JSBC Labs architectural guidance, not behaviour supplied automatically by an imperative Apex call or JavaScript Promise.
Reconcile every cache after commit
A successful Apex response does not automatically refresh every record displayed through Lightning Data Service. Salesforce documents that Apex-provisioned data is not managed by LDS. After an imperative Apex write, call notifyRecordUpdateAvailable() for affected record IDs so LDS-backed wires across instantiated components can obtain current data. For wired Apex data, use refreshApex() with the provisioned value.
Do not mix Apex and LDS casually for the same record. Salesforce's 2026 LWC anti-pattern guidance warns that their independent caches can diverge and recommends one consistent data source per record where possible. When a use case genuinely combines them, document which operation owns mutation and which caches require refresh. A toast that says Saved while sibling components show old values is a consistency defect, not cosmetic lag.
Separate local rollback from remote outcomes
An Apex transaction can roll back Salesforce DML; it cannot rewind an email already sent or a remote order already accepted. If the command crosses a system boundary, decide whether the remote call happens before commit, after commit or through an asynchronous message. Define correlation IDs, timeouts, duplicate handling, reconciliation and compensation for the chosen pattern. Do not imply atomicity across systems because the user initiated everything from one modal.
For long-running or independently recoverable work, let the synchronous transaction record an accepted business request, then hand off to a governed asynchronous process. Return a status the component can explain and later refresh. This creates an honest contract: the Salesforce request was accepted, while fulfilment remains pending. The alternative—a spinner waiting across several systems—couples user experience to every dependency and turns network uncertainty into duplicate business work.
Test the contract, not only the component
Jest tests should prove button states, parameters, success rendering and error rendering. Apex tests must prove atomic rollback, permissions, validation, idempotent replay, bulk behaviour and structured outcomes. Add integration tests for cache refresh, stale records, simultaneous edits, lost responses and unavailable dependencies. Test with real permission-set personas; an administrator happy path says little about the production transaction.
Instrument each command with a correlation ID, operation name, actor, duration, outcome category and affected business identifiers—without logging sensitive payloads. Monitor duplicates, conflicts, retries, partial external outcomes and cache-related support incidents. An LWC succeeds when it communicates intent and state clearly. The business transaction succeeds when the server can enforce, repeat, diagnose and recover the promised outcome independently of the browser that started it.
Official references
- Salesforce Developers: LWC Data Guidelines
- Salesforce Developers: Lightning Data Service
- Salesforce Developers: Call Apex Methods Imperatively
- Salesforce Developers: Call Apex Methods
- Salesforce Developers: Edit a Record
- Salesforce Developers: Secure Apex Classes
- Salesforce Developers: notifyRecordUpdateAvailable
- Salesforce Developers Blog: Data and Communication Anti-Patterns in LWC