Unmanaged RAP
RAP / CDS / ODataintermediate

Unmanaged RAP Part 1: When to Use It — Locks, Numbering and Messages

Key takeaway

Understand when unmanaged RAP is the right choice, exactly which responsibilities move from the framework to you, and implement the first three: locking, key numbering and translating legacy errors into RAP messages.

What this complete series covers

Rebuild the same Purchase Request on top of an existing legacy API with unmanaged RAP: locks, numbering, messages, the transactional buffer, PATCH-safe handlers, the save sequence — and when a lighter option is the better choice.

Who it is for
Best for SAP consultants, developers, and learners who want a practical understanding of Unmanaged RAP.
Prerequisites
Complete the Managed RAP Tutorial first (at least Parts 1–3). Comfort with ABAP OO, internal tables and function modules.
Final outcome
You will be able to decide between managed, additional save, unmanaged save and unmanaged; build a transactionally safe unmanaged BO around legacy code; and plan a BOPF or SEGW migration.
  1. Part 1: Unmanaged RAP Part 1: When to Use It — Locks, Numbering and Messages

    Understand when unmanaged RAP is the right choice, exactly which responsibilities move from the framework to you, and implement the first three: locking, key numbering and translating legacy errors into RAP messages.

  2. Part 2: Unmanaged RAP Part 2: Architecture — CDS, BDEF and Transactional Buffer

    Set up the complete unmanaged RAP architecture for a brownfield application: CDS views for queries, the full unmanaged behavior definition, the behavior pool layout, and the transactional buffer class you own.

  3. Part 3: Unmanaged RAP Part 3: Create, Update, Delete and Read Handlers

    Implement the unmanaged handler methods correctly: create into the buffer, partial updates with %control, delete including the create-then-delete case, a buffer-aware read method, and create-by-association for items.

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

    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.

  5. Part 5: Unmanaged RAP Part 5: Choosing the Implementation Type and Migrating BOPF/SEGW

    Compare the four RAP implementation types — managed, managed with additional save, managed with unmanaged save and unmanaged — choose correctly, and follow a safe step-by-step path for moving BOPF objects and SEGW services to RAP.

System and release assumptions: Examples target recent SAP S/4HANA / ABAP Cloud capabilities. RAP, CDS, OData, and Fiori Elements syntax and tooling can differ by ABAP platform release.

Explanation

📌 KEY POINTS
✅ Unmanaged = you implement buffer, CRUD, read, lock, numbering and saver.

✅ Use it only when legacy logic must control editing; otherwise prefer unmanaged save.

✅ Lock method uses an enqueue lock object on the root key.

⚠️ Every rejection needs FAILED + REPORTED with a clear message.

▶ BEFORE YOU START
This series assumes you know the managed flow from the Managed RAP series (Parts 1–3): BDEF, handler methods, EML, FAILED/REPORTED/MAPPED. We reuse the same Purchase Request example so you can compare both approaches line by line. The difference: here an existing application layer (a legacy function group or class, called "the legacy API" below) already creates, changes and deletes purchase requests, with years of business rules inside it. We must not bypass it.

━━━━━━━━━━━━━━━━━━━━
▶ WHEN TO CHOOSE UNMANAGED

Managed RAP assumes the framework can write your tables directly. That is wrong when:

  • An existing API must stay the single owner of the data — its checks, change documents, follow-on postings and user exits must run.
  • The data is not in simple tables you own (e.g. it lives behind a BAPI, in several systems, or needs complex persistence logic).
  • You are wrapping a brownfield application with a RAP service, without rewriting it.

If you only need extra work at save time (a change log, an extra table, an event), you probably do not need full unmanaged — see "managed with additional save" and "managed with unmanaged save" in Part 5.
Rule: greenfield with your own tables → managed. Existing business logic that must remain authoritative → unmanaged.

━━━━━━━━━━━━━━━━━━━━
▶ WHAT MOVES FROM THE FRAMEWORK TO YOU

In managed RAP the framework implements it; in unmanaged RAP you implement it:

  • Transactional buffer — you keep created/changed/deleted instances in your own buffer class until save.
  • Create, update, delete, read, create-by-association, read-by-association — handler methods (Part 3).
  • Locking — the lock method, using a lock object.
  • Numbering — assigning keys to new instances.
  • Save sequence — the saver class: finalize, check_before_save, save, cleanup (Part 4).
  • Translating the legacy API's messages into RAP messages.

What RAP still gives you: the BDEF contract, EML, the service and OData layer, authorization and feature-control hooks, ETag checks (using your read method), draft support, and the orchestration of phases. You still must not call COMMIT WORK — the framework still owns the LUW.

━━━━━━━━━━━━━━━━━━━━
▶ 1. LOCKING

With "lock master" in an unmanaged BDEF, RAP calls your lock method before any change. You set an exclusive lock with a lock object (create lock object EZPR_HEAD on table ZPR_HEAD in ADT or SE11). Use cl_abap_lock_object_factory — it works in both classic and ABAP Cloud development.

If the lock is held by someone else, report the instance in FAILED and add a message naming the user. RAP releases the locks automatically at the end of the transaction — you do not dequeue manually.

If the legacy API sets its own locks, reuse the same lock object so both old transactions and the new RAP app block each other correctly.

━━━━━━━━━━━━━━━━━━━━
▶ 2. NUMBERING

Options, same as managed, but you do more of the work:

  • Early numbering — declare "early numbering" and implement earlynumbering_create. You assign the key in the interaction phase and return %cid → key in MAPPED. Use this for UUIDs, or when the legacy API can give you a number immediately.
  • Late numbering — declare "late numbering" and assign the final key in the saver's adjust_numbers method during save. Use it when the legacy API only returns the real document number when it saves (typical for BAPIs creating documents). Until then RAP works with a preliminary ID (%pid).

In this series the technical key is a UUID assigned by early numbering; the human-readable RequestID is returned by the legacy API at save time.

━━━━━━━━━━━━━━━━━━━━
▶ 3. MESSAGES — TRANSLATING LEGACY ERRORS

Legacy APIs return messages as BAPIRET2 tables or raise exceptions. RAP needs them per instance in REPORTED, and needs failed instances in FAILED. So you map:

  • Each message of type E or A → FAILED entry for that instance + error message in REPORTED.
  • W → warning, I → information, S → success message in REPORTED only.
  • Use new_message( id number severity v1–v4 ) to keep the original message class and number — users and support see the same message text as in the old transaction.

Never collapse all legacy messages into one generic "save failed" text; the per-instance, per-field detail is what makes RAP UIs usable.

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

  1. Create lock object EZPR_HEAD (primary key REQUEST_UUID) for table ZPR_HEAD.
  2. Create the BDEF with "unmanaged implementation in class zbp_r_purchreq_u unique;" (full BDEF in Part 2).
  3. Implement lock and earlynumbering_create as in the code example.
  4. Create a reusable helper method that converts a BAPIRET2 table into REPORTED/FAILED; you will use it in Parts 3 and 4.

━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU HAVE AFTER PART 1

The three responsibilities that are easiest to forget in unmanaged RAP — locks, keys and messages — are handled. Part 2 sets up the complete architecture: CDS views, full BDEF and the buffer class.

Code example

ABAP Code
// ===== BDEF header (unmanaged) =====unmanaged implementation in class zbp_r_purchreq_u unique;strict ( 2 ); define behavior for ZR_PurchReqU alias PurchaseRequestlock masterauthorization master ( global, instance )etag master LocalLastChangedAtearly numbering{  create; update; delete;  association _Item { create; }  field ( readonly ) RequestUUID, RequestID, LocalLastChangedAt;} " ===== Lock method =====  METHOD lock.    TRY.        DATA(lock) = cl_abap_lock_object_factory=>get_instance( iv_name = 'EZPR_HEAD' ).      CATCH cx_abap_lock_failure INTO DATA(lock_error).        RAISE SHORTDUMP lock_error.   " lock object missing = technical error    ENDTRY.     LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>).      TRY.          lock->enqueue( it_parameter = VALUE #(                           ( name = 'REQUEST_UUID' value = REF #( <key>-RequestUUID ) ) ) ).        CATCH cx_abap_foreign_lock INTO DATA(foreign_lock).          APPEND VALUE #( %tky = <key>-%tky ) TO failed-purchaserequest.          APPEND VALUE #( %tky = <key>-%tky                          %msg = new_message_with_text(                                   severity = if_abap_behv_message=>severity-error                                   text     = |Request is being edited by { foreign_lock->user_name }| ) )            TO reported-purchaserequest.        CATCH cx_abap_lock_failure INTO lock_error.          RAISE SHORTDUMP lock_error.      ENDTRY.    ENDLOOP.    " No dequeue: RAP releases locks at the end of the transaction  ENDMETHOD. " ===== Early numbering: assign UUID keys to new instances =====  METHOD earlynumbering_create.    LOOP AT entities INTO DATA(entity).      TRY.          APPEND VALUE #( %cid        = entity-%cid                          RequestUUID = cl_system_uuid=>create_uuid_x16_static( ) )            TO mapped-purchaserequest.        CATCH cx_uuid_error.          APPEND VALUE #( %cid = entity-%cid ) TO failed-purchaserequest.      ENDTRY.    ENDLOOP.  ENDMETHOD. " ===== Helper: legacy BAPIRET2 messages -> RAP FAILED / REPORTED =====  METHOD map_messages.   " IMPORTING tky, return TYPE bapiret2_t; CHANGING failed, reported    LOOP AT return INTO DATA(ret).      DATA(severity) = SWITCH if_abap_behv_message=>t_severity( ret-type                         WHEN 'E' OR 'A' THEN if_abap_behv_message=>severity-error                         WHEN 'W'        THEN if_abap_behv_message=>severity-warning                         WHEN 'S'        THEN if_abap_behv_message=>severity-success                         ELSE                 if_abap_behv_message=>severity-information ).       APPEND VALUE #( %tky = tky                      %msg = new_message( id       = ret-id                                          number   = ret-number                                          severity = severity                                          v1 = ret-message_v1 v2 = ret-message_v2                                          v3 = ret-message_v3 v4 = ret-message_v4 ) )        TO reported-purchaserequest.       IF ret-type CA 'EA'.        APPEND VALUE #( %tky = tky ) TO failed-purchaserequest.      ENDIF.    ENDLOOP.  ENDMETHOD.

Real project scenario

A company wanted a Fiori app for its 15-year-old custom purchase request transaction. The transaction's function group contained approval-limit checks, budget reservation and change documents. A first attempt used managed RAP on the same tables — and silently skipped all of that logic, so requests created in the new app had no budget reservation.
The team switched to unmanaged RAP. The handler methods collected changes in a buffer, and the saver called the existing function modules, so every old rule ran again. Two details made it production-grade: the lock method reused the same lock object as the old transaction (so a user in the classic screen and a user in Fiori could not edit the same request), and the BAPIRET2 messages were mapped one-to-one with their original message class and number, so the support team recognized every error immediately.

Common mistakes

  • Choosing unmanaged for a greenfield object with its own tables — you then rewrite everything the framework would have done for free.
  • Choosing managed for data that an existing API must own, silently bypassing its business rules.
  • Forgetting the lock method, or using a different lock object than the legacy transaction, so both can edit in parallel.
  • Calling DEQUEUE manually — RAP releases locks at the end of the transaction.
  • Mapping all legacy messages to one generic error, losing message class, number and field context.
  • Reporting a message with severity error but not adding the instance to FAILED (or the reverse).
  • Assuming COMMIT WORK is allowed because "unmanaged means I own everything" — the framework still owns the LUW.

Best practices

  • Decide managed vs unmanaged by who must own persistence, not by habit.
  • Reuse the legacy application's lock object in the RAP lock method.
  • Use early numbering with UUIDs for technical keys; use late numbering when the legacy API assigns the real number at save.
  • Keep original message class and number when mapping BAPIRET2 → RAP messages.
  • Build the message-mapping helper once and reuse it in every handler and the saver.
  • Use cl_abap_lock_object_factory rather than calling ENQUEUE function modules directly, so the code is cloud-ready.

Interview angle

Frequent questions: "When would you use unmanaged RAP?", "What do you have to implement yourself in unmanaged?", "How is locking done in unmanaged RAP?", "How do you handle BAPI messages in RAP?"
Strong answer: unmanaged is for cases where existing business logic or an existing API must remain the owner of persistence, typical in brownfield wrapping. The developer implements the transactional buffer, CRUD and read methods, lock method, numbering and the saver phases; RAP still provides the contract, EML, services, ETag and draft orchestration, and still owns the LUW. Mention late numbering with adjust_numbers when the real document number is only known at save, and mapping BAPIRET2 to REPORTED/FAILED per instance with the original message class.

Practice this lesson

Quick check

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

2. In an unmanaged lock method, enqueue raises cx_abap_foreign_lock for one key. What should the method do?

3. In an unmanaged business object, how is a Fiori list report query with filters answered?

Interview questions

1. When would you choose unmanaged RAP instead of managed RAP, and what do you have to implement yourself once you do?

Answer it in your own words first, then compare.

2. How do you implement locking in unmanaged RAP, and what should happen when another user already holds the lock?

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