OData V4 Part 6: ETags and Optimistic Concurrency
Understand optimistic concurrency in OData V4 with RAP: how ETags are delivered, how clients send If-Match, what 412 and 428 mean, and how RAP etag master fields drive the whole mechanism.
Explanation
📌 KEY POINTS
✅ The server sends the ETag with every read (@odata.etag / ETag header).
✅ The client sends it back in If-Match on PATCH, DELETE and actions.
✅ Mismatch → 412 Precondition Failed; missing If-Match where required → 428.
⚠️ If-Match: * disables the check — never use it in business clients.
━━━━━━━━━━━━━━━━━━━━
▶ WHERE WE ARE
Parts 1–5 covered calls and errors. This last part explains one mechanism that every write client must implement: ETags. (RAP Business Objects Part 4 explains the BO side.)
━━━━━━━━━━━━━━━━━━━━
▶ 1. THE PROBLEM
Two users read the same request at 9:00. User A changes the amount at 9:05. User B changes the description at 9:10 based on the 9:00 data. Without protection, B's write silently wins — and B never saw A's change. Locks cannot help: the two changes were not simultaneous.
━━━━━━━━━━━━━━━━━━━━
▶ 2. HOW ETAGS SOLVE IT
- In RAP, "etag master LocalLastChangedAt" defines the version field. Its value changes on every change.
- When a client reads an entity, OData returns the version as "@odata.etag" in the JSON (and as ETag header for single entities), e.g. W/"20260924090000.1234560".
- On PATCH/DELETE/action, the client sends If-Match: W/"20260924090000.1234560".
- RAP compares it with the current value:
– equal → change allowed;
– different → 412 Precondition Failed. The client must re-read and let the user decide.
━━━━━━━━━━━━━━━━━━━━
▶ 3. 428 PRECONDITION REQUIRED
If an ETag is defined and the client sends no If-Match, the service can reject the request with 428. This forces clients to take part in concurrency control.\n\n━━━━━━━━━━━━━━━━━━━━\n▶ 4. IF-MATCH: *\n"*" means "any version". It bypasses the check and brings back lost updates. Use it only for tools that intentionally overwrite (e.g. an admin repair job) — never in normal clients.
━━━━━━━━━━━━━━━━━━━━
▶ 5. ETAGS AND DRAFT
- Draft instances have their own ETag handling; Fiori Elements manages it automatically.
- The total etag protects draft activation against changes of the active data (Draft Handling Part 3).
- For custom UI5 apps using the OData V4 model, ETags are handled by the model — you mainly need to handle 412 messages.
━━━━━━━━━━━━━━━━━━━━
▶ 6. WHAT CLIENTS MUST DO
- Read → store @odata.etag with the data.
- Write → send If-Match with the stored value.
- On 412 → reload, show the current data, let the user re-apply.
- After a successful write → use the new ETag from the response for the next write.
━━━━━━━━━━━━━━━━━━━━
▶ 7. BACKEND PREREQUISITES
- etag master field on each entity that can be changed individually.
- The field is updated on every change (managed: @Semantics.systemDateTime.localInstanceLastChangedAt; unmanaged: set in code and return it from read).
━━━━━━━━━━━━━━━━━━━━
▶ STEP BY STEP
- GET a request and note @odata.etag.
- PATCH it with If-Match → 200, new ETag.
- PATCH again with the OLD ETag → 412.
- PATCH without If-Match → 428 (if required).
✅ CHECKPOINT: you can implement ETag handling in any client. Next topic: ABAP Cloud.
Code example
### 1. Read: server delivers the versionGET {root}/PurchaseRequest(RequestUUID=4c2a…,IsActiveEntity=true)# 200 OK# ETag: W/"20260924090000.1234560"# { "@odata.etag": "W/\"20260924090000.1234560\"", "RequestID": "4711",# "Description": "Office chairs", ... } ### 2. Write with If-Match (current version) -> success, new ETagPATCH {root}/PurchaseRequest(RequestUUID=4c2a…,IsActiveEntity=true)Content-Type: application/jsonX-CSRF-Token: <token>If-Match: W/"20260924090000.1234560" { "Description": "Ergonomic office chairs" }# 200 OK ETag: W/"20260924091000.5550000" ### 3. Write with an OUTDATED ETag -> conflict detectedPATCH {root}/PurchaseRequest(RequestUUID=4c2a…,IsActiveEntity=true)If-Match: W/"20260924090000.1234560"{ "Description": "Chairs (old screen)" }# 412 Precondition Failed -> client must re-read ### 4. Write without If-Match where required# 428 Precondition Required ### 5. NEVER in business clients# If-Match: * -> skips the check, lost updates return // ===== Backend: the field behind the ETag (managed RAP) =====// BDEF: etag master LocalLastChangedAt// CDS : @Semantics.systemDateTime.localInstanceLastChangedAt: true// local_last_changed_at as LocalLastChangedAt,Real project scenario
A custom mobile app for approvers used If-Match: * because "412 errors were annoying". Two approvers occasionally worked on the same request; the second approval overwrote a budget correction made minutes earlier, and a request was approved with the old amount.
The app was changed to store @odata.etag, send it with every write and handle 412 by reloading and showing "This request was changed — please review". Lost updates disappeared, and auditors accepted the process because every approval was now based on current data.
Common mistakes
- Clients ignoring ETags or using If-Match: *.
- Not updating the stored ETag after a successful write.
- Treating 412 as a technical error instead of a business conflict.
- Backend etag field not changing on every change (missing annotation, unmanaged code).
- Confusing 412 (mismatch) with 428 (missing header).
Best practices
- Define etag master on every independently changeable entity.
- Clients: store ETag on read, send If-Match on write, handle 412 with reload.
- Require If-Match for APIs (428) so no client skips the check.
- Use the OData V4 model in UI5 to handle ETags automatically.
- Test the conflict path explicitly.
Interview angle
Questions: "How does optimistic concurrency work in OData?", "What do 412 and 428 mean?", "Where does the ETag come from in RAP?"
Strong answer: the server returns the ETag (from the RAP etag master field) with each read; clients send it in If-Match; a mismatch yields 412 and the client must re-read; a missing If-Match where required yields 428; If-Match: * bypasses the check. In managed RAP the field is maintained via @Semantics.systemDateTime.localInstanceLastChangedAt; draft adds the total etag for activation.
Practice this lesson
Quick check
1. A mobile client receives "412 Precondition Failed" on PATCH. What does it mean?
2. Two buyers edit the same request via an API. Which client behavior prevents lost updates?
3. Which response indicates that a required If-Match header was not sent?
Interview questions
1. How does optimistic concurrency with ETags work in a RAP OData V4 service?
Answer it in your own words first, then compare.
2. A mobile approval app sometimes overwrites budget corrections made minutes earlier. What is the cause and fix?
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
- OData V4 Part 1: The OData V4 Contract — Metadata, Keys, Navigation, Queries
- OData V4 Part 2: Publishing a RAP Service with an OData V4 Binding
- OData V4 Part 3: Test CRUD Requests, Payloads and Responses
- OData V4 Part 4: Actions, Functions and Batch Processing
- OData V4 Part 5: Troubleshooting HTTP Errors, Metadata and Payloads