Back to all posts

COMMIT WORK Is Not Just a Database Save

COMMIT WORK Is Not Just a Database Save infographic Visual summary Key ideas at a glance

Most ABAP developers pick up COMMIT WORK somewhere along the way and learn it as a one-liner: "it saves the data". That short version is wrong in almost every interesting way. COMMIT WORK is a closing statement for a logical unit of work. It triggers a sequence of actions, several of them outside the calling program. The database commit is the last meaningful step in that sequence, not the meaning of the statement itself.

This post is about what that sequence actually contains, what an SAP LUW (Logical Unit of Work) even is, why it is a different thing from a database transaction, and why a hidden COMMIT WORK inside a reusable API or BAdI (Business Add-In) quietly breaks transaction control for everyone above it on the stack.

What an LUW is

An LUW (Logical Unit of Work) is a unit of change that either succeeds completely or has no visible effect. It comes in two flavours in ABAP, and they are not the same thing.

A database LUW is a database transaction in the conventional sense. It begins after the previous database commit or rollback and ends with the next one. While it is open, the database tracks every change so that a rollback can revert all of them in one shot. Most engines do this with a per-transaction record of original values, often called an undo log or undo segment; PostgreSQL achieves the same effect through Multi-Version Concurrency Control (MVCC) tuple visibility. SAP HANA also uses MVCC for transactional isolation: it keeps parallel record versions, exposes row-store MVCC version memory separately from column-store MVCC structures, and tracks transactions with MVCC timestamps and commit IDs.1 Either everything in the database LUW commits, or none of it survives.

A SAP LUW is the application's unit of change. In the controlled case, its boundaries are the points where ABAP issues COMMIT WORK or ROLLBACK WORK; ending the current program or internal session also ends the current SAP LUW, but without running the registered procedures.2 One SAP LUW can span several dialog steps, several work processes, and several database LUWs. The whole reason it exists is that ABAP programs often need to register changes early, in one work process, and have them executed later, in a different work process, against a database LUW the program does not own.

A short glossary for the rest of this post:

  • Update task: a deferred-execution mechanism for function modules. CALL FUNCTION 'Z_FOO' IN UPDATE TASK does not call the function. It records the call and its parameters in the system tables VBHDR, VBMOD, and VBDATA. The kernel runs it later, in a dedicated update work process, when COMMIT WORK reaches that registration. Update modules come in two priorities. V1 modules carry the database-critical work and run in a single shared database LUW for that SAP LUW. V2 modules run after V1 has succeeded, in a separate database LUW, for non-critical follow-up work like statistics updates.
  • PERFORM ON COMMIT: a similar deferral, but for a local subroutine in the current program. The subroutine is queued and runs in the current work process when COMMIT WORK executes. There is a PERFORM ON ROLLBACK counterpart for the rollback path.
  • bgRFC: background remote function call, the modern background RFC mechanism that supersedes classic tRFC and qRFC patterns. CALL FUNCTION 'Z_FOO' IN BACKGROUND UNIT registers a remote-enabled function module to be executed asynchronously by the bgRFC scheduler after COMMIT WORK. The older equivalents are tRFC (transactional remote function call, registered with IN BACKGROUND TASK for asynchronous once-only delivery) and qRFC (queued remote function call, the same idea with queue-based serialization). Both are obsolete and slowly being retired by SAP, but the runtime still supports them.

The two LUWs collapse into a single thing only in the simplest case: one program, direct SQL, one COMMIT WORK, no other registrations. Almost every other ABAP transactional pattern, including update tasks, PERFORM ON COMMIT, transactional RFC, background RFC, and the persistence service of Object Services, exists because the SAP LUW and the database LUW had to be decoupled.

What COMMIT WORK actually triggers

COMMIT WORK works through a fixed sequence of actions. The order is documented and stable across releases.3 It is also the order in which things go wrong, so the concrete steps are the actual content:

  1. Run all subroutines registered with PERFORM ON COMMIT, in ascending LEVEL order when specified, otherwise in registration order, in the current work process.
  2. Notify the persistence service of Object Services. Persistent classes that implement IF_OS_STATE register themselves when their managed attributes change. At this point in the commit, the persistence service walks the registered objects and calls their save methods, which in turn issue the actual CALL FUNCTION ... IN UPDATE TASK calls for the modified data.4 After this step the runtime objects are invalidated by IF_OS_STATE~INVALIDATE.
  3. Process the registered update function modules. All V1 modules run first, in the order they were registered, in one shared database LUW inside the update work process. V2 modules run afterward in another shared database LUW, only if V1 succeeded.
  4. Process registered bgRFC and tRFC units. One database LUW per RFC destination, dispatched asynchronously to the target system.
  5. Release SAP locks held by the current program, depending on the _SCOPE parameter that was passed to the corresponding enqueue function module.
  6. Issue a database commit on every open database connection. This ends the current database LUW in the dialog work process and closes its open cursors.
  7. Raise the TRANSACTION_FINISHED event of CL_SYSTEM_TRANSACTION_STATE with KIND = COMMIT_WORK, so listening code can react. In asynchronous update mode, this event means the update was initiated; it is not an "all update work has finished" hook.

Every step is application-visible. A failing PERFORM ON COMMIT subroutine can terminate the commit before step 6 ever runs. A short-dumping update module rolls back its own database LUW in the update work process and lands in SM13 as a failed update record. A failing bgRFC destination affects only its own unit. The database commit at step 6 is real, but it is the consequence of the SAP LUW closing successfully, not the meaning of COMMIT WORK.

One subtle property: in asynchronous update mode, the dialog work process and the update work process operate on different database LUWs at different times. Locally executed work, such as a PERFORM ON COMMIT subroutine that writes directly to the database, ends up in the dialog work process's database LUW. Update-task work ends up in the update work process's database LUW. The two can succeed or fail independently of each other.

How registration looks in code

Classic SAP standard posting paths in areas such as FI, MM, and SD commonly register update work for COMMIT WORK instead of doing all database changes directly in the dialog step, and their customer extensions usually run inside that caller-owned transaction boundary. The three patterns below are what those registrations look like; modern RAP code uses a different flow that is covered further down.

" Subroutine deferred to commit time, runs in this work process
PERFORM update_local_buffer ON COMMIT.

" Update function module deferred to commit time, runs in an update work process
CALL FUNCTION 'Z_AIRPORT_UPDATE' IN UPDATE TASK
  EXPORTING iv_airport_id = 'FRA'.

" Remote function module deferred to commit time, runs asynchronously via bgRFC
CALL FUNCTION 'Z_SEND_AIRPORT_UPDATE' IN BACKGROUND UNIT lo_unit
  EXPORTING iv_airport_id = 'FRA'.

None of these statements changes the database. They register intent. The actual work runs when COMMIT WORK reaches them in the order documented above. ROLLBACK WORK deletes the registrations.

The mental model is consistent across all three: register now, fire on commit, get cleared on rollback. Once that pattern clicks, the rest of the SAP LUW machinery reads as variations on the same idea.

A self-contained PERFORM ON COMMIT example

PERFORM ON COMMIT is the easiest register-and-defer mechanism to demonstrate without setting up update modules or RFC destinations. It also happens to expose enough of the SAP LUW state to make the introspection class CL_SYSTEM_TRANSACTION_STATE worth knowing about.

REPORT zcommit_work_demo.

DATA gt_log TYPE STANDARD TABLE OF string.

FORM step_b.
  APPEND |  step_b: in_perform_on_commit = { cl_system_transaction_state=>get_on_commit( ) }| TO gt_log.
ENDFORM.

FORM step_a.
  APPEND |  step_a: sap_luw_key = { cl_system_transaction_state=>get_sap_luw_key( ) }| TO gt_log.
ENDFORM.

FORM rollback_step.
  APPEND |  rollback_step: in_on_rollback = { cl_system_transaction_state=>get_on_rollback( ) }| TO gt_log.
ENDFORM.

START-OF-SELECTION.
  APPEND |before any registration| TO gt_log.
  APPEND |  sap_luw_key = { cl_system_transaction_state=>get_sap_luw_key( ) }| TO gt_log.

  PERFORM step_b ON COMMIT.
  PERFORM step_a ON COMMIT.
  PERFORM rollback_step ON ROLLBACK.

  APPEND |after registration, before COMMIT WORK| TO gt_log.
  COMMIT WORK.
  APPEND |after COMMIT WORK| TO gt_log.

  PERFORM step_a ON COMMIT.
  PERFORM rollback_step ON ROLLBACK.

  APPEND |before ROLLBACK WORK| TO gt_log.
  ROLLBACK WORK.
  APPEND |after ROLLBACK WORK| TO gt_log.

  cl_demo_output=>display( gt_log ).

Three details are worth noticing.

First, the subroutines do not run when PERFORM ... ON COMMIT is reached. They run only when COMMIT WORK executes. That is the whole purpose of the construct. Registration is a no-op against the database; only COMMIT WORK turns it into work.

Second, inside step_b, cl_system_transaction_state=>get_on_commit( ) returns 1. Outside the commit phase it returns 0. There are parallel methods for the rollback path (get_on_rollback( )) and for code running in an update work process (get_in_update_task( )). These are reliable signals for code that needs to assert it is or is not inside a transactional phase, without resorting to the debugger.

Third, cl_system_transaction_state=>get_sap_luw_key( ) returns the identifier of the current SAP LUW. It is the right hook for correlating logs across the dialog work process and any update or RFC work processes that handle the same SAP LUW.

COMMIT WORK AND WAIT

The plain COMMIT WORK schedules its registered work and lets execution continue with the next statement after it. Without AND WAIT, the calling program does not block on the update work process. Execution resumes as soon as the registrations have been handed off, which means the dialog program runs on while the V1 update modules are still being processed elsewhere.

COMMIT WORK AND WAIT adds one synchronisation step. The dialog program blocks until all V1 update modules for this SAP LUW have finished. V2 modules continue to run asynchronously in either case.

Two consequences fall out of that:

The first is the obvious one. If the next statement in the program needs the database row that the V1 update wrote, AND WAIT is the safe choice. Without it, the row may not exist yet on the database when the next SELECT runs.

The second is failure handling. With AND WAIT, a V1 update failure returns to the dialog program with sy-subrc <> 0, so the calling code can branch on it. Without AND WAIT, the V1 failure is recorded in SM13 as a failed update record, but the calling program has already moved on. There is no return code to react to.

A small synchronous example using SET UPDATE TASK LOCAL:

REPORT zupdate_task_local_demo.

START-OF-SELECTION.
  SET UPDATE TASK LOCAL.

  " Replace with an update function module from your own system
  CALL FUNCTION 'Z_AIRPORT_UPDATE' IN UPDATE TASK.

  COMMIT WORK AND WAIT.

SET UPDATE TASK LOCAL is the sharper switch. With local update active, V1 update modules are stored in ABAP memory instead of the VB... tables and run in the current work process, in a separate internal session, when COMMIT WORK executes. Both COMMIT WORK and COMMIT WORK AND WAIT then behave synchronously for those V1 modules. V2 modules are not affected; they still run asynchronously through the normal update queue. Local update is the right default in test code and setup routines, where asynchronous timing makes assertions unstable.

ROLLBACK WORK has its own action list

ROLLBACK WORK is the inverse statement and runs its own ordered sequence:5

  1. Execute every subroutine registered with PERFORM ON ROLLBACK.
  2. Delete the registrations of every subroutine registered with PERFORM ON COMMIT.
  3. Notify the persistence service of Object Services so that managed persistent objects reset their attributes.
  4. Delete every update function module registration from the VB... tables.
  5. Delete every RFC call registered with CALL FUNCTION ... IN BACKGROUND UNIT or ... IN BACKGROUND TASK.
  6. Release SAP locks held by the current program for which _SCOPE = 2.
  7. Issue a database rollback on every open database connection, ending the current database LUW.
  8. Raise TRANSACTION_FINISHED with KIND = ROLLBACK_WORK.

Two details are easy to miss.

ROLLBACK WORK only cancels work that is still registered for the current SAP LUW. If a previous COMMIT WORK has already dispatched a V1 update, ROLLBACK WORK issued later cannot pull that work back. The earlier commit closed its SAP LUW and started a new one; the rollback only affects what the program has registered since then.

The other detail is the lock scope. The _SCOPE parameter of an ENQUEUE function module decides who owns the lock and when it goes away. _SCOPE = 1 ties the lock only to the dialog owner; neither COMMIT WORK nor ROLLBACK WORK releases it, so the lock survives until an explicit DEQUEUE or until the program ends. _SCOPE = 2 ties the lock only to the update owner; if at least one update function module was registered for this SAP LUW, COMMIT WORK or ROLLBACK WORK hands the lock over and releases it when the update completes. _SCOPE = 3 ties the lock to both owners and is released only when the last of the two owners releases it. "I called rollback, why is the lock still held?" is almost always answered by "you set the wrong scope on enqueue."

The rules COMMIT WORK enforces on its callers

The runtime enforces three forbidden positions for COMMIT WORK and short-dumps if they are violated:

  • Inside an update function module: runtime error COMMIT_IN_POSTING.
  • Inside a PERFORM ON COMMIT subroutine: runtime error COMMIT_IN_PERFORM_ON_COMMIT.
  • Inside a PERFORM ON ROLLBACK subroutine: runtime error COMMIT_IN_PERFORM_ON_COMMIT. The same error name is raised for both registration variants.

The reason is structural. Steps 1 and 3 of the action list are themselves running in those positions. A nested COMMIT WORK would mean re-entering the action list while the previous one is unfinished. The kernel refuses.

In practice this matters most for code that runs inside a BAdI implementation, a customer exit, an enhancement spot, or a generic utility class. A library that calls COMMIT WORK "to be safe" can short-dump as soon as a caller invokes it from inside one of these contexts. Any library documented as safe to call anywhere is making a contract it cannot keep if it commits internally.

Hidden commits break transaction control

The most common bug pattern around COMMIT WORK is the hidden one. A reusable API, a BAdI, a utility class, a "save log entry" helper. Any of them might contain a COMMIT WORK that is invisible to the caller. The caller assembles a multi-step transaction and expects to control the commit boundary itself. Halfway through, a helper somewhere in the call stack flushes everything for them.

That breaks two things at once.

It breaks the caller's SAP LUW. Anything the caller had registered with PERFORM ON COMMIT or IN UPDATE TASK runs at the hidden commit, not at the boundary the caller intended. If the caller then issues an explicit ROLLBACK WORK after detecting an error, only the work registered after the hidden commit is rolled back. The earlier work is already gone.

It breaks the caller's database LUW. Cursors close at the database commit; even OPEN CURSOR WITH HOLD does not survive COMMIT WORK. Locks with _SCOPE = 2 are released. The caller's expectation of a single open database transaction is silently false.

The clean fix is for the helper to register its work with IN UPDATE TASK or PERFORM ON COMMIT and let the caller commit. The blunt fix is to remove the COMMIT WORK and document that the caller must commit. The wrong fix, sometimes seen in older codebases, is to wrap the caller in SET UPDATE TASK LOCAL and hope.

A related case is the implicit database commit triggered by the kernel. The end of a dialog step, a synchronous RFC call, an HTTP roundtrip through the Internet Communication Framework (ICF), the ABAP HTTP request and response layer, or a WAIT statement can close the database LUW. They do not close the SAP LUW. Code that batches data with INSERT statements and then issues an RFC in the middle has already lost atomicity at the database level, even before any COMMIT WORK runs.

The WAIT case is easy to underestimate because it looks like scheduling, not transaction control. Each use of WAIT performs a database commit, except in updates.6 A polling loop with WAIT UP TO 1 SECONDS inside a transaction is therefore not just waiting. It is repeatedly ending the current database LUW while the SAP LUW remains open.

Debugging the actual sequence

When an SAP LUW behaves unexpectedly, both the COMMIT WORK call site and the work registered to run with it are worth inspecting. The call site matters because a hidden COMMIT WORK in a helper or BAdI implementation will close the SAP LUW earlier than the caller expected. The registered work matters because what fails is usually inside an update module or a PERFORM ON COMMIT subroutine, not on the COMMIT WORK line itself.

SM13 shows update records and their state. Failed V1 records sit there and are not retried automatically. Knowing which update modules registered, and which ones failed, takes the conversation past "the commit did not work" into specifics.

SM12 shows current SAP locks, including ones whose scope was set wrong. A row that "cannot be edited" five seconds after a rollback often has a _SCOPE = 1 lock still held until the program ends.

CL_SYSTEM_TRANSACTION_STATE is useful inside code that needs to assert it is, or is not, in a transactional phase. Asserting cl_system_transaction_state=>get_on_commit( ) = 0 at the top of a public API is a fast way to catch a future COMMIT WORK that ends up called from inside a PERFORM ON COMMIT subroutine.

In the debugger, a PERFORM ON COMMIT subroutine shows up as invoked by the runtime processing of COMMIT WORK, not from the line that originally registered it. The subroutine runs in the same work process, but at the moment the SAP LUW closes rather than where the registration happened.

ABAP Cloud and the controlled SAP LUW

ABAP Cloud (the strict ABAP language version used by the SAP BTP ABAP Environment and by the cloud-development model in on-stack S/4HANA) reshapes the SAP LUW around explicit transactional phases rather than around COMMIT WORK registrations. Classic update tasks (CALL FUNCTION ... IN UPDATE TASK) are not part of ABAP Cloud, and FORM/PERFORM is obsolete in the cloud language version with only temporary support, so PERFORM ON COMMIT is not a portable cloud registration pattern.

What replaces them is the controlled SAP LUW, a check mechanism that the runtime applies to detect violations of transactional contracts.7 The contracts say which statements are allowed during which phase, and the application activates the phases explicitly through CL_ABAP_TX:

  • CL_ABAP_TX=>modify( ) starts a modify phase. Code in this phase is meant to compute and stage changes. Database modifications are not permitted in the modify phase at all, even from APIs classified as modify APIs via IF_ABAP_TX_MODIFY; the classified APIs are simply the ones that may be called during a modify phase.
  • CL_ABAP_TX=>save( ) starts a save phase. This is where database modifications are allowed, and it is the equivalent role that update modules play in classic ABAP.

COMMIT WORK and ROLLBACK WORK still exist as statements in the cloud language version, but their use is constrained by the active phase. Inside RAP managed runtime steps (modify operations and the saver sequence), the framework owns the SAP LUW and explicit COMMIT WORK/ROLLBACK WORK is rejected. RAP consumers close successful EML work with COMMIT ENTITIES and discard it with ROLLBACK ENTITIES.8 COMMIT ENTITIES triggers the RAP save sequence and implicitly triggers COMMIT WORK; ROLLBACK ENTITIES resets the transactional buffer and implicitly triggers ROLLBACK WORK.

The practical consequence for code that has to work in ABAP Cloud:

  • Do not port classic update-task patterns. Use unmanaged or managed RAP behavior implementations and let COMMIT ENTITIES close the SAP LUW.
  • For non-RAP transactional code, use CL_ABAP_TX=>modify( ) and CL_ABAP_TX=>save( ) to mark phases, and write persistence code inside the save phase.
  • SET UPDATE TASK LOCAL is irrelevant in ABAP Cloud because the underlying update mechanism does not exist there. In classic ABAP the same statement is still required at the start of every SAP LUW that wants local update; the setting does not persist across COMMIT WORK.

Practical rules for clean transaction boundaries

Most of the rules below follow directly from the action lists. The short version that holds up in practice:

  • Do not call COMMIT WORK in any code that might be reused by another caller. Reusable APIs should register their work and return. The caller decides when the SAP LUW ends.
  • BAdI implementations, exits, enhancements, framework callbacks, and utility classes are reusable code by construction. Treat them as forbidden territory for explicit commits.
  • Use COMMIT WORK AND WAIT when the next line of the program depends on the V1 update result. Use plain COMMIT WORK only when the program has no need to know whether V1 succeeded.
  • Set the lock scope deliberately. _SCOPE = 1 ignores COMMIT WORK and ROLLBACK WORK entirely. _SCOPE = 2 hands the lock to the update task at commit time. _SCOPE = 3 keeps both owners and releases only when both have released. Pick the one that matches the SAP LUW the code actually has.
  • Treat implicit database commits as a known hazard. RFC calls, dialog step ends, ICF roundtrips, ABAP messaging-channel delivery, ABAP Push Channel (APC) sends, and WAIT statements can all close the database LUW even though they do not close the SAP LUW.6 Bundle work that must be atomic into a single in-process step that ends with COMMIT WORK.
  • For unit tests, prefer SET UPDATE TASK LOCAL so V1 updates run in the same work process and surface their effects synchronously. Tests that depend on asynchronous V1 updates are difficult to keep stable.
  • In ABAP Cloud, treat the controlled SAP LUW and CL_ABAP_TX as the primary model, as described in the previous section. The classic update-task patterns are not portable to the cloud language version.

COMMIT WORK started as a convenient shorthand and ended up as the central closing statement of the SAP LUW. Treating it as "save the data" is the part that ages badly. Once the SAP LUW is the unit of thought, the rest of the rules read as the consequences of a coherent design rather than a list of trivia.

Further reading

Sources

Footnotes

  1. SAP Help, Multiversion Concurrency Control (MVCC) Issues; SAP Help, M_VERSION_MEMORY System View; SAP Help, M_CS_MVCC System View

  2. SAP Help, SAP LUW; SAP samples, ABAP Cheat Sheet: SAP LUW

  3. SAP Help, COMMIT WORK

  4. SAP Help, Saving Persistent Objects

  5. SAP Help, ROLLBACK WORK

  6. SAP Help, Database Commit; SAP Help, WAIT UNTIL 2

  7. SAP Help, Controlled SAP LUW; SAP Help, Checking Transactional Consistency with Controlled SAP LUW

  8. SAP Help, COMMIT ENTITIES; SAP Help, LUWs in ABAP