Behavior Implementations
RAP / CDS / ODataintermediate

Behavior Implementations Part 2: Determinations and Validations Without Side Effects

Key takeaway

Write determinations and validations that are safe to run any number of times: idempotent logic, the right triggers, set-based EML in local mode, no side effects outside the BO, and messages that disappear once fixed.

Explanation

📌 KEY POINTS
✅ Determinations recompute — never accumulate.

✅ Validations only read and report (with %state_area and %element).

✅ Read external data once for all keys.

⚠️ No commits, mails or remote calls in determinations or validations.

▶ WHERE WE ARE
Part 1 showed the structure of the behavior pool. Determinations and validations are the most frequently written methods — and the easiest to get subtly wrong.

WHAT "WITHOUT SIDE EFFECTS" MEANS
RAP decides when these methods run, and they can run more than once: several triggers in one request, draft Prepare, then save again. Therefore:

  • Determinations must be idempotent: running twice gives the same result as running once.
  • Validations must only read and report; they never change data.
  • Neither may cause effects outside the BO: no database writes, no RFC or HTTP calls, no e-mails, no COMMIT. Such effects belong in the save phase or after the commit.

━━━━━━━━━━━━━━━━━━━━
▶ THE DETERMINATION PATTERN

  1. READ ENTITIES … IN LOCAL MODE for all keys, only the fields you need.
  2. Filter out instances that need no change (e.g. status already set) — this is what makes it idempotent.
  3. One MODIFY ENTITIES … IN LOCAL MODE for the remaining instances.
  4. Pass on messages from the MODIFY into reported if needed.

Avoid determinations that modify the fields they are triggered by; they can trigger themselves again. Keep triggers narrow ("field Quantity, Price" rather than all fields).

━━━━━━━━━━━━━━━━━━━━
▶ ON MODIFY VS ON SAVE — CHOOSE DELIBERATELY

  • on modify — the user must see the value immediately (default status, calculated totals in the draft UI).
  • on save — the value is needed only when the data is persisted, or must be assigned once (document numbers), or is expensive.

━━━━━━━━━━━━━━━━━━━━
▶ THE VALIDATION PATTERN

  1. READ ENTITIES … IN LOCAL MODE for all keys.
  2. For each instance: first append an empty message with the validation's %state_area to reported — this clears messages from earlier runs (important with draft).
  3. If the rule is violated: append the instance to failed and a message to reported with the same %state_area and %element for the field.
  4. For validations on children, report on the child entity (failed-item, reported-item) and, if helpful, add %path to the parent so Fiori shows it in context.

━━━━━━━━━━━━━━━━━━━━
▶ READING WHAT YOU NEED

  • FIELDS ( A B ) instead of ALL FIELDS reduces work.
  • To validate against other data (e.g. supplier blocked), read that data once for all keys: a SELECT from a released CDS view with FOR ALL ENTRIES (after checking the driver table is not empty) or a join — never inside the loop.
  • Read related entities via association: READ ENTITIES … BY \_Item.

━━━━━━━━━━━━━━━━━━━━
▶ STEP BY STEP

  1. For every rule: determination or validation? on modify or on save? Which trigger fields?
  2. Implement with the patterns above.
  3. Test idempotency: run the same EML change twice and check that nothing changes the second time.
  4. Test draft: fix an error and check the message disappears.

━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU HAVE AFTER PART 2

Reliable automatic logic. Part 3 covers actions and user-facing messages.

Code example

ABAP Code
" ===== Determination: idempotent default =====METHOD setInitialStatus.  READ ENTITIES OF zr_purchreq IN LOCAL MODE    ENTITY PurchaseRequest FIELDS ( Status ) WITH CORRESPONDING #( keys )    RESULT DATA(requests).   DELETE requests WHERE Status IS NOT INITIAL.       " idempotent: only if still empty  CHECK requests IS NOT INITIAL.   MODIFY ENTITIES OF zr_purchreq IN LOCAL MODE    ENTITY PurchaseRequest UPDATE FIELDS ( Status )    WITH VALUE #( FOR r IN requests ( %tky = r-%tky  Status = 'N' ) )    REPORTED DATA(modify_reported).   reported = CORRESPONDING #( DEEP modify_reported ).ENDMETHOD. " ===== Validation: supplier must not be blocked (reads external data once) =====METHOD validateSupplier.  READ ENTITIES OF zr_purchreq IN LOCAL MODE    ENTITY PurchaseRequest FIELDS ( Supplier ) WITH CORRESPONDING #( keys )    RESULT DATA(requests).   " one read for all suppliers - never inside the loop  DATA(suppliers) = VALUE string_table( FOR r IN requests WHERE ( Supplier IS NOT INITIAL ) ( CONV #( r-Supplier ) ) ).  IF suppliers IS NOT INITIAL.    SELECT Supplier, PostingIsBlocked      FROM i_supplier      FOR ALL ENTRIES IN @suppliers      WHERE Supplier = @suppliers-table_line      INTO TABLE @DATA(supplier_data).  ENDIF.   LOOP AT requests INTO DATA(request).    " 1. clear old messages of this validation    APPEND VALUE #( %tky = request-%tky  %state_area = 'VALIDATE_SUPPLIER' )      TO reported-purchaserequest.     " 2. check the rule    DATA(sup) = VALUE #( supplier_data[ Supplier = request-Supplier ] OPTIONAL ).    IF request-Supplier IS INITIAL OR sup-PostingIsBlocked = abap_true.      APPEND VALUE #( %tky = request-%tky ) TO failed-purchaserequest.      APPEND VALUE #( %tky        = request-%tky                      %state_area = 'VALIDATE_SUPPLIER'                      %msg        = new_message_with_text(                                      severity = if_abap_behv_message=>severity-error                                      text     = |Supplier { request-Supplier } is missing or blocked| )                      %element-Supplier = if_abap_behv=>mk-on )        TO reported-purchaserequest.    ENDIF.  ENDLOOP.ENDMETHOD. " ===== WRONG patterns =====" determination that ADDS instead of SETS:  TotalAmount = TotalAmount + x   (not idempotent)" validation that fixes data:               MODIFY ENTITIES ... in a validation" side effect:                              CALL FUNCTION 'Z_SEND_MAIL' in a determination" per-row DB access:                        SELECT SINGLE inside LOOP AT requests

Real project scenario

Users reported that some purchase requests showed doubled totals after editing. The determination for the total did "TotalAmount = TotalAmount + item amount" for the changed item. With draft, the determination ran several times (on item change, then again in Prepare), so the amount was added more than once.
The fix made it idempotent: always read all items of the request and recompute the total from scratch. In the same review, the team found a validation that "corrected" an empty currency by setting a default — it was moved into a determination, because validations must not change data. A regression test now runs every determination twice and compares results.

Common mistakes

  • Non-idempotent determinations (incrementing instead of recomputing).
  • Determinations triggered by the fields they change, causing loops.
  • Validations that modify data.
  • External calls (mail, RFC, HTTP) or commits in determinations or validations.
  • Not clearing messages via %state_area, so fixed errors stay visible in draft.
  • SELECT SINGLE inside the loop over instances.
  • FOR ALL ENTRIES without checking that the driver table is not empty.

Best practices

  • Read all needed data once, process the set, modify once.
  • Make determinations recompute, not accumulate.
  • Keep triggers narrow and deliberate (on modify vs on save).
  • Validations: clear with %state_area, then report with %element.
  • Read external data from released CDS views, once for all keys.
  • Test idempotency and draft message behavior explicitly.

Interview angle

Questions: "Why must determinations be idempotent?", "Can a validation change data?", "How do you clear validation messages in draft?"
Strong answer: RAP can run determinations several times per transaction, so they must recompute rather than accumulate and should skip instances that are already correct; validations only read and report failed/reported; neither may cause external side effects. %state_area with an initial empty message clears old messages. Mention set-based reads of external data and narrow triggers.

Practice this lesson

Quick check

1. A validation checks suppliers with SELECT SINGLE inside a loop over all instances. What is the recommended fix?

2. A developer needs to ensure that a customer order cannot be saved if its total amount is negative. Which behavior implementation construct is the correct choice for this requirement?

3. In a RAP behavior definition, a determination is defined with the trigger point 'ON SAVE'. What is the key characteristic of this trigger point compared to 'ON MODIFY'?

Interview questions

1. What rules must determinations and validations follow, and what must they never do?

Answer it in your own words first, then compare.

2. A purchase request total is sometimes doubled after editing in draft. The determination adds the changed item's amount to the total. What is wrong and how do you fix it?

Answer it in your own words first, then compare.

Practice more with Interview AI →

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

Interview questions on this topic

Related SAP topics