Salta ai contenuti

Safe to Retry

Questi contenuti non sono ancora disponibili nella tua lingua.

A yellow robot arm holds a copper sleeve over a row of steel cylinders, above one that already has its sleeve fitted, while the last two still wait for theirs.
Image generated with AI

The question this module answers: How do integrations and agents write without creating duplicates or corrupting data when they have to retry?

A Retail Partner’s DC manager files a damage claim in the portal: the dishwasher arrived with a dented door. Customer care approves it, and Salesforce sends the finance ERP a request through the integration layer: credit the partner $180 for this claim. The ERP issues the credit. Then the connection drops, and the answer never makes it back.

What does the integration layer know? It knows it sent the request. It knows it didn’t get a successful response. It does not know the credit failed. So it does what it was built to do: it tries again. The ERP gets a second request to credit the partner $180, and it does. The partner now has $360 for one dented door, and somebody in finance spends a morning finding it.

How the credit doubled A timeline in two lines, read left to right, built frame by frame. The first attempt: attempt 1, credit the partner $180, is sent to the finance ERP, which issues credit 1, $180. The lost answer: the ERP answers done, but that answer, in coral and marked with a red X, never reaches the sender. The retry: on the next line, attempt 2 sees only a timeout and resends; the finance ERP issues credit 2, $180, in coral. Underneath: nothing malfunctioned; the retry worked perfectly; that's the problem. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.48.0", "built": "2026-10-27", "id": "DGM-DA-TX-32", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "cells 0.4.0", "theme": "stevetecharc 1.4.0"} Attempt 1 credit the partner $180 Finance ERP issues credit 1: $180 The answer: done never reaches the sender Attempt 2 a timeout, so it resends Finance ERP issues credit 2: $180 sends answers sends again Nothing malfunctioned. The retry worked perfectly. That's the problem. HOW TO READ IT where it goes wrong: $360 for one claim © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

Nothing malfunctioned. The retry worked perfectly. That’s the problem. Modules 1 and 2 were about keeping transactions from colliding; even after all four levers, some will still fail, and networks still time out. This module is about the moment after: whether the second attempt can do no harm.

Chapter 9 made this point, and it’s worth building on: a request that failed and a request whose answer was lost look identical to the caller. The request may never have reached the ERP. It may have reached it and failed. It may have succeeded completely, with only the response lost. From the caller’s side, all three can look like one word: timeout.

So reliable architecture can’t assume “no response means nothing happened.” And a timeout doesn’t mean the other side stopped working. The ERP may still be processing the first request when the retry arrives, and now the caller is competing with itself. Timeout and failed aren’t synonyms. Design for the ambiguity, and retries become a data architecture problem.

Some actions are harmless to repeat; many aren’t. Credit the partner, retry: two credits. Ship a replacement part, retry: two parts on two trucks. Create a case for the Recipient’s complaint, retry: two cases and two agents calling the same person. Bill a delivery, retry: the partner is invoiced twice and disputes the whole invoice.

A reliable system isn’t one where operations never fail. That’s impossible. It’s one designed so that the failures we expect don’t turn into wrong business records.

In module 1, I gave you the word: idempotency. Multiple attempts mustn’t break something. Press the elevator button five times and you still get one elevator.

That doesn’t mean nothing happens on the second request. The receiver might recognize it already did the work and return the first answer. It might update the same record again with the same values. It might ignore the duplicate. What matters is that the retry doesn’t create another business event that shouldn’t exist. Stripe, which processes payments for a living, puts it in one line: idempotency is for “safely retrying requests without accidentally performing the same operation twice.”

How can the ERP tell whether this is a second credit, or a retry of the first one? It needs an identity to check.

Sometimes the record’s own identity is enough. In Checkpoint 1 we gave every delivery a key: the Retail Partner’s own order number, a unique External ID on the Work Order. A resent manifest upserts on that key, so it updates the same Work Order instead of creating a second one. Billing works the same way: each delivery is billed once, so if the ERP keys its invoice line on that same delivery key, a retried “bill this delivery” finds the line it already wrote. That’s record identity: which record is this?

Credits are different. One delivery can legitimately earn more than one credit: the dented dishwasher today, a missing installation part next week. So the delivery key can’t tell a retry from a genuine second credit. The operation needs its own identity: which business action is this? At Steve Trucking, every credit comes from a claim, and every claim is a Case with its own number. Send that claim number with the request as the idempotency key, and the ERP can ask: have I already processed this claim’s credit? If yes, don’t credit again; return the first result.

The same credit, retried twice Two equal panels. Retry without a key, in coral, where each attempt looks new: first attempt, credit the partner $180, and the ERP issues it; then the answer is lost, and the retry looks like a new request; the result, two credits, $360 for one claim. Retry with the claim number, in green, the key being claim C-48213: first attempt, credit claim C-48213 for $180, and the ERP issues it and records the key; then the answer is lost, and the retry carries C-48213 again; the result, already done, the first answer comes back: one credit, $180. Underneath: the retry has to carry the same identity as the first attempt. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.48.0", "built": "2026-10-27", "id": "DGM-DA-TX-30", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "comparison 0.4.0", "theme": "stevetecharc 1.4.0"} Retry without a key each attempt looks new FIRST ATTEMPT Credit the partner $180 The ERP issues it THEN The answer is lost The retry looks like a new request THE RESULT Two credits: $360 for one claim Retry with the claim number the key: claim C-48213 FIRST ATTEMPT Credit claim C-48213, $180 The ERP issues it and records the key THEN The answer is lost The retry carries C-48213 again THE RESULT Already done: the first answer comes back One credit: $180 The retry has to carry the same identity as the first attempt. © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

Salesforce’s Well-Architected guidance says the same: design actions “so a retried call produces the same end state as a single successful call, using an idempotency key such as a request ID or external ID with upsert logic instead of insert, so a retry updates the same record rather than creating a duplicate.” Inside Salesforce, that’s the tool you already have: a unique External ID and an upsert. Save each incoming request on a record whose External ID is the idempotency key, and a second arrival can’t create a second record; the unique field rejects it.

Here’s what both kinds of identity look like in Apex. Record identity first: a resent manifest upserts on the delivery key, so it finds the Work Orders it already wrote.

// Partner_Delivery_Key__c is a unique External ID on the Work Order (Checkpoint 1).
// Found: update it. Not found: create it. Resent: the same Work Order, not a second one.
List<Database.UpsertResult> results = Database.upsert(
workOrders,
WorkOrder.Fields.Partner_Delivery_Key__c,
false // allOrNone: one bad row doesn't fail the rest
);

Database.upsert takes the field “that upsert() uses to match sObjects with existing records”; it can be “a custom field marked as external ID” (Database Class (opens in a new tab), Apex Reference Guide, checked 2026-10-31). And if the key “is matched multiple times, then an error is generated,” which is chapter 10’s point about keys that aren’t really unique (Upserting Records (opens in a new tab), Apex Developer Guide, checked 2026-10-31).

Then operation identity, on the receiving side. Say the credit requests arrive in Salesforce, and each one is saved on a Credit_Request__c whose Claim_Number__c is a unique External ID. Here an insert is the right call, not an upsert: a retry mustn’t overwrite the first request, it must be recognized.

Credit_Request__c request = new Credit_Request__c(
Claim_Number__c = claimNumber, // the idempotency key, unique
Partner__c = partnerId,
Amount__c = amount,
Status__c = 'Received'
);
Database.SaveResult result = Database.insert(request, false);
if (!result.isSuccess()
&& result.getErrors()[0].getStatusCode() == StatusCode.DUPLICATE_VALUE) {
// We already have this claim's credit: it's a retry, not a second credit.
// Look up the first request and return its result.
}

DUPLICATE_VALUE is Salesforce’s status code for “a duplicate value for a field that must be unique” (Core Data Types Used in API Calls (opens in a new tab), SOAP API Developer Guide, checked 2026-10-31). The database enforces the key, so two copies arriving at the same moment still make one record.

A retry has to represent the same intended operation. Don’t generate a brand-new key every time you retry. If attempt one carries claim C-48213 and the retry carries a freshly made request number, the ERP has no way to know they’re the same credit. From its side, two different credit requests arrived.

So the key is created once, when the business decides to act, and stored with the work: the claim number is already on the Case. Every retry, by the integration layer, a batch job or a person pressing Resend, sends that same key. And decide how long the receiver remembers keys. Stripe, for example, can prune a key once it’s 24 hours old. A retry that comes after the receiver has forgotten the key is a new request again.

Where Should I Require an Idempotency Key?

Section titled “Where Should I Require an Idempotency Key?”

I wouldn’t add idempotency machinery to every field update in the system just because we can. Ask what happens if the operation runs twice. Setting a delivery’s status to Delivered twice is harmless: it’s still Delivered. A second $180 credit isn’t. A second replacement part isn’t. A second invoice isn’t.

So require an operation key where repeating the action creates another business event with a real consequence: money, goods, a message to a customer. The higher the consequence, the stronger the guarantee I want.

Idempotency isn’t only about inserts. The API method doesn’t decide it; the business meaning does. “Set the status to Delivered” can run twice safely. “Add one to the Manifest’s delivered count” can’t: run it twice and the count is wrong.

That matters for module 2’s design. The platform event trigger that keeps the Manifest’s counts current will sometimes see an event more than once: Salesforce’s own retry mechanism for these triggers sends a batch again, and a trigger that resumes after a failure can pick up events it had started. Well-Architected’s advice for replays is that they work when “the subscriber is idempotent.” So the subscriber shouldn’t add one per event. It should recount the Manifest’s Work Orders and write the total. Recount twice, and you get the right number twice.

In chapter 9 we established proof of delivery: the client owns the conversation, and whoever knows what should arrive proves that it did. If I send data to another system, I don’t get to say, “I called the API, my job is done.” I need to know it got there, and retry is part of meeting that obligation.

Now add the companion rule: if the client owns the retry, the architecture must make the retry safe. Otherwise we’ve built a contradiction. We tell the sender, “keep trying until you’re sure it arrived,” while the receiver is designed so that every try might create another copy. That isn’t reliability. Completeness and idempotency belong together.

Here’s what that looks like on the same lost answer. Before it sends, the integration layer writes the request to its send log with the claim number. The ERP issues the credit and records the claim number. The answer is lost, exactly as before. The retry carries the same claim number; the ERP finds it already done and returns the first answer, with its credit reference. Now the send log can mark the claim delivered, with the ERP’s own number as the proof. One credit, and proof that it arrived.

The same credit, delivered and proven A timeline in two lines, read left to right, built frame by frame, the pair to the diagram of how the credit doubled. The first attempt: attempt 1, credit claim C-48213 for $180, logged as sent, goes to the finance ERP, which issues the credit and records C-48213. The lost answer: the ERP answers done, but the answer, in coral with a red X, never reaches the sender. The retry: on the next line, attempt 2 sees a timeout and resends with the same key, C-48213; the finance ERP, in green, finds C-48213 already done and returns the first answer. The proof: the send log, in green, marks it delivered, with the ERP's credit number. Underneath: the same lost answer; one credit, and proof that it arrived. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.48.0", "built": "2026-10-27", "id": "DGM-DA-TX-33", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "cells 0.4.0", "theme": "stevetecharc 1.4.0"} Attempt 1 credit claim C-48213, $180; logged as sent Finance ERP issues the credit and records C-48213 The answer: done never reaches the sender Attempt 2 a timeout, so it resends C-48213 Finance ERP C-48213 already done: the first answer Send log delivered, with the ERP's credit number sends answers same key answers The same lost answer. One credit, and proof that it arrived. HOW TO READ IT one credit, $180, and proof that it arrived © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

Inside One Salesforce Transaction: All, Part, or Back to Here

Section titled “Inside One Salesforce Transaction: All, Part, or Back to Here”

Before we retry anything across the network, know what Salesforce does with a failure inside one transaction. By default, it’s all or nothing: “If the request doesn’t complete successfully, all database changes are rolled back” (Transaction Control (opens in a new tab), Apex Developer Guide, checked 2026-10-31). That’s a gift. Half a credit never gets saved.

You have two ways to choose something else.

Record by record. The Database methods take allOrNone. Set it to false, and the records that can save, save; each one that can’t comes back with its own error. That’s how you retry only what failed, and only the failures worth retrying:

List<Database.SaveResult> results = Database.update(workOrders, false);
List<WorkOrder> retryLater = new List<WorkOrder>();
for (Integer i = 0; i < results.size(); i++) {
if (results[i].isSuccess()) {
continue;
}
Database.Error err = results[i].getErrors()[0];
if (err.getStatusCode() == StatusCode.UNABLE_TO_LOCK_ROW) {
retryLater.add(workOrders[i]); // transient: try again later, with backoff
} else {
// permanent until fixed (validation, bad data): park it, don't retry
}
}

Back to a savepoint. When several steps must stand or fall together, but the transaction should carry on, set a savepoint and roll back to it:

Savepoint beforeCredit = Database.setSavepoint();
try {
insert request; // the credit request, keyed by claim number
claim.Status = 'Credit Requested';
update claim; // the claim Case moves on with it
} catch (DmlException e) {
Database.rollback(beforeCredit); // undo both, never just one
// ...then record the failure, outside the work we just rolled back
}

Across the API. An integration can ask for the same all-or-nothing. A partner’s new delivery arrives as a Work Order and its line items. Sent as separate calls, a failure halfway leaves a Work Order with half its lines. Sent as one Composite request with allOrNone, it all saves or none of it does, and both upsert on their keys, so a resend is safe too:

POST /services/data/v68.0/composite
{
"allOrNone": true,
"compositeRequest": [
{
"method": "PATCH",
"url": "/services/data/v68.0/sobjects/WorkOrder/Partner_Delivery_Key__c/REDROCK-8472914-1",
"referenceId": "workOrder",
"body": { "Status": "New", "Subject": "Dishwasher delivery and install" }
},
{
"method": "PATCH",
"url": "/services/data/v68.0/sobjects/WorkOrderLineItem/Partner_Line_Key__c/REDROCK-8472914-1-01",
"referenceId": "haulAway",
"body": {
"WorkOrder": { "Partner_Delivery_Key__c": "REDROCK-8472914-1" },
"Description": "Haul away the old dishwasher"
}
}
]
}

“If the value is true, the entire composite request is rolled back”; if false, “the remaining subrequests that don’t depend on the failed subrequest are executed” (Composite Request Body (opens in a new tab), REST API Developer Guide, checked 2026-10-31). One request carries “up to 25 subrequests” (Send Multiple Requests Using Composite (opens in a new tab), checked 2026-10-31). The line item finds its Work Order by the partner’s key, not by a Salesforce ID, the way Salesforce’s upsert guide shows for “a relationship” referenced “using an external ID” (Insert or Update (Upsert) a Record Using an External ID (opens in a new tab), checked 2026-10-31).

Three rules from the same guide. Savepoints aren’t free: “Each savepoint you set counts against the governor limit for DML statements.” A record inserted before the rollback keeps its ID in your variable, so to insert it again, “create an sObject variable without setting its ID.” And a rollback only undoes Salesforce’s changes. That’s why the call to the ERP doesn’t happen in the same transaction: with saves still pending, a callout fails with “You have uncommitted work pending. Please commit or rollback before calling out.” So the credit request is saved and committed first, and a separate transaction sends it: the send log from proof of delivery, written down before anything leaves.

Test knowledge. A Salesforce transaction is all or nothing by default: any failure rolls back every change. Database methods with allOrNone set to false allow partial success, returning a result per record so you can retry only the failures. Database.setSavepoint() and Database.rollback() undo part of a transaction; each counts against the DML statement limit. A rollback can’t undo a callout, and a callout can’t be made with uncommitted work pending. Across the REST API, a Composite request with allOrNone: true rolls back every subrequest if one fails.

Not every failure deserves another attempt. Sort them into three kinds before deciding anything.

  • Transient: try again later. A record locked by another transaction, a timeout before the request was processed, a server that’s briefly overloaded. Waiting can change the result.
  • Permanent until someone fixes it: don’t retry. The claim number is missing, the amount fails validation, the request is malformed. Retrying the same payload twenty times gives you twenty failures. That’s not resilience; it’s noise. Park it, fix it, then replay it.
  • Unknown: check first. The request timed out and you can’t tell whether it ran. Ask the receiver by the key whether the operation already happened, or retry with the same key and let idempotency answer. Never retry an unknown outcome with a new identity.

Well-Architected spells out the common cases: retry network timeouts with a short backoff; after a rate-limit response (HTTP 429), retry “after the Retry-After header value”; retry server errors (5xx) “with exponential backoff”; for other client errors (4xx), “Don’t retry; fix the request”; and for governor limit errors, “Don’t retry in the same transaction; re-queue as an async operation.” Its agent guidance names our case directly: lock contention is a transient failure “worth one bounded retry,” while a validation error “fails identically on repetition.”

When a request comes back, or doesn't A decision ladder read from top to bottom. It starts with a request that comes back, or doesn't. Did it succeed? If yes, done: log the proof. If no: is the outcome unknown, as after a timeout? If yes, check by the key, then retry only with the same key. If no, a dashed barrier: under the attempt limit? If not, stop and alert. Then: could waiting change the result, as with a lock, a timeout or an overloaded server? If yes, retry later, with backoff and jitter, under the same key. If no, it's permanent: park it, fix it, then replay it. Underneath: decide by what you know about the outcome; every retry carries the same key. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.48.0", "built": "2026-10-27", "id": "DGM-DA-TX-31", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "flow/decision 0.1.0", "theme": "stevetecharc 1.4.0"} A request comes back or doesn't Did it succeed? Done log the proof Is the outcome unknown? Check by the key then retry only with the same key Under the attempt limit? If not, stop and alert Could waiting change the result? Retry later backoff and jitter, the same key It's permanent park it, fix it, then replay it yes no yes no yes no Decide by what you know about the outcome. Every retry carries the same key. HOW TO READ IT a step a question an answer yes goes right to an answer; no goes down to the next question a barrier: nothing crosses until it holds © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

Test knowledge. Know how to make loads and integrations safe to repeat: upsert on a unique External ID so a resent record updates instead of duplicating; give an operation that can legitimately repeat (a credit, a payment) its own unique key, kept the same on every retry. Know which failures to retry: transient ones (record locks, timeouts, server errors) with backoff; rate limits after the wait the server gives; never validation errors or malformed requests until they’re fixed.

Back to 4 p.m. A hundred status updates collide on one Manifest and fail with UNABLE_TO_LOCK_ROW. If all hundred retry immediately, we’ve sent the same traffic straight back into the same intersection. They collide again, retry again, and now we have a retry storm. Marc Brooker of Amazon put it plainly: “when failures are caused by overload, retries that increase load can make matters significantly worse.”

So a retry needs a schedule. Backoff waits before trying again, and waits longer each time. Well-Architected’s example: about 1 second, then 2, then 4, then 8, with the longest wait capped at 30 to 60 seconds. And add jitter, a little randomness, so a hundred workers that failed together don’t all wake up together; it “adds some amount of randomness to the backoff to spread the retries around in time.”

On Salesforce’s own event bus, that schedule is built in. Module 2’s subscriber can ask for its batch again by throwing EventBus.RetryableException, and “events are resent after a small delay. The delay increases in subsequent retries.” Before the retry, “the DML operations performed in the trigger before the retry are rolled back,” so a half-done recount never sticks (Retry Event Triggers with EventBus.RetryableException (opens in a new tab), Platform Events Developer Guide, checked 2026-10-31). Retry the lock, not everything:

trigger WorkOrderStatusSubscriber on Work_Order_Status__e (after insert) {
Set<Id> manifestIds = new Set<Id>();
for (Work_Order_Status__e e : Trigger.new) {
manifestIds.add((Id) e.Manifest_Id__c);
}
try {
ManifestCounts.recount(manifestIds);
} catch (Exception e) {
Boolean lockWait = e instanceof QueryException // FOR UPDATE waited too long
|| (e instanceof DmlException
&& ((DmlException) e).getDmlType(0) == StatusCode.UNABLE_TO_LOCK_ROW);
if (lockWait && EventBus.TriggerContext.currentContext().retries < 4) {
throw new EventBus.RetryableException('Manifest locked: try this batch again later');
}
// Not a lock, or retried enough: hand it to the recovery path. Don't drop it.
}
}

The recount is what makes this safe: the resent batch recounts the same Manifests and writes the same right totals. A trigger can run “up to 10 times when it’s retried (the initial run plus 9 retries),” then “moves to the error state and stops processing new events,” and Salesforce recommends fewer than nine retries. That’s the next section’s point, in Salesforce’s own numbers.

I don’t want a data architect memorizing “retry three times at these exact intervals.” That’s implementation policy. I want the architect asking: what failure are we recovering from, and what makes us believe waiting will change the outcome?

People say, “We need this to happen exactly once.” What they usually mean is, “We can’t tolerate the business action happening twice.” That’s a valid requirement. But across a network, with lost responses and retries, literal exactly-once execution is a promise nobody can keep.

The question I can design for is: how do I make repeated delivery produce the correct business result? Effectively once, not exactly once. That leads straight back to the tools in this module: a stable operation identity, idempotency, checking state before acting, deduplication, safe retries and, at the end, reconciliation that proves the books match. Those are things we can build and test.

A retry policy needs an exit. Otherwise a bad request lives forever, retrying in the background. After the allowed attempts, the work moves to a different path: an error queue or an exception record, an alert to someone who owns it, a fix to the data, then a replay. Chapter 9 showed two ways to keep that list: a send status and a retry count on the record, or on an integration log.

What it must never do is disappear. Proof of delivery still applies: a failed retry isn’t permission to forget the work. It means the work moved to the recovery path.

If the work rides on platform events, know how long you can go back for it. High-volume platform event messages “are stored for 72 hours (3 days)”; legacy standard-volume events, which Salesforce retires in Summer ‘27, only 24 hours. A subscriber that’s down longer than that can’t replay what it missed, so the recovery path can’t depend on the event bus alone.

Test knowledge. Know that a subscriber can replay high-volume platform events from the last 72 hours, and that new custom platform events are high volume by default (since API version 45.0). Beyond that window, recovery needs its own record of what was sent.

Agents Have the Same Unknown Outcome Problem

Section titled “Agents Have the Same Unknown Outcome Problem”

Agents make all of this more common. An agent decides a claim qualifies for a credit, calls an action, and the action times out. A naive agent reasons, “It didn’t work, I’ll try again.” But it doesn’t know that. It only knows it didn’t get confirmation. If the action was “search the knowledge base,” trying again is harmless. If it was “credit the partner,” we need much stronger guarantees.

The agent shouldn’t be improvising transaction recovery. I’d rather give it an established, tested action, Issue Partner Credit, that owns its own validation, authorization, idempotency key, retry behavior, logging and the exact records it may change. The agent decides when the business action is appropriate, within its guardrails. The established process carries it out safely. For any agent action with a real consequence, the action’s contract should answer: what identifies this operation? Can it run twice safely? What happens after a timeout? Which errors are retried, how long do we wait, when do we stop, and who’s told when we do? Those are architecture decisions, not prompt-writing decisions.

Beyond the exam: what AI changes. Agents retry quickly and on their own, so unknown outcomes and duplicate actions become more likely. Give agents established actions whose identity, idempotency, retry limits and write scope are fixed in the design, rather than expecting the model to reason its way out of an unknown outcome.

How do we know the retry design works? Test it, and not only the happy path. The request succeeds and the response disappears. The request fails before it’s processed. The record is locked. The same key arrives twice; the same key arrives ten times. Two workers send the same operation at the same moment. The agent retries after a timeout. The ERP is down for an hour. The recovery job restarts halfway through.

Then prove the business result, not “the API returned 200 eventually”: one credit per claim, one invoice line per delivery, the Manifest’s count matching its Work Orders. Trust, but verify.

  • Which operations are harmful to repeat, and which identity (the record’s key, or an operation key) makes each one safe.
  • Where the operation key is created, where it’s stored, and how long the receiver remembers it.
  • Which failures are retried, with what backoff and jitter, and which go straight to correction.
  • What happens after the last automatic attempt: who’s told, where the work waits, and how it’s replayed.
  • What contract an agent’s actions must meet before an agent may call them.

Back to the dented dishwasher. The credit request timed out, and the integration layer retried, exactly as it should. With the claim number as the key, the ERP saw C-48213, recognized it, and returned the first answer. One credit, $180, and nobody in finance spent the morning hunting for it.

That’s the chapter in one line. Module 1: see what a save locks. Module 2: design the collisions out. Module 3: make sure that when something fails anyway, and it will, the second attempt does no harm. Build for today, plan for tomorrow, and plan for failure.

Next: Data Security for Humans, Applications, and Agents