If you’ve already laid the groundwork with the ABAP RESTful Application Programming Model, you know that RAP isn’t just another framework — it’s a fundamental shift in how we build business applications on SAP S/4HANA. In Part 1 of this RAP series, we covered the architectural foundations: behavior definitions, business objects, and the basic anatomy of a RAP application. Now it’s time to get our hands dirty.
This article focuses on the parts of RAP that truly determine whether your application is production-ready: business logic implementation, validations, and determinations. These are the components where most projects either succeed gracefully or collapse under edge cases. I’ve watched both happen on real customer systems — and I’ll share what separates the two.
Why Business Logic Placement Matters More Than You Think
Before writing a single line of handler code, you need to make a deliberate architectural decision: where does your business logic actually live?
In classic ABAP, business logic was scattered across BAdIs, user-exits, function modules, and the occasional FORM routine from 2003 that nobody dares to touch. RAP gives you a structured answer to this problem. Your logic lives in handler classes — specifically in the ABAP behavior implementation (BDEF implementation class), and it’s organized into clearly defined hook points.
The three most critical hook points you need to master are:
- Validations — Check data integrity, raise messages, prevent incorrect saves
- Determinations — Derive and auto-calculate field values based on triggers
- Actions — Trigger explicit business processes (we’ll cover actions in Part 3)
Get this separation right and your application will be maintainable. Get it wrong and you’ll be debugging mysterious side effects six months from now.
Setting Up the Behavior Implementation Class
Assuming you have a CDS-based business object (as detailed in our CDS consumption views article), your behavior definition (BDEF) already declares which validations and determinations exist. Now you need to implement them.
Here’s a realistic BDEF excerpt for a purchase requisition-like entity:
/** Behavior Definition: ZI_PurchaseRequisition **/
managed implementation in class zbp_i_purchase_requisition unique;
define behavior for ZI_PurchaseRequisition alias PurchReq
persistent table zpur_req_head
lock master
authorization master ( instance )
etag master LastChangedAt
{
create;
update;
delete;
field ( readonly ) PurchReqId, CreatedAt, CreatedBy;
field ( mandatory ) MaterialNumber, Plant, Quantity, Unit;
/** Validations **/
validation validateMaterial on save { create; update; }
validation validateQuantity on save { create; update; }
/** Determinations **/
determination setDefaultPlant on modify { create; }
determination calculateNetValue on modify { field Quantity, PricePerUnit; }
mapping for zpur_req_head corresponding;
}
The key phrases here are on save and on modify. These are your triggers. on save validations fire when the user attempts to persist data. on modify determinations fire reactively when specific fields change. Understanding this distinction prevents an entire class of bugs.
Implementing Validations: More Than Just IF Checks
A validation in RAP isn’t just an IF statement wrapped in a method. It’s a contract between your business object and its consumers. It must:
- Read the relevant data for the affected instances
- Evaluate the business rule
- Report failures back via the RAP message framework — not via exceptions
Here’s a production-grade validation implementation:
CLASS lhc_PurchReq DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS validateMaterial FOR VALIDATE ON SAVE
IMPORTING keys FOR PurchReq~validateMaterial.
METHODS validateQuantity FOR VALIDATE ON SAVE
IMPORTING keys FOR PurchReq~validateQuantity.
ENDCLASS.
CLASS lhc_PurchReq IMPLEMENTATION.
METHOD validateMaterial.
" Step 1: Read current state of the affected instances
READ ENTITIES OF zi_purchaserequisition IN LOCAL MODE
ENTITY PurchReq
FIELDS ( MaterialNumber Plant )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_purch_req)
FAILED DATA(lt_failed).
" Step 2: Collect unique material/plant combinations for DB check
DATA lt_mat_plant TYPE SORTED TABLE OF mara
WITH UNIQUE KEY matnr WITH HEADER LINE.
LOOP AT lt_purch_req INTO DATA(ls_req).
" Check if material number is initial - mandatory check
IF ls_req-MaterialNumber IS INITIAL.
APPEND VALUE #(
%tky = ls_req-%tky
) TO failed-purchreq.
APPEND VALUE #(
%tky = ls_req-%tky
%state_area = 'VALIDATE_MATERIAL'
%msg = new_message(
id = 'ZPUR_REQ_MSGS'
number = '001'
severity = if_abap_behv_message=>severity-error
v1 = ls_req-MaterialNumber
)
%element-MaterialNumber = if_abap_behv=>mk-on
) TO reported-purchreq.
CONTINUE.
ENDIF.
" Validate material exists in MARA
SELECT SINGLE matnr
FROM mara
WHERE matnr = @ls_req-MaterialNumber
INTO @DATA(lv_matnr).
IF sy-subrc <> 0.
APPEND VALUE #( %tky = ls_req-%tky ) TO failed-purchreq.
APPEND VALUE #(
%tky = ls_req-%tky
%state_area = 'VALIDATE_MATERIAL'
%msg = new_message(
id = 'ZPUR_REQ_MSGS'
number = '002'
severity = if_abap_behv_message=>severity-error
v1 = ls_req-MaterialNumber
)
%element-MaterialNumber = if_abap_behv=>mk-on
) TO reported-purchreq.
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD validateQuantity.
READ ENTITIES OF zi_purchaserequisition IN LOCAL MODE
ENTITY PurchReq
FIELDS ( Quantity )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_purch_req).
LOOP AT lt_purch_req INTO DATA(ls_req).
IF ls_req-Quantity <= 0.
APPEND VALUE #( %tky = ls_req-%tky ) TO failed-purchreq.
APPEND VALUE #(
%tky = ls_req-%tky
%state_area = 'VALIDATE_QUANTITY'
%msg = new_message(
id = 'ZPUR_REQ_MSGS'
number = '003'
severity = if_abap_behv_message=>severity-error
)
%element-Quantity = if_abap_behv=>mk-on
) TO reported-purchreq.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
Notice the pattern: you always use READ ENTITIES IN LOCAL MODE inside a validation. This reads from the RAP transactional buffer — the state the user intends to save — not the database. This is critical. If you query the database directly, you’ll validate the old data, not the pending changes.
Also notice the %element mapping. Setting %element-FieldName = if_abap_behv=>mk-on tells the Fiori UI exactly which field is in error. The UI highlights it automatically. This is the difference between a professional application and a frustrating one.
Implementing Determinations: Reactive Business Logic Done Right
Determinations are where RAP really shines compared to legacy approaches. Think of them as reactive field derivations — when something changes, something else is automatically recalculated.
A common pitfall I see in projects: developers put too much logic in determinations, making them behave like mini workflows. Keep determinations focused on field value derivation. Complex orchestration belongs in actions.
METHOD setDefaultPlant FOR DETERMINE ON MODIFY
IMPORTING keys FOR PurchReq~setDefaultPlant.
" Read current instances
READ ENTITIES OF zi_purchaserequisition IN LOCAL MODE
ENTITY PurchReq
FIELDS ( Plant CreatedBy )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_purch_req).
DATA lt_update TYPE TABLE FOR UPDATE zi_purchaserequisition\PurchReq.
LOOP AT lt_purch_req INTO DATA(ls_req).
" Only set default if Plant is empty
IF ls_req-Plant IS INITIAL.
" Derive default plant from user parameters
DATA(lv_plant) = CONV werks_d( cl_abap_context_info=>get_system_attribute(
if_abap_context_info=>system_attribute-logon_plant ) ).
" Fallback to hardcoded default if user has no parameter
IF lv_plant IS INITIAL.
lv_plant = '1000'.
ENDIF.
APPEND VALUE #(
%tky = ls_req-%tky
Plant = lv_plant
%control-Plant = if_abap_behv=>mk-on
) TO lt_update.
ENDIF.
ENDLOOP.
" Modify the transactional buffer
MODIFY ENTITIES OF zi_purchaserequisition IN LOCAL MODE
ENTITY PurchReq
UPDATE FIELDS ( Plant )
WITH lt_update
REPORTED DATA(lt_reported).
rv_failed_reported = REDUCE #(
INIT s = VALUE failed_reported( )
FOR msg IN lt_reported
NEXT s = VALUE #( BASE s
reported-purchreq = VALUE #( BASE s-reported-purchreq
( LINES OF msg-purchreq ) ) ) ).
ENDMETHOD.
METHOD calculateNetValue FOR DETERMINE ON MODIFY
IMPORTING keys FOR PurchReq~calculateNetValue.
READ ENTITIES OF zi_purchaserequisition IN LOCAL MODE
ENTITY PurchReq
FIELDS ( Quantity PricePerUnit )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_purch_req).
DATA lt_update TYPE TABLE FOR UPDATE zi_purchaserequisition\PurchReq.
LOOP AT lt_purch_req INTO DATA(ls_req).
DATA(lv_net_value) = ls_req-Quantity * ls_req-PricePerUnit.
APPEND VALUE #(
%tky = ls_req-%tky
NetValue = lv_net_value
%control-NetValue = if_abap_behv=>mk-on
) TO lt_update.
ENDLOOP.
MODIFY ENTITIES OF zi_purchaserequisition IN LOCAL MODE
ENTITY PurchReq
UPDATE FIELDS ( NetValue )
WITH lt_update
REPORTED DATA(lt_reported).
ENDMETHOD.
The MODIFY ENTITIES IN LOCAL MODE call is essential here. You’re writing back to the RAP transactional buffer, not the database. The framework handles the actual persistence during the save sequence. Never call direct database updates inside a determination — that breaks the RAP transaction model entirely.
The save_modified Method: Your Last Line of Defense
For unmanaged RAP scenarios (or hybrid scenarios with with additional save), you’ll implement the save_modified method. This is where you perform the actual database writes for legacy table structures that don’t map cleanly to CDS.
CLASS lsc_ZI_PURCHASEREQUISITION DEFINITION INHERITING FROM cl_abap_behavior_saver.
PROTECTED SECTION.
METHODS save_modified REDEFINITION.
METHODS cleanup_finalize REDEFINITION.
ENDCLASS.
CLASS lsc_ZI_PURCHASEREQUISITION IMPLEMENTATION.
METHOD save_modified.
" Handle CREATE operations
IF create-purchreq IS NOT INITIAL.
DATA lt_insert TYPE TABLE OF zpur_req_head.
LOOP AT create-purchreq INTO DATA(ls_create).
APPEND CORRESPONDING zpur_req_head( ls_create ) TO lt_insert.
ENDLOOP.
INSERT zpur_req_head FROM TABLE @lt_insert.
ENDIF.
" Handle UPDATE operations
IF update-purchreq IS NOT INITIAL.
LOOP AT update-purchreq INTO DATA(ls_update).
UPDATE zpur_req_head
SET material_number = CASE WHEN ls_update-%control-MaterialNumber = cl_abap_behv=>flag_changed
THEN @ls_update-MaterialNumber
ELSE material_number END,
quantity = CASE WHEN ls_update-%control-Quantity = cl_abap_behv=>flag_changed
THEN @ls_update-Quantity
ELSE quantity END,
net_value = CASE WHEN ls_update-%control-NetValue = cl_abap_behv=>flag_changed
THEN @ls_update-NetValue
ELSE net_value END
WHERE purch_req_id = @ls_update-PurchReqId.
ENDLOOP.
ENDIF.
" Handle DELETE operations
IF delete-purchreq IS NOT INITIAL.
DELETE zpur_req_head
FROM TABLE @( CORRESPONDING #( delete-purchreq MAPPING purch_req_id = PurchReqId ) ).
ENDIF.
ENDMETHOD.
METHOD cleanup_finalize.
" Clean up any draft-related or temporary state here
ENDMETHOD.
ENDCLASS.
The %control structure is your best friend in save_modified. It tells you precisely which fields were actually changed in the current transaction, so you don’t accidentally overwrite fields with empty values.
Common Pitfalls and How to Avoid Them
Having reviewed and debugged RAP implementations across multiple S/4HANA projects, here are the mistakes I see most often:
1. Querying the Database Inside Validations Instead of Using READ ENTITIES
If you bypass READ ENTITIES IN LOCAL MODE and go directly to the database, you’ll validate stale data. The user could have changed a value that hasn’t been saved yet — your validation will see the old value and pass when it should fail. Always use the transactional buffer.
2. Raising Exceptions Instead of Reporting to the Framework
Classic ABAP developers instinctively throw exceptions when something goes wrong. In RAP, you populate the failed and reported structures. Raising an unhandled exception in a handler method will crash the entire OData call with a generic error — zero useful feedback for the user.
3. Over-triggering Determinations
If your determination triggers on create and update for all fields, it fires constantly. Be precise with your trigger conditions. Use field FieldName triggers to limit determination execution to only when relevant data changes. This has a direct performance impact — especially relevant if you’ve read our ABAP performance optimization guide.
4. Mixing Validation and Determination Responsibilities
A determination should never reject a save — that’s a validation’s job. A validation should never modify data — that’s a determination’s job. Keep the responsibilities clean. This principle aligns with the broader clean code practices we’ve discussed in earlier articles.
Testing Your Business Logic: Don’t Skip This
RAP business logic is fully testable using ABAP Unit Tests with the CL_ABAP_BEHV_TEST_ENVIRONMENT class. This is not optional — if you’re building production applications, you need automated tests. Our article on ABAP unit testing in S/4HANA covers the foundational approach, but for RAP specifically, the key is isolating the behavior implementation from the database.
CLASS ltc_validate_material DEFINITION FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_env TYPE REF TO if_abap_behv_test_environment.
METHODS setup.
METHODS teardown.
METHODS test_invalid_material FOR TESTING.
METHODS test_valid_material FOR TESTING.
ENDCLASS.
CLASS ltc_validate_material IMPLEMENTATION.
METHOD setup.
mo_env = cl_abap_behv_test_environment=>create(
entity_type = 'ZI_PURCHASEREQUISITION' ).
ENDMETHOD.
METHOD teardown.
mo_env->destroy( ).
ENDMETHOD.
METHOD test_invalid_material.
" Prepare test data with invalid material
DATA lt_create TYPE TABLE FOR CREATE zi_purchaserequisition\PurchReq.
APPEND VALUE #(
%cid = 'TEST_001'
MaterialNumber = 'NONEXISTENT'
Quantity = 10
Unit = 'EA'
) TO lt_create.
" Execute the create operation
MODIFY ENTITIES OF zi_purchaserequisition
ENTITY PurchReq
CREATE FROM lt_create
FAILED DATA(lt_failed)
REPORTED DATA(lt_reported).
COMMIT ENTITIES BEGIN
RESPONSE OF zi_purchaserequisition
FAILED DATA(lt_commit_failed)
REPORTED DATA(lt_commit_reported).
COMMIT ENTITIES END.
" Assert that the operation failed
cl_abap_unit_assert=>assert_not_initial(
act = lt_commit_failed-purchreq
msg = 'Expected validation failure for nonexistent material' ).
ENDMETHOD.
METHOD test_valid_material.
" This would mock MARA lookup and test the happy path
" Implementation omitted for brevity — see unit testing article for mocking patterns
ENDMETHOD.
ENDCLASS.
What’s Coming in Part 3
We’ve covered validations and determinations in depth. In Part 3 of this RAP series, we’ll tackle RAP Actions — both instance-bound and static — along with the draft handling mechanism. Draft is where RAP truly differentiates itself from any previous SAP programming model, and it’s where many implementations get architecturally wrong. You won’t want to miss it.
Also worth revisiting: if you’re uncertain about how CDS access control integrates with RAP’s authorization master concept, our CDS DCL and access control article provides exactly the foundation you need before diving into RAP authorization objects.
Key Takeaways
- Always use
READ ENTITIES IN LOCAL MODEinside validations and determinations — never query the database directly for pending changes - Validations report errors via
failedandreportedstructures; they never modify data - Determinations derive field values via
MODIFY ENTITIES IN LOCAL MODE; they never reject saves - Use
%controlinsave_modifiedto process only actually changed fields - Keep determination triggers precise — over-triggering is a direct performance concern
- Automated testing with
CL_ABAP_BEHV_TEST_ENVIRONMENTis non-negotiable for production quality
If you found this article useful, drop a comment below with the specific RAP challenge you’re working through — I read every response and often use real questions as the basis for follow-up content. Share this with a colleague who’s still wrestling with legacy BAPI-based development. The migration path to RAP is worth every bit of the learning curve.

