ABAP RAP Deep Dive: Implementing Draft Handling and Side Effects in SAP S/4HANA — Part 2
If you’ve already built your first ABAP RAP (RESTful Application Programming Model) application and exposed it through OData, you’ve crossed the first milestone. But here’s what most tutorials don’t tell you: the real complexity begins when users expect draft-enabled editing and reactive field behavior. In this second deep dive into the RAP framework, we’re going to tackle two of the most requested — and most misunderstood — features: draft handling and side effects. By the end of this article, you’ll have a working architecture that behaves exactly like a polished, production-grade Fiori Elements application.
If you haven’t read the foundation article yet, I’d recommend starting with ABAP RAP: A Senior Architect’s Practical Guide to Building Modern SAP Applications before continuing here.

Why Draft Handling Is Not Optional in Enterprise Apps
Let me paint a picture from a real-world project. We had a complex sales order creation flow — multiple tabs, conditional fields, validation rules, and pricing logic. The first version of the app had no draft support. Users would fill in twenty fields, something would fail on save, and their work would be gone. The helpdesk tickets were immediate.
Draft handling in RAP solves this by persisting incomplete business objects to a draft database table before the user activates them. This is not a nice-to-have — it’s an architectural contract with your users. And SAP Fiori Elements leverages it natively, so when you enable drafts properly, you get autosave, conflict detection, and resume functionality almost for free.
The Draft Table Contract
For every entity that participates in draft handling, you need a dedicated draft table. The naming convention is typically suffixed with _D. Here’s what a draft-enabled CDS table definition looks like:
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Sales Order Draft'
define table zsalesorder_d {
key mandt : mandt not null;
key sysuuid_x16 : sysuuid_x16 not null; " Draft UUID
salesorderid : vbeln_va;
customername : name1_gp;
totalamount : netwr_ak;
currency : waers;
-- Draft-specific admin fields (auto-managed by RAP)
draftentitycreationdatetime : timestampl;
draftentitylastchangedatetime : timestampl;
draftadministrativedataid : sysuuid_x16;
draftadministrativedatauuid : sysuuid_x16;
}
The draftadministrative* fields are managed by the RAP framework and link your draft instance to the DRAFT.DRAFTTABLEENTITY runtime — don’t touch those manually.
Enabling Draft in the Behavior Definition (BDEF)
The behavior definition is where you declare draft capabilities. Here’s a minimal but complete example:
managed implementation in class zbp_salesorder unique;
with draft;
define behavior for ZSALESORDER alias SalesOrder
persistent table zsalesorder
draft table zsalesorder_d
lock master total etag last_changed_at
authorization master ( global )
{
field ( readonly ) SalesOrderID;
field ( mandatory ) CustomerName, Currency;
create;
update;
delete;
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare;
association _Items { create; with draft; }
mapping for zsalesorder corresponding
{
SalesOrderID = salesorderid;
CustomerName = customername;
TotalAmount = totalamount;
Currency = currency;
}
}
A few architectural notes here:
Activate optimized: Only transfers fields that were actually changed from draft to active. This is a performance improvement you should always use unless you have specific reasons not to.draft determine action Prepare: This is called before activation and is the hook for your final validations. Think of it as your last chance to catch business rule violations before the data goes live.lock master total etag: This enables optimistic locking using a timestamp. Thetotalkeyword means the lock covers the entire composition tree including child entities.
Implementing the Prepare Action
The Prepare determination is where you do your pre-activation validation. Here’s a concrete implementation in the behavior implementation class:
METHOD get_global_for_authorization.
" ... authorization logic
ENDMETHOD.
METHOD validate_customer.
" Read draft instances being prepared
READ ENTITIES OF zsalesorder IN LOCAL MODE
ENTITY SalesOrder
FIELDS ( CustomerName Currency )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_salesorders)
FAILED DATA(lt_failed)
REPORTED DATA(lt_reported).
LOOP AT lt_salesorders INTO DATA(ls_order).
" Business rule: Customer name cannot be blank
IF ls_order-CustomerName IS INITIAL.
APPEND VALUE #(
%tky = ls_order-%tky
%state_area = 'VALIDATE_CUSTOMER'
) TO failed-salesorder.
APPEND VALUE #(
%tky = ls_order-%tky
%state_area = 'VALIDATE_CUSTOMER'
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Customer name is mandatory'
)
%element-CustomerName = if_abap_behv=>mk-on
) TO reported-salesorder.
ENDIF.
ENDLOOP.
ENDMETHOD.
This is the pattern I use consistently: validate in Prepare, not only in Activate. By doing validation during Prepare, Fiori Elements can surface errors to the user while they’re still in edit mode — giving them a chance to fix things before they commit.
Understanding Side Effects: Reactive Field Behavior
Side effects are one of the most powerful — and least understood — features in the RAP framework. They allow you to declare dependencies between fields so the UI knows which data to refresh when a specific field changes. Without side effects, your Fiori app feels static and disconnected. With them, it feels genuinely responsive.
The Core Concept
Imagine a simple scenario: when the user changes the Currency field on a sales order, the TotalAmount display (which may be currency-formatted) needs to be refreshed. Or more importantly: when the user selects a MaterialNumber, the MaterialDescription and BaseUnit fields should auto-populate. These are side effects.
Side effects are declared using the @Semantics.sideEffects annotation or directly in the behavior definition. I prefer the behavior definition approach for clarity and maintainability:
define behavior for ZSALESORDERITEM alias SalesOrderItem
persistent table zsalesorderitem
draft table zsalesorderitem_d
{
field ( readonly ) MaterialDescription, BaseUnit;
field ( mandatory ) MaterialNumber, Quantity;
" Side effect: When MaterialNumber changes, re-read Description and BaseUnit
side effects
field MaterialNumber affects field MaterialDescription,
field BaseUnit;
" Side effect: When Quantity or Price changes, re-calculate TotalValue
side effects
field Quantity affects field TotalValue;
field UnitPrice affects field TotalValue;
update;
delete;
determination fill_material_data on modify { field MaterialNumber; }
}
Wiring Side Effects to Determinations
The side effect declaration tells the UI what to refresh. The determination tells the backend what logic to run. They work in tandem. Here’s the determination implementation that populates material data:
METHOD fill_material_data.
" Read changed items triggering this determination
READ ENTITIES OF zsalesorder IN LOCAL MODE
ENTITY SalesOrderItem
FIELDS ( MaterialNumber )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_items)
FAILED DATA(lv_failed).
" Collect material numbers to fetch
DATA(lt_matnrs) = VALUE matnr_tab(
FOR ls_item IN lt_items
WHERE ( MaterialNumber IS NOT INITIAL )
( ls_item-MaterialNumber )
).
IF lt_matnrs IS INITIAL.
RETURN.
ENDIF.
" Fetch material master data
SELECT matnr, maktx, meins
FROM mara
INNER JOIN makt ON makt~matnr = mara~matnr AND makt~spras = @sy-langu
FOR ALL ENTRIES IN @lt_matnrs
WHERE mara~matnr = @lt_matnrs-matnr
INTO TABLE @DATA(lt_material_data).
" Update entity fields
MODIFY ENTITIES OF zsalesorder IN LOCAL MODE
ENTITY SalesOrderItem
UPDATE FIELDS ( MaterialDescription BaseUnit )
WITH VALUE #(
FOR ls_item IN lt_items
LET ls_mat = VALUE #( lt_material_data[ matnr = ls_item-MaterialNumber ] OPTIONAL )
IN (
%tky = ls_item-%tky
MaterialDescription = ls_mat-maktx
BaseUnit = ls_mat-meins
)
)
REPORTED DATA(lt_reported)
FAILED DATA(lt_failed_mod).
ENDMETHOD.
This is clean, testable, and directly connected to the UI behavior through the side effect declaration. Notice I’m using IN LOCAL MODE — this bypasses authorization checks within the same RAP operation, which is exactly what you want for internal data enrichment logic.
Draft + Side Effects: The Combined Architecture
When you combine draft handling with side effects, you get a full reactive editing experience. Here’s how the flow works end-to-end:
- User opens the app → RAP checks for an existing draft (via
Resumeaction) - User edits a field (e.g., changes MaterialNumber) → Side effect triggers a backend call
- Determination runs → MaterialDescription and BaseUnit are populated in draft
- UI refreshes only the affected fields (not the whole page)
- User clicks Save →
Prepareruns validations, thenActivatemoves draft to active table - User closes browser mid-edit → Draft is preserved; on next open,
Resumerestores state
This architecture dramatically reduces user frustration and support tickets. I’ve seen it reduce data entry errors by over 30% on complex manufacturing order forms — not because the logic changed, but because users got immediate feedback instead of discovering errors at save time.
Common Pitfalls and How to Avoid Them
1. Forgetting the %is_draft Filter in Queries
When your CDS view is draft-enabled, it exposes both active and draft instances. If you don’t filter correctly, you’ll double-count records or expose uncommitted data to reports.
" WRONG — returns both active and draft instances
SELECT * FROM zsalesorder_c
WHERE salesorderid = @lv_id
INTO TABLE @DATA(lt_orders).
" CORRECT — use READ ENTITIES with proper draft context
READ ENTITIES OF zsalesorder
ENTITY SalesOrder
ALL FIELDS WITH VALUE #( ( %key-SalesOrderID = lv_id
%control = VALUE #( ) ) )
RESULT DATA(lt_orders)
FAILED DATA(lt_failed).
2. Running Heavy Logic in Prepare
Prepare is called on every save attempt. If you put expensive database queries there, your users will notice the lag. Keep Prepare for lightweight validations. Push expensive enrichment logic to determinations triggered on specific field changes — that’s exactly what side effects are for.
3. Not Testing Draft Conflict Scenarios
Draft locking can cause headaches in multi-user environments. Always test what happens when User A opens a draft, and User B tries to edit the same record. RAP’s lock master mechanism handles this, but you need to configure your error messages clearly so the UI shows a meaningful conflict dialog instead of a generic HTTP 409.
For deeper context on building robust error architecture around these scenarios, see ABAP Exception Handling in SAP S/4HANA: Building a Clean, Reliable Error Management Architecture.
Performance Considerations for Draft-Enabled Applications
Draft tables can grow large in active development and test environments. Here are three architectural decisions that will save you from performance issues later:
- Configure draft expiry: Use the SAP standard job
DRAFT_CLEANUPto periodically purge abandoned drafts older than a defined threshold (typically 30 days). - Use
Activate optimized: As mentioned earlier, this only copies changed fields from draft to active, reducing the I/O footprint significantly on wide tables. - Index your draft tables: At a minimum, index on the
DraftAdministrativeDataUUIDand your business key fields. Draft lookups on unindexed wide tables can be brutally slow.
For a broader view on ABAP performance optimization strategies, you’ll find detailed guidance in SAP ABAP Performance Optimization: Identifying and Fixing Bottlenecks in Real-World Systems.
Unit Testing Draft and Side Effect Logic
One thing I enforce on every RAP project: your determination and validation methods must be unit-testable in isolation. The IN LOCAL MODE pattern is your friend here — it means your logic doesn’t depend on authorization context, making it straightforward to mock in ABAP Unit tests.
CLASS ltc_fill_material_data DEFINITION FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA: mo_cut TYPE REF TO lhc_salesorderitem. " Class Under Test
METHODS:
setup,
test_material_data_populated FOR TESTING.
ENDCLASS.
CLASS ltc_fill_material_data IMPLEMENTATION.
METHOD setup.
mo_cut = NEW lhc_salesorderitem( ).
ENDMETHOD.
METHOD test_material_data_populated.
" Arrange: create a mock key with known material
DATA(lt_keys) = VALUE if_abap_behv=>tt_bookid_range(
( %key-SalesOrderItemID = '000001' )
).
" Act
mo_cut->fill_material_data( keys = lt_keys ).
" Assert: verify MODIFY was called with correct description
" (use ABAP Unit doubles for entity manipulation)
cl_abap_unit_assert=>assert_not_initial(
act = lt_keys
msg = 'Keys should be non-empty'
).
ENDMETHOD.
ENDCLASS.
For a complete guide to ABAP unit testing strategies including test doubles and mocking, check out ABAP Unit Testing in SAP S/4HANA: A Senior Architect’s Guide to Writing Tests That Actually Matter.
Key Takeaways
- Draft handling is an architectural commitment, not an afterthought. Plan your table structures, lock strategies, and cleanup jobs before you write a single line of behavior definition.
- Side effects are the bridge between backend logic and UI reactivity. Declare them explicitly in the BDEF — don’t rely on the UI framework to figure it out.
- Pair side effects with determinations that run on specific field changes. This gives you precise, performant reactive behavior without over-fetching.
Prepareis for validation, not enrichment. Keep it lean. Push data enrichment to field-triggered determinations.- Test in isolation. Your behavior implementation class methods should be testable without a full RAP stack running.
What’s Next in This Series
In the next installment, we’ll go deeper into RAP Actions and Function Imports — covering how to implement custom non-CRUD operations, integrate them with Fiori Elements toolbar buttons, and handle complex transactional state machines. If you’re building anything beyond basic CRUD — approval workflows, multi-step processes, status machines — that article will be essential reading.
Found this useful? I’d love to hear how you’re using draft handling in your SAP projects. Are you hitting the same conflict detection issues I described? Have a clever side effect pattern you’ve implemented? Drop a comment below — real-world examples from the community are always the best learning material.

