RAP Business Objects Part 4: Locking, ETags and Concurrency Control
Understand how RAP protects data from conflicting changes: exclusive locks, ETags for stale data, the total ETag with draft, and how responsibilities differ between managed and unmanaged objects.
Explanation
📌 KEY POINTS
✅ Lock = no simultaneous edits (pessimistic).
✅ ETag = no saving outdated data (optimistic, HTTP 412).
✅ Total ETag = protects draft activation.
⚠️ Unmanaged code must update the ETag field itself.
▶ WHERE WE ARE
Part 3 explained the transaction phases. Several users and systems work on the same data at the same time; this part explains the three mechanisms that keep their changes from destroying each other. Managed RAP Part 2 shows the implementation for the Purchase Request.
━━━━━━━━━━━━━━━━━━━━
▶ THREE PROBLEMS, THREE MECHANISMS
- Two transactions change the same instance at the same moment → LOCK.
- A user changes data based on an outdated screen → ETAG.
- A draft is edited for a long time while the active data might change → TOTAL ETAG plus the draft's exclusive lock.
━━━━━━━━━━━━━━━━━━━━
▶ 1. LOCKS (PESSIMISTIC)
- Declared with "lock master" on the root and "lock dependent by _Parent" on children.
- Before an instance is changed, RAP sets an exclusive lock on the root key. Changing an item locks its whole BO.
- A second transaction trying to change the same BO gets an error saying who holds the lock.
- Locks are released at the end of the transaction — without draft that is seconds.
- Managed: the framework locks. Unmanaged: you implement the lock method with a lock object (Unmanaged RAP Part 1).
━━━━━━━━━━━━━━━━━━━━
▶ 2. ETAGS (OPTIMISTIC)
- Declared with "etag master <field>", e.g. LocalLastChangedAt, on each entity that can be changed on its own.
- The consumer receives the current ETag value with every read (OData ETag header). When it sends a change, it includes the value (If-Match). If the stored value is different, RAP rejects the change — OData returns HTTP 412 and the user must reload.
- The ETag field must change on every change. Managed RAP does this automatically for fields annotated with @Semantics.systemDateTime.localInstanceLastChangedAt. In unmanaged RAP you set it yourself in every update, and your read method must return the buffered value.
- Children can have their own etag master so that changing one item does not invalidate the others, or use "etag dependent by _Parent".
━━━━━━━━━━━━━━━━━━━━
▶ 3. DRAFT, EXCLUSIVE LOCK AND TOTAL ETAG
With draft enabled:
- When a user starts editing an active instance (Edit action), RAP creates a draft and takes an exclusive lock on the active instance for the whole draft session. Other users see "locked by" and cannot edit.
- If the lock expires (the user leaves for a long time), the draft still exists; the Resume action re-acquires the lock.
- "total etag <field>" on the root (usually LastChangedAt, a timestamp changed on any change of the whole BO) is checked when the draft is activated: if the active data changed in the meantime, activation is rejected instead of overwriting.
━━━━━━━━━━━━━━━━━━━━
▶ LOCK VS ETAG IN ONE SENTENCE
Locks stop two people editing at the same time; ETags stop someone saving an old version later.
━━━━━━━━━━━━━━━━━━━━
▶ TYPICAL DESIGN
- Root: lock master, etag master LocalLastChangedAt, total etag LastChangedAt (if draft).
- Children: lock dependent by _Parent, own etag master LocalLastChangedAt.
- Admin fields maintained automatically via @Semantics annotations (managed) or by your code (unmanaged).
━━━━━━━━━━━━━━━━━━━━
▶ STEP BY STEP (TEST)
- Open the same request in two browsers. Edit in the first — the second gets "locked by" when trying to edit (draft lock).
- Without draft: read an instance with EML, change it in a second session and commit, then try to update with the old ETag from the first session — the update fails.
- Check that LocalLastChangedAt changes on every save.
━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU HAVE AFTER PART 4
A clear model of RAP concurrency. Part 5 covers the remaining controls: who may do what, when it is allowed, and how messages reach the user.
Code example
// ===== Concurrency declarations for a draft-enabled BO =====define behavior for ZR_PurchReq alias PurchaseRequestpersistent table zpr_headdraft table zpr_head_dlock master // exclusive lock on the root keytotal etag LastChangedAt // checked when a draft is activatedetag master LocalLastChangedAt // stale-data check for direct changesauthorization master ( global, instance ){ /* ... */ } define behavior for ZR_PurchReqItem alias Itempersistent table zpr_itemdraft table zpr_item_dlock dependent by _Request // item change locks the whole requestetag master LocalLastChangedAt // items versioned individuallyauthorization dependent by _Request{ /* ... */ } // ===== CDS: fields that make ETags work 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 // ===== OData view of the same mechanism =====// GET .../PurchaseRequest(RequestUUID=...,IsActiveEntity=true)// <- ETag: W/"20260924101500.1234560"// PATCH same URL, If-Match: W/"20260924101500.1234560"// -> 200 if unchanged// -> 412 Precondition Failed if someone saved in between // ===== Unmanaged: YOU must keep the ETag moving =====// GET TIME STAMP FIELD row-local_last_changed_at. " in every update handler// ...and return LocalLastChangedAt from the read method (buffer first)Real project scenario
An internal approval app had lock master but no ETag. Two approvers regularly opened the same list in the morning. One changed an amount at 9:05; the other approved at 9:20 from the screen loaded at 9:00 — the approval was recorded against an amount the second approver had never seen.
Locks could not help, because the two changes were not simultaneous. Adding etag master LocalLastChangedAt (with the correct @Semantics annotation) made the second approval fail with a "data has changed" message and a reload. Auditors accepted the fix because every approval was now guaranteed to be based on the current data.
Common mistakes
- Thinking locks alone prevent stale overwrites.
- An ETag field that does not change on every change (missing annotation, or not set in unmanaged code).
- Setting the ETag field manually in managed RAP.
- Enabling draft without a total etag.
- Unmanaged read methods that return the database ETag instead of the buffered one.
- Clients (custom UIs, integrations) that ignore 412 instead of reloading.
Best practices
- Always combine lock master with an etag master.
- Use framework-maintained timestamp fields for ETags in managed RAP.
- Add total etag on the root whenever draft is enabled.
- Give children their own etag master when they are edited independently.
- Treat HTTP 412 as normal protection and design clients to reload.
- Test concurrency explicitly with two sessions before go-live.
Interview angle
Questions: "Lock vs ETag in RAP?", "What is the total ETag?", "How does draft affect locking?"
Strong answer: locks are pessimistic and prevent simultaneous changes; ETags are optimistic and reject changes based on outdated data (HTTP 412). With draft, an exclusive lock is held on the active instance for the draft session and the total ETag is checked at activation. In managed RAP the ETag field is maintained automatically via @Semantics; in unmanaged RAP you must set it and return it from the read method.
Practice this lesson
Quick check
1. User A saves an order from a screen loaded before user B's change. Which mechanism rejects A's save?
2. With draft enabled, what does the total ETag on the root entity protect?
3. An action calls a legacy function that ends with COMMIT WORK. Which problem does this cause?
Interview questions
1. Explain locks, ETags and the total ETag in RAP. Which problem does each one solve?
Answer it in your own words first, then compare.
2. Users sometimes get 'data has been changed by another user' immediately after their own change in an unmanaged BO. What do you investigate?
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.
More lessons in this topic
- RAP Business Objects Part 1: Designing the BO — Root, Children and Boundaries
- RAP Business Objects Part 2: Managed vs Unmanaged — A Decision Framework
- RAP Business Objects Part 3: Transactional Buffer, Save Sequence and the RAP LUW
- RAP Business Objects Part 5: Authorization, Feature Control and Messages