Draft Handling Part 3: Draft Locking, ETags and Concurrency
Master concurrency in draft scenarios: the exclusive lock during editing, lock expiry and the optimistic phase, the total ETag at activation, and what users see in each situation.
Explanation
📌 KEY POINTS
✅ Edit sets an exclusive lock on the active instance — others see "Locked by …".
✅ The exclusive lock expires after a while; the draft stays, but another user may then edit.
✅ The total ETag protects activation: if active data changed meanwhile, activation fails.
⚠️ Without total etag, a returning user could overwrite someone else's saved changes.
━━━━━━━━━━━━━━━━━━━━
▶ WHERE WE ARE
Part 2 walked through the lifecycle. This part answers the concurrency questions: who may edit while a draft exists, and what happens when a draft is old?
━━━━━━━━━━━━━━━━━━━━
▶ 1. PHASE 1 — EXCLUSIVE LOCK
When user A clicks Edit, RAP locks the active instance (lock master on the root) and keeps it locked while A works — across many requests. User B sees "Locked by A" and cannot create a draft. This is what makes long editing sessions safe.
━━━━━━━━━━━━━━━━━━━━
▶ 2. PHASE 2 — LOCK EXPIRY (OPTIMISTIC PHASE)
If A stops working, the exclusive lock expires after a configured period (the draft stays). From now on:
- B may edit the object: B's Edit creates B's own draft only if A's draft is discarded or has become invalid — Fiori shows "Unsaved changes by A" and B can decide to take over, which discards A's draft.
- If B does not take over and A returns, A's Resume re-acquires the lock.
💡 TIP: the durations for exclusive lock and draft lifetime are system settings; ask your basis/RAP team before promising users specific times.
━━━━━━━━━━━━━━━━━━━━
▶ 3. THE TOTAL ETAG
"total etag LastChangedAt" on the root is a timestamp that changes whenever anything in the BO changes. When a draft is created, RAP remembers the active value. On Resume or Activate, RAP compares:
- unchanged → fine;
- changed (someone else saved the active object meanwhile, e.g. through an API) → Resume/Activate is rejected, preventing A from overwriting newer data.
Without a total etag, RAP cannot detect this, and A's old draft could overwrite B's newer changes.
━━━━━━━━━━━━━━━━━━━━
▶ 4. ETAG MASTER VS TOTAL ETAG
- etag master (LocalLastChangedAt, per entity) — protects individual non-draft changes (APIs, direct updates) and draft instance updates.
- total etag (LastChangedAt, root) — protects draft resume/activation against changes of the whole BO.
Both fields must be maintained (automatically via @Semantics in managed RAP).
━━━━━━━━━━━━━━━━━━━━
▶ 5. NON-DRAFT CHANGES WHILE A DRAFT EXISTS
An API (non-draft projection) tries to update an object that is being edited in draft:
- while the exclusive lock is held → the API call fails with a lock error;
- after the lock expired → the API may succeed; the total ETag then makes the old draft's activation fail.
This is the correct behavior: the newer committed change wins, the old draft must be re-done.
━━━━━━━━━━━━━━━━━━━━
▶ 6. WHAT USERS SEE
- "Locked by <user>" — exclusive lock held by someone else.
- "Unsaved changes by <user>" — another user's expired draft exists; taking over discards it.
- "Data has been changed by another user" at save — total ETag mismatch.
━━━━━━━━━━━━━━━━━━━━
▶ STEP BY STEP (TEST)
- User A: Edit request 4711, change the description, do not save.
- User B: try Edit → "Locked by A".
- After the lock expires (or in test by releasing it), change 4711 via an API.
- User A: Save → activation rejected because of the total ETag.
✅ CHECKPOINT: you can explain every lock-related message in a draft app.
Code example
// ===== Declarations that make draft concurrency work =====define behavior for ZR_PurchReq alias PurchaseRequestpersistent table zpr_headdraft table zpr_head_dlock master // exclusive lock during editingtotal etag LastChangedAt // compared on Resume / Activateetag master LocalLastChangedAt // per-instance stale checkauthorization master ( global, instance ){ /* ... */ draft action Edit; draft action Resume; // re-acquire lock for an old draft draft action Activate optimized; draft action Discard; draft determine action Prepare { }} // ===== CDS: both fields maintained automatically (managed) =====// @Semantics.systemDateTime.localInstanceLastChangedAt: true// local_last_changed_at as LocalLastChangedAt, // etag master// @Semantics.systemDateTime.lastChangedAt: true// last_changed_at as LastChangedAt, // total etag // ===== Timeline =====// 09:00 A: Edit 4711 -> draft A + exclusive lock// 09:05 B: Edit 4711 -> "Locked by A"// 09:05 API: PATCH 4711 -> lock error (lock held)// ... lock expires, draft A still exists// 10:30 API: PATCH 4711 -> succeeds, LastChangedAt changes// 11:00 A: Save (Activate) -> rejected: total ETag differs -> A must reload " ===== Detect the edit conflict in EML =====" MODIFY ENTITIES OF zr_purchreq ENTITY PurchaseRequest" EXECUTE Edit FROM VALUE #( ( %key = key %param-preserve_changes = abap_true ) )" FAILED DATA(f)." f-purchaserequest[ 1 ]-%fail-cause = if_abap_behv=>cause-locked -> someone is editingReal project scenario
An integration updated purchase request statuses every hour through an API. Occasionally, a buyer's long-running draft was activated afterwards and silently reset the status set by the integration, because the BO had lock master but no total etag.
After adding "total etag LastChangedAt" with the proper @Semantics annotation, activation of such outdated drafts failed with "data has been changed". The buyer reloaded, re-applied the edit on current data, and no integration update was lost again.
Common mistakes
- Enabling draft without a total etag.
- Using the same field for etag master and total etag without understanding both roles.
- Promising users fixed lock durations without checking system settings.
- Treating "Locked by" as an error rather than protection.
- Omitting Resume, so returning users cannot continue.
- APIs that ignore lock errors instead of retrying later.
Best practices
- Always declare total etag on draft-enabled roots.
- Maintain both timestamp fields via @Semantics annotations.
- Test concurrency with two users and an API call.
- Design APIs to retry on lock errors.
- Explain the three user messages (locked, unsaved changes, data changed) to key users.
Interview angle
Questions: "How does locking work with draft?", "What is the total ETag for?", "What happens when an API changes an object that is being edited in draft?"
Strong answer: Edit sets an exclusive lock held during the draft session; after expiry the draft remains and Resume re-acquires the lock; the total ETag, remembered at draft creation, is checked at resume/activation so outdated drafts cannot overwrite newer data. An API update fails while the lock is held; after expiry it can succeed and the old draft then fails at activation.
Practice this lesson
Quick check
1. A manager's draft from Friday is activated on Monday after an API changed the same object. What prevents the old draft from overwriting the change?
2. How does the RAP draft handling framework typically ensure that only the user who created a draft can continue editing it?
3. What is the purpose of the standard 'Resume' draft action?
Interview questions
1. How do the exclusive lock and the total ETag work together in draft scenarios?
Answer it in your own words first, then compare.
2. An hourly integration sets statuses via API, but long-running drafts later reset them. Why, and how do you prevent it?
Answer it in your own words first, then compare.
Reference-verified: the steps are checked against official SAP documentation and tutorials, and object and field names in the sample code are checked for consistency. The code has not been compiled or run on an SAP system by ERPClimb. Syntax marked release-dependent varies by SAP release, so check it in your own system.