Zum Inhalt springen

The Lock Footprint of a Save

Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.

One yellow robot arm's gripper holds a steel part on the line while a second arm's gripper hangs open beside it, waiting its turn.
Image generated with AI

The question this module answers: What does saving one record actually lock?

It’s four in the afternoon at Steve Trucking. Across nine distribution centers, 330 trucks are on their last few stops. A crew in one city taps Delivered in the Field Service app. A crew across town taps Installed. Another marks an exception: the Recipient wasn’t home. Each delivery goes through about eight status changes in a day, and with about 3,300 deliveries a day, that’s more than 26,000 saves, and a lot of them land in the same late-afternoon hour. At the same moment, a Retail Partner’s system is asking for status through its API, a dispatch coordinator is moving tomorrow’s stops around, and tomorrow’s revised manifest is arriving.

Every one of those saves looks like one record: one Work Order, one status. But each Work Order belongs to a Manifest, the partner’s list for that DC and that day, with a few hundred deliveries on it. And every Work Order points at its Retail Partner, and there are only three of them. So hundreds of crews, each saving a different record, can all be reaching for the same parent at the same moment.

Different records, the same parents Three columns. On the left, the 4 p.m. writers: crews, 330 trucks using the Field Service app, sending Delivered; the Retail Partners, through the integration layer, sending revised lines; coordinators, moving tomorrow's stops. Their arrows all reach the middle, highlighted: Work Orders, a few hundred, one per delivery. Then, on the right, the same parents: one Manifest per DC per day, and the Retail Partner's Account, only three of them. Every Work Order points at both. Underneath: different records, the same parents. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.48.0", "built": "2026-10-27", "id": "DGM-DA-TX-10", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "cells 0.4.0", "theme": "stevetecharc 1.4.0"} THE 4 P.M. WRITERS DIFFERENT RECORDS THE SAME PARENTS Crews 330 trucks, the Field Service app Retail Partners status and revisions, through the integration layer Coordinators moving tomorrow's stops Work Orders a few hundred, one per delivery Manifest one per DC per day Retail Partner Account: only three of them Delivered revised lines rescheduled each one points here each one points here Different records, the same parents. HOW TO READ IT the records each writer saves © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

That’s this module’s question. The user thinks, “I updated one record.” The architect needs to ask: what is the lock footprint of that save? Under concurrency, the size and shape of that footprint decide whether the design works at 4 p.m. or only at 10 a.m.

Before Salesforce, let’s look at the problem locking solves. A delivery’s line count says 2. Two processes read it at the same moment: one adds a haul-away, one adds a part. Each writes back 3. The right answer is 4. Without protection, the second write quietly erases the first. That’s called a lost update, and it’s the reason every database locks something.

A lock gives one transaction temporary control of the records it needs, so another transaction can’t make a conflicting change at the same time. That protection is a good thing. The trouble starts when lots of transactions need the same locks at the same time.

Two Ways to Lock: Optimistic and Pessimistic

Section titled “Two Ways to Lock: Optimistic and Pessimistic”

There are two broad ways a system can protect a record, and they start from opposite assumptions.

Optimistic locking assumes you won’t collide. Nothing is frozen while you work. You read the record, make your changes, and at save time the system checks whether anyone else changed the record since you read it. Usually nobody has, and the save goes through. If somebody has, the system catches it then, and your save is rejected so you can reload and try again. You don’t pay for protection you rarely need; you deal with the conflict when it actually happens.

Pessimistic locking assumes you will collide. So you take the lock before you start your change: lock, change, release. Nobody else can change that record until you’re done. Some systems take that lock the moment a user clicks Edit, and hold it while the edit screen is open. On the web that’s rarely practical. People open a record and go to lunch, close the browser tab, lose their connection, and the server never hears about it. The lock can be held for a long time, and you need a rule for when to release it: a timeout, or someone allowed to break the lock. Document-management systems do this on purpose with check-out and check-in. General record editing on the web almost never does.

So which one is Salesforce? Both, at two different levels.

  • Across the user’s edit session, Salesforce is optimistic. Opening a record to edit it locks nothing. If someone else saves the record while you’re editing, Salesforce catches it when you save, with what it calls collision detection: “The record was modified by [user] during your edit session. Make a note of the data you entered, then reload the record and enter your updates again.”
  • Inside the save itself, Salesforce is pessimistic, but brief. While the save’s transaction runs, the records it changes are locked, and so are related records: “When you perform a DML operation on one record, related records are locked in addition to the record in question.” The lock is released when the transaction completes. Another transaction that needs one of those records waits, up to 10 seconds, and then fails.
  • FOR UPDATE is pessimistic locking you ask for in Apex: lock these records now, for the rest of this transaction.

Integrations can be optimistic too, and they should be when they update a record that people also edit. The partner’s system reads a delivery at 9:00, and a coordinator changes the delivery window at 9:05. At 9:10 the partner sends its update, built from what it read at 9:00. Without a check, it quietly overwrites the coordinator’s change: the lost update again, this time across the API. Salesforce’s REST API lets the caller say “only if nobody has changed it since I read it”:

PATCH /services/data/v68.0/sobjects/WorkOrder/0WOxx0000000001
If-Unmodified-Since: Fri, 30 Oct 2026 09:00:00 GMT
Content-Type: application/json
{ "Status": "Rescheduled" }

REST API “processes the request only if the data hasn’t changed since the specified date. Otherwise, a 412 Precondition Failed status code is returned, and the request isn’t processed.” The caller then reads the record again and decides, which is exactly what Salesforce asks of a user who hits collision detection. Two details from the same page: this is “weak validation,” compared to the record’s last modified date, and the stronger If-Match check with ETags works on sObject Rows for “Account records only” (Conditional Request Headers (opens in a new tab), REST API Developer Guide, checked 2026-10-31).

Optimistic at the user level, pessimistic at the transaction level, and hopefully that transaction lasts about a second. That’s why, when I need to be sure nobody changes a record while my code decides what to do with it, I query it FOR UPDATE, get my lock, make my changes, and let the transaction finish. And I usually lock one record, not fifty thousand. A lock is protection, but it’s also a line: everyone else who needs that record waits behind you.

Test knowledge. Know what Salesforce locks and for how long: the records a save changes and related records, held until the transaction completes; a transaction waiting on a lock waits up to 10 seconds, then fails: a locking query throws a QueryException, a DML statement a DmlException with the status code UNABLE_TO_LOCK_ROW. FOR UPDATE locks queried records explicitly, can’t be combined with ORDER BY, and its locks are released if the transaction makes a callout. Other users can still read a locked record, so a read-then-write without FOR UPDATE can overwrite someone else’s change.

Here’s the lost update from the start of this module, at Steve Trucking. Say the Manifest keeps a count of its delivered Work Orders in a field, Delivered_Count__c, and a little Apex adds to it each time a crew taps Delivered. (A roll-up summary would do this for us on a master-detail. The Manifest is a lookup, so it’s coded, which is common when a team has moved off master-detail for exactly the reasons in this chapter.)

Two crews tap Delivered in the same second. Both transactions read the Manifest: 211 delivered. Both add one. Both write 212. The right answer is 213. Locked records can still be read, which is why: Salesforce’s own guide warns that a second update can “overwrite changes made by the locking client if the second client obtained an old copy of the record,” and gives the fix: “the second client must lock the record first. The locking process returns a fresh copy of the record” (Locking Statements (opens in a new tab), Apex Developer Guide, checked 2026-10-31).

So we read the Manifests FOR UPDATE:

public with sharing class ManifestDeliveredCount {
// Called from the Work Order trigger: for each Manifest, how many of its
// Work Orders just became Delivered in this transaction
public static void add(Map<Id, Integer> deliveredByManifest) {
// Lock the Manifests and read a fresh copy of each.
// Anyone else who needs them now waits until this transaction completes.
List<Manifest__c> manifests = [
SELECT Id, Delivered_Count__c
FROM Manifest__c
WHERE Id IN :deliveredByManifest.keySet()
FOR UPDATE // a locking query can't use ORDER BY
];
for (Manifest__c manifest : manifests) {
Decimal current = manifest.Delivered_Count__c == null ? 0 : manifest.Delivered_Count__c;
manifest.Delivered_Count__c = current + deliveredByManifest.get(manifest.Id);
}
update manifests;
// No callout after the lock: a callout releases FOR UPDATE locks.
// The locks are released when the transaction completes.
}
}

Now the second crew’s save waits at the query until the first finishes, then reads 212 and writes 213. That’s the whole idea: lock, read fresh, change, finish. It’s bulkified the way Salesforce code has to be (one query for all the Manifests in the save, not one per Work Order), and it locks only the Manifests this save touches.

Notice what it does at 4 p.m., though. Every crew’s save on the same Manifest now lines up behind this lock. That’s the footprint on purpose, and it’s the right call for a count that must be correct. Module 2 shows how to make the line shorter, including moving this count out of the crew’s save altogether, and module 3 shows why the version you’d run in production recounts the Manifest instead of adding one.

And when the line is too long? The wait runs out at 10 seconds. The locking query throws a QueryException; an update that can’t get its lock throws a DmlException, and its status code says why:

try {
ManifestDeliveredCount.add(deliveredByManifest);
} catch (QueryException e) {
// The FOR UPDATE query waited more than 10 seconds for the lock.
// (QueryException covers other query errors too; log the message.)
// Hand the work to a safe retry (module 3); don't swallow it.
} catch (DmlException e) {
if (e.getDmlType(0) == StatusCode.UNABLE_TO_LOCK_ROW) {
// The update waited more than 10 seconds for a lock.
// Hand the work to a safe retry (module 3); don't swallow it.
} else {
throw e;
}
}

getDmlType “returns the value of the System.StatusCode enum” for a failed row (Exception Class and Built-In Exceptions (opens in a new tab), Apex Reference Guide, checked 2026-10-31), and Salesforce describes UNABLE_TO_LOCK_ROW as “a deadlock or timeout condition” (Core Data Types Used in API Calls (opens in a new tab), SOAP API Developer Guide, checked 2026-10-31). Catching it isn’t the fix. It’s how you see the collision and hand the work somewhere safe. The fix is in the design, and that’s the rest of this chapter.

This is why concurrency problems are so hard to find during design. I create a record: it works. You create one: it works. The developer runs the unit tests: they pass. The business runs UAT: it works. Then production arrives with users, integrations, batch jobs, data loads, scheduled automation and agents, all writing at the same time, and something that worked perfectly one transaction at a time starts failing.

Nothing about the individual transaction changed. What changed was concurrency: the transactions started competing for the same records. Salesforce’s Well-Architected guidance says it plainly: “Concurrent execution reveals locking issues invisible in single-user testing.” When I teach parallel loads, I say it this way: it will work today and fail tomorrow, because whether it fails depends on what everything else in the org is doing at that moment.

So stop asking only “what record am I saving?” and ask: what records does this save need to protect while it completes? That’s the lock footprint.

Take one crew’s tap at 4 p.m. The record they think they saved is one Work Order, now Delivered. Here’s what that save can reach:

  • The Work Order itself, locked while the save runs.
  • The Manifest it belongs to, locked because the Work Order points at it.
  • The Retail Partner’s Account, locked for the same reason.
  • Anything a roll-up or automation writes. If the Manifest counts its delivered Work Orders, the Manifest isn’t only locked; it’s saved, inside the crew’s save. If a flow updates the Service Appointment or posts a status for the partner, those records join the footprint too.
The lock footprint of one save On the left, highlighted, the record the crew thinks they saved: one Work Order, one delivery, now Delivered. Frame by frame, what the save also reaches. First, the Manifest and the Retail Partner's Account, each locked because the Work Order points at it. Then, if a roll-up counts the Manifest's deliveries, the Manifest's own save, with its validation, flows and triggers, runs under the lock. Last, if automation updates it, the Service Appointment. Underneath: the footprint is everything the save must protect until it commits. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.48.0", "built": "2026-10-27", "id": "DGM-DA-TX-11", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "cells 0.4.0", "theme": "stevetecharc 1.4.0"} THE RECORD YOU THINK YOU SAVED WHAT THE SAVE ALSO REACHES Work Order one delivery, now Delivered Manifest locked: the Work Order points here Retail Partner Account, locked the same way Service Appointment if automation updates it The Manifest's own save its validation, flows and triggers, under the lock lookup lock lookup lock if a roll-up counts it automation The footprint is everything the save must protect until it commits. HOW TO READ IT the record on the screen locked or saved inside the same transaction © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

One record on the screen. Four or five records in the transaction. Now put a few hundred crews on top of that.

This is where data modeling becomes concurrency architecture. We normally teach relationships in terms of meaning, cardinality, ownership, deletion and security. At scale I also want you asking: what does this relationship cause Salesforce to lock, and what does it cause Salesforce to do while the lock is held?

Start with the part people get wrong. “It’s only a lookup, so the parent isn’t involved.” It is. Salesforce’s engineering team put it plainly: “Every time a record is inserted or updated, Salesforce must lock the target records that are selected for each lookup field.” That’s how Salesforce keeps the parent from being deleted halfway through your save. A master-detail locks the master too. So a lookup doesn’t avoid the lock.

What a master-detail can add is work on the parent. A roll-up summary field lives on the master, and Salesforce’s own order of execution says what happens when a detail is saved: “If the record contains a roll-up summary field or is part of a cross-object workflow, performs calculations and updates the roll-up summary field in the parent record. Parent record goes through save procedure.” Then the same again for the grandparent. So with a roll-up, every Work Order save also runs the Manifest’s save: its validation rules, its triggers and flows, and any roll-up above it, all while the Manifest is locked.

That’s the difference I’d teach. Not “master-detail makes a bigger lock,” because Salesforce doesn’t publish its lock internals beyond what’s documented here. It’s that a roll-up makes more work happen while the lock is held, and a longer hold is a longer line. Salesforce’s Well-Architected framework names the same trade: master-detail gives you roll-up summaries “at the cost of tighter coupling,” while “lookups provide flexibility at the cost of custom rollup logic.” That’s why, when it was critical, I’ve moved a master-detail to a lookup and coded the roll-ups myself. The win comes when that counting runs outside the child’s save, later or in batches. Move it into a flow that updates the parent’s count in the same save, and you’ve put the problem straight back.

Test knowledge. Know that a lookup locks the record it points to when a child is inserted or updated, so lookups can cause lock contention too (lookup skew). Know that a roll-up summary makes the child’s save update the parent, and that the parent then goes through its own save (and the grandparent’s roll-up after it), inside the same transaction. Master-detail parents also carry chapter 7’s guidance on children per parent.

When I teach deadlocks, I use two cars stuck at an intersection. Let me start with the intersection itself.

At 4 p.m., 330 trucks are each heading to a different house. Different routes, different stops, different Recipients. But near the DC, they all have to cross the same intersection. A truck clears it in a second, so on a normal day nobody notices. Only one can be in the middle at a time, though, and when they all arrive at once, the line backs up. The trucks at the back wait, and wait, and give up.

That intersection is the shared parent. Each crew’s save is a different Work Order, but every one of them has to cross the same Manifest, and only one at a time can hold its lock or run its roll-up. Salesforce’s waiting limit is the trucks at the back: after 10 seconds, the save fails with UNABLE_TO_LOCK_ROW. Chapter 7 called this skew, too many children under one parent, and Salesforce says what it does at 4 p.m.: when a parent piles up children, “row-lock contention emerges during concurrent operations.” This is the transaction-level reason skew hurts: a crowded parent isn’t only a modeling concentration, it’s a concurrency concentration.

In my experience, the most common cause of lock errors is parallel processing at the same time, typically competing incoming APIs. System A sends updates. System B sends updates. A bulk job is running too. Each one looks reasonable on its own, but they arrive together and touch overlapping data. Then you see UNABLE_TO_LOCK_ROW, and the temptation is to call it an API problem or a random Salesforce error. I don’t. I want to know: what are these transactions all trying to lock? Find the overlap, and that’s usually where the architecture problem is hiding.

Data loads show it too. When I ask a class whether they run loads in serial or parallel mode, the usual answer is: go parallel, and if there are locking errors, switch to serial. Grab the speed, wait till you run into the speed bumps, then slow the car down. That’s fair, but you can do better than slowing down, and the next module shows how. Chapter 10 has the loading side: sort by parent, and keep all of one parent’s children in the same batch.

Now the deadlock. A single lock that’s taken just makes the next transaction wait. A deadlock needs a multi-step lock: two transactions that each lock two records, in a different order. Transaction A updates the Retail Partner’s Account first, then needs the Manifest. Transaction B updated the Manifest first, and now needs the Account. It’s like two cars stuck at an intersection: each is waiting for the other to move, and neither one moves. That’s a deadlock. Something has to break the loop by failing a transaction: a database that detects the deadlock fails one of them, and which one is hard to predict; on Salesforce, a transaction that waits past 10 seconds fails too. So at least one side fails, sometimes both.

Two trucks stuck in the intersection Transaction A on the left, Transaction B on the right, the two records between them. First: Transaction A holds the Retail Partner's Account; Transaction B holds the Manifest. Then each waits for the record the other holds: A waits for the Manifest, B waits for the Account. The waiting arrows are red, each marked with an X: a closed loop, and neither can move. Underneath: each holds what the other needs; lock in the same order everywhere. {"author": "Steve Simpson", "blueprint": "1.0.1", "builder": "standard 1.57.1", "built": "2026-10-31", "id": "DGM-DA-TX-12", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "cells 0.5.0", "theme": "stevetecharc 1.4.0"} TRANSACTION A TWO RECORDS TRANSACTION B Transaction A Account first, then the Manifest Retail Partner Account Manifest one DC, one day Transaction B Manifest first, then the Account holds waits for waits for holds Each holds what the other needs. Lock in the same order everywhere. HOW TO READ IT a lock each transaction holds © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

Salesforce works hard to keep its own engine out of that intersection. The Apex runtime “first locks sObject parent records, then children,” and “locks sObject records in order of ID when multiple records of the same type are being edited.” Your code has to do the same: the guide’s advice is “accessing tables and rows in the same order from all locations in an application.” Same order everywhere, and two trucks can’t each be waiting on the other.

If I’m holding a lock, I want to hold it for as little time as I reasonably can. Back at the intersection: if every truck clears it quickly, traffic flows. If someone parks in the middle to make a phone call, everything backs up.

So when I look at a transaction, I ask: how many records does it touch? How much synchronous automation runs? Which related records get updated? What could happen later instead? How long is each lock held? That doesn’t mean “make everything asynchronous.” It means the synchronous transaction should contain only the work that truly has to happen before the user sees Saved.

Locking Protects Integrity. It Doesn’t Decide Business Truth.

Section titled “Locking Protects Integrity. It Doesn’t Decide Business Truth.”

At 4 p.m. a coordinator changes a delivery window, the partner’s system sends a revised manifest line, and an agent decides the same delivery needs rescheduling. Locking stops those transactions from physically corrupting each other’s writes. It can’t tell you who should own that field, which update should win, or whether the agent should have been allowed to make the change. Those are architecture decisions, and we made them in chapters 6 and 9. Concurrency doesn’t replace those rules; it makes them more important. A lock can protect the transaction. It can’t define the truth.

We used to design for humans, integrations and batch jobs. Now add agents. An agent can query, decide, call an action, update and retry, over and over, without getting tired. The platform’s locking rules haven’t changed. What changed is the number of actors and how they behave. Remember the Number of Zeros: a footprint that was fine with ten people may hurt with hundreds or thousands of automated actions.

So when somebody says, “We’re going to let the agent update deliveries,” my next questions are: which deliveries? Through what process? At what rate? What else writes them? And what does each save lock? Give agents a focused scope on updates, and have them call established, tested processes rather than writing records directly.

Beyond the exam: what AI changes. Agents add another concurrent writer to an architecture already serving users, integrations and automation. Give them narrow update scope and established, tested processes; don’t give an agent broad write access and hope the transaction model sorts it out.

When I troubleshoot locking, I don’t start with “can we retry it?” Retry may be part of the answer eventually, but first I want to see the shape of the transaction. Draw it: this record changes; that parent is locked; that roll-up runs; that automation updates another record. If the same transaction also touches setup records such as users or permission sets, mark that too: DML on these setup objects “can’t be mixed with DML on non-setup sObjects in the same transaction,” so that’s a boundary you have to design around. Then overlay another transaction, and another. Where the footprints overlap is the hot spot, and once you can see the collision you can decide what to do about it. That’s module 2.

And when a retry does come, and something always retries, whether it’s the app, the integration layer or an agent, it has to be safe: multiple attempts mustn’t break something. That’s called idempotency. Press the elevator button five times and you still get one elevator. Bill a Retail Partner five times for one delivery, and you’ve billed them five times. Module 3 is about making the second attempt behave like the first.

  • Which records a business transaction may actually lock beyond the record the user or integration appears to be saving.
  • Where relationships, roll-ups, lookups, sharing and automation create overlapping lock footprints.
  • Which shared parent or referenced records can become concurrency hot spots as parallel volume increases.
  • Which work truly belongs inside the synchronous transaction and which work can happen later.
  • How human, integration, batch and agent writers overlap against the same data model.

At 10 a.m. a crew taps Delivered and nothing happens to anyone else. At 4 p.m. the same tap, on a different Work Order, lands in a line of hundreds, all reaching for the same Manifest. The record didn’t change. The data model didn’t change. What changed was how many saves arrived at once. So before you tune anything, draw the footprint: this record, its parent, its roll-up, its automation. Then put the 4 p.m. traffic on top of it. Where the footprints overlap is where the architecture lives. Module 2 is about what to do there.

Next: Designing Out Lock Contention