Unmanaged RAP
RAP / CDS / ODataadvanced

Unmanaged RAP Part 4: Save Sequence, Transaction Safety and Legacy APIs

Key takeaway

Implement the saver class that hands the transactional buffer to the legacy API: finalize, check before save, adjust numbers, save and cleanup — and learn the rules that keep legacy BAPIs and function modules from breaking RAP's all-or-nothing transaction.

Explanation

📌 KEY POINTS
✅ Order: finalize → check_before_save → adjust_numbers → save → cleanup.

✅ Everything that can fail must fail before save.

✅ Call legacy posting APIs IN UPDATE TASK from save.

⚠️ Never COMMIT WORK or BAPI_TRANSACTION_COMMIT in RAP code.

▶ WHERE WE ARE
After Part 3 all changes sit in lcl_buffer. Nothing is in the database. This part writes the saver — the only code that calls the legacy API to persist data. Getting it right decides whether your RAP app is transactionally safe.

━━━━━━━━━━━━━━━━━━━━
▶ THE SAVE SEQUENCE — ORDER AND PURPOSE

When the consumer commits (Save in Fiori, COMMIT ENTITIES in EML), RAP calls the saver methods in this order:

  1. finalize — last derivations before checks: assign semantic numbers, recalculate totals, set derived fields. You may still report errors here.
  2. check_before_save — the final consistency check. Call the legacy API in check or test-run mode. If anything fails, report it in FAILED and REPORTED.

→ If finalize or check_before_save reported any failure, RAP stops. It calls cleanup_finalize, nothing is saved, the buffer is kept, and the user can correct the data.

  1. adjust_numbers — only with late numbering: map preliminary IDs (%pid) to the final keys returned by the legacy API.
  2. save — the point of no return. Hand every buffered change to the legacy API's save. Errors are no longer allowed here.
  3. The framework commits (COMMIT WORK is done by RAP, not you).
  4. cleanup — clear the buffer so the next transaction starts clean.

━━━━━━━━━━━━━━━━━━━━
▶ THE GOLDEN RULE

Everything that can fail must fail in finalize or check_before_save. The save method must only write. If save hits an error, RAP cannot report it properly and the transaction is in an undefined state — this typically ends in a short dump. So:

  • Use the legacy API's test-run / check mode in check_before_save (many BAPIs have a TESTRUN parameter; custom APIs often have a CHECK method).
  • In save, call the posting routine only for data that passed checks.

RULES FOR CALLING LEGACY APIs IN SAVE
Allowed:

  • Direct database writes to your own tables.
  • Function modules called IN UPDATE TASK — they run when RAP's commit happens, inside the same LUW.
  • APIs that write through the update task internally and expect the caller to commit.

Not allowed:

  • COMMIT WORK, ROLLBACK WORK, or BAPI_TRANSACTION_COMMIT / BAPI_TRANSACTION_ROLLBACK.
  • APIs that commit internally, open dialogs or pop-ups, or use CALL TRANSACTION / SUBMIT.
  • Raising messages with MESSAGE TYPE 'E' or exceptions that are not caught.
  • Synchronous RFC calls that commit in another system — that commit is outside RAP's LUW. If the local save fails later, the remote system already has the data.

━━━━━━━━━━━━━━━━━━━━
▶ HOW TO HANDLE REMOTE OR SLOW SIDE EFFECTS

Separate them from the critical save:

  • Register them to run after commit: background RFC (bgRFC) units, the background processing framework (bgPF) on current releases, or RAP business events that a consumer processes asynchronously.
  • Only persist the business document in save; notify, replicate or print afterwards.

━━━━━━━━━━━━━━━━━━━━
▶ HOW TO INSPECT A LEGACY API BEFORE USING IT

Before wiring a BAPI or function module into save, check its source (or its documentation) for: COMMIT WORK, CALL SCREEN / POPUP_*, CALL TRANSACTION, SUBMIT, MESSAGE without RAISING, and synchronous RFC. Also check whether it keeps a global buffer that must be refreshed. If it violates the rules, wrap it: move its checks into check_before_save and its update-task posting into save, or run it in a separate asynchronous step.

━━━━━━━━━━━━━━━━━━━━
▶ ABAP CLOUD NOTE

Most legacy function modules are not released for ABAP Cloud. An unmanaged BO calling them is typically developed in Standard ABAP (classic language version). Alternatively, wrap the legacy API in a classic class, release the wrapper for use in cloud development, and call the wrapper from an ABAP Cloud BO.

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

  1. Implement finalize (assign RequestID from the legacy numbering routine) and check_before_save (legacy check per request with its items, messages mapped with the helper from Part 1).
  2. Implement save: collect creates, updates and deletes from the buffer and pass them to the legacy posting function IN UPDATE TASK.
  3. Implement cleanup (clear buffer) and cleanup_finalize (undo anything finalize set, e.g. drawn numbers if you track them).
  4. Test with EML: create a request that the legacy check rejects → COMMIT ENTITIES RESPONSE returns FAILED and the legacy message; table unchanged. Create a valid one → committed, RequestID filled.

━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU HAVE AFTER PART 4

A complete, transactionally safe unmanaged business object: legacy rules run, all-or-nothing save, correct messages. Part 5 shows the alternatives (managed with unmanaged save, additional save) and how to migrate BOPF and SEGW implementations.

Code example

ABAP Code
" ===== Saver class (local class in ZBP_R_PURCHREQ_U) =====CLASS lsc_zr_purchreq_u DEFINITION INHERITING FROM cl_abap_behavior_saver.  PROTECTED SECTION.    METHODS finalize          REDEFINITION.    METHODS check_before_save REDEFINITION.    METHODS save              REDEFINITION.    METHODS cleanup           REDEFINITION.    METHODS cleanup_finalize  REDEFINITION.ENDCLASS. CLASS lsc_zr_purchreq_u IMPLEMENTATION.   METHOD finalize.    " Last derivations - may still fail. Assign the readable number.    LOOP AT lcl_buffer=>heads ASSIGNING FIELD-SYMBOL(<head>)         WHERE change = lcl_buffer=>flag-created AND request_id IS INITIAL.      <head>-request_id = zcl_pr_legacy_api=>next_request_id( ).    ENDLOOP.  ENDMETHOD.   METHOD check_before_save.    " Run the legacy business checks in CHECK mode - nothing is written    LOOP AT lcl_buffer=>heads INTO DATA(head) WHERE change <> lcl_buffer=>flag-deleted.       DATA(messages) = zcl_pr_legacy_api=>check(        request = CORRESPONDING #( head )        items   = VALUE #( FOR i IN lcl_buffer=>items                           WHERE ( parent_uuid = head-request_uuid                                   AND change <> lcl_buffer=>flag-deleted )                           ( CORRESPONDING #( i ) ) ) ).       LOOP AT messages INTO DATA(msg) WHERE type CA 'EA'.        APPEND VALUE #( RequestUUID = head-request_uuid ) TO failed-purchaserequest.        APPEND VALUE #( RequestUUID = head-request_uuid                        %msg = new_message( id       = msg-id                                            number   = msg-number                                            severity = if_abap_behv_message=>severity-error                                            v1 = msg-message_v1 v2 = msg-message_v2                                            v3 = msg-message_v3 v4 = msg-message_v4 ) )          TO reported-purchaserequest.      ENDLOOP.    ENDLOOP.    " Any FAILED entry here -> RAP stops, calls cleanup_finalize, buffer is kept  ENDMETHOD.   METHOD save.    " Point of no return: only write, never fail, never commit    DATA creates TYPE STANDARD TABLE OF zpr_head WITH EMPTY KEY.    DATA updates TYPE STANDARD TABLE OF zpr_head WITH EMPTY KEY.    DATA deletes TYPE STANDARD TABLE OF zpr_head WITH EMPTY KEY.    DATA items   TYPE STANDARD TABLE OF zpr_item WITH EMPTY KEY.     creates = VALUE #( FOR h IN lcl_buffer=>heads WHERE ( change = lcl_buffer=>flag-created ) ( CORRESPONDING #( h ) ) ).    updates = VALUE #( FOR h IN lcl_buffer=>heads WHERE ( change = lcl_buffer=>flag-updated ) ( CORRESPONDING #( h ) ) ).    deletes = VALUE #( FOR h IN lcl_buffer=>heads WHERE ( change = lcl_buffer=>flag-deleted ) ( CORRESPONDING #( h ) ) ).    items   = VALUE #( FOR i IN lcl_buffer=>items WHERE ( change = lcl_buffer=>flag-created ) ( CORRESPONDING #( i ) ) ).     IF creates IS INITIAL AND updates IS INITIAL AND deletes IS INITIAL AND items IS INITIAL.      RETURN.    ENDIF.     " Legacy posting runs when RAP commits - inside the same LUW    CALL FUNCTION 'Z_PR_POST_REQUESTS' IN UPDATE TASK      EXPORTING        it_create       = creates        it_update       = updates        it_delete       = deletes        it_items_create = items.    " NO COMMIT WORK, NO BAPI_TRANSACTION_COMMIT - the framework commits  ENDMETHOD.   METHOD cleanup.    lcl_buffer=>clear( ).          " after a successful save  ENDMETHOD.   METHOD cleanup_finalize.    " finalize/check failed: undo what finalize derived, KEEP user changes    LOOP AT lcl_buffer=>heads ASSIGNING FIELD-SYMBOL(<head>)         WHERE change = lcl_buffer=>flag-created.      CLEAR <head>-request_id.    ENDLOOP.  ENDMETHOD. ENDCLASS. " ===== Late numbering variant (when the legacy API returns the real key) =====" BDEF: late numbering   instead of   early numbering" METHODS adjust_numbers REDEFINITION." METHOD adjust_numbers."   LOOP AT lcl_buffer=>heads ... WHERE change = 'C'."     APPEND VALUE #( %pid = <preliminary id>  RequestID = <final number> )"       TO mapped-purchaserequest."   ENDLOOP." ENDMETHOD.

Real project scenario

An unmanaged wrapper called a custom "create requisition" function module in the save method. The function module had been written for a classic screen and ended with COMMIT WORK. In testing everything looked fine. In production, a user saved a request with three items, and a database error on the third item occurred after the function's internal commit. The header and two items were already committed, the third was not, and RAP could not roll back.
A second issue: the same function sent an RFC to the warehouse system synchronously, so a failure later in the local save left orphan reservations in the warehouse.

The fix: the business checks from the function were exposed as a check method and called in check_before_save; the posting part was split into an update-task function module without COMMIT WORK and called in save; the warehouse notification was registered as a bgRFC unit that only runs after RAP's commit. Partial saves and orphan reservations stopped.

Common mistakes

  • Doing checks in save — errors there cannot be reported and usually cause a dump.
  • Calling legacy code that contains COMMIT WORK, BAPI_TRANSACTION_COMMIT, pop-ups, CALL TRANSACTION or SUBMIT.
  • Synchronous RFC calls in save that commit in the remote system, outside RAP's transaction.
  • Forgetting to clear the buffer in cleanup, leaking data into the next save in the same session.
  • Clearing the buffer in cleanup_finalize — the user loses their changes after a validation error.
  • Sending records that were created and deleted in the same transaction to the legacy delete.
  • Assigning numbers in check_before_save; numbering belongs in finalize or adjust_numbers.
  • Mapping all legacy errors to one instance instead of the instance they belong to.

Best practices

  • Everything that can fail: finalize or check_before_save. save only writes.
  • Prefer update-task function modules or direct writes to your own tables in save.
  • Inspect every legacy API for commits, dialogs and synchronous RFC before using it.
  • Move remote and slow side effects after the commit (bgRFC, bgPF, business events).
  • Keep original message IDs and numbers when reporting legacy errors.
  • Use late numbering with adjust_numbers when only the legacy save knows the final number.
  • Test the failure path explicitly: a rejected save must leave the database unchanged and keep the buffer.

Interview angle

Very common senior question: "Explain the RAP save sequence." Name the methods in order — finalize, check_before_save, adjust_numbers, save, cleanup (and cleanup_finalize on failure) — and say what each is for. Then state the key rule: after check_before_save the transaction is past the point of no return, so save must not fail.
Follow-ups: "How do you use a BAPI in RAP?" (check mode in check_before_save, posting in save, never BAPI_TRANSACTION_COMMIT, verify no internal commit), "How do you handle RFC calls?" (not synchronously in save; use bgRFC/bgPF or events after commit), "When do you need adjust_numbers?" (late numbering, final key known only at save). A concrete story of a partial save caused by a hidden COMMIT WORK is a strong answer.

Practice this lesson

Quick check

1. Where should an unmanaged BO run the legacy API's business checks so that errors can still be reported to the user?

2. Which statement is allowed in the save method of an unmanaged RAP saver class?

3. Which situation is the strongest reason to implement a RAP business object as unmanaged?

Interview questions

1. Explain the RAP save sequence in an unmanaged business object: which saver methods run, in which order, and where is the point of no return?

Answer it in your own words first, then compare.

2. A legacy function module you must use in RAP ends with COMMIT WORK and also sends a synchronous RFC to a warehouse system. How do you integrate it safely?

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