If you have written more than a handful of ABAP RAP behavior definitions, you have declared a determination and had to make a choice: should it run on modify or on save?
It is easy to answer that question by instinct, or by copying whatever pattern sits in the class next to it. The application works either way, at least when you test it or when the key user does. The trouble shows up later, when an edge case comes up that testing never covered, and nobody on the team can explain why the determination was placed where it was.
This is not a syntax problem. Both triggers are declared the same way, and either one will compile and run. It is an architectural decision, and it becomes expensive to get wrong only after the application is already being used heavily in production.
Strip away the keywords for a moment. The real difference between the two triggers comes down to timing: when each one runs, and how many times.
A RAP transaction spends most of its life in the interaction phase. Every operation you send during that phase, create, update, delete, or an action, gets written to the transactional buffer rather than to the database directly. That buffer is what allows a UI to hold several changes open across multiple round trips before anything is actually persisted. A determination declared on modify runs immediately after the operation that triggers it, inside that interaction phase, while the buffer is still open. If a user changes a field three times before saving, and that field is a trigger condition, the determination runs three times, once per change, and every result is visible to the rest of the transaction as soon as it happens.
Saving moves the transaction into the save sequence. One step inside that sequence, the FINALIZE phase, is where the framework settles anything that still needs to be computed before the data is written. A determination declared on save runs there, exactly once, no matter how many times the source data changed on the way to that point. If an instance went through a create and then two updates before the user saved, the determination does not fire three separate times. The framework aggregates the operations performed on that instance into a single effective operation for the purpose of evaluating the trigger condition, and the determination runs once, against the final state.
That is the entire technical difference: which phase the code runs in, and how many times it can fire. Everything else, when to use which, is a question of what depends on the value and when.
Three questions cover almost every case you will run into, and one common instinct is worth resisting.
First: Does anything else in the same transaction need this value to be final before the user saves? That includes other fields on the same screen, other determinations that fire after it, and validations that check it. If the answer is yes, the value has to be available on modify, because on save happens too late for anything else in the transaction to react to it.
Second: Is the source data likely to change more than once before save, and does the user need to see the derived value update as that happens? If someone is actively working with a value, watching it change as they go, on modify is what makes the application feel responsive instead of opaque.
Third, and this is where most people go wrong: resist the urge to decide based on how expensive the calculation looks. A cheap calculation is not automatically an on modify candidate, and an expensive one is not automatically on save. The deciding factor is dependency, not cost, and it is easy to get that backwards even when you think you already know better.
Here is where it went wrong for me.
An outbound delivery in a logistics scenario, built as a RAP composition with handling units as child instances, needs a total weight at header level because the truck has a hard capacity limit. A warehouse worker packs the delivery by adding handling units one at a time.
The obvious move, if you have built a sales order total before, is to treat this the same way: sum the child values, put the determination on save, and let it run once when everything is final. It looks identical to summing item amounts into an order total, and that pattern is on save in most of the examples you will find. So that is where I put it at first.
Then came the question that mattered: what happens if the worker adds handling units past the truck's capacity? With the calculation on save, the answer is uncomfortable. The total weight field stays whatever it was until the save sequence runs. The worker can keep adding units well past the limit, see nothing wrong, and only find out when they try to save and the validation rejects the whole transaction. At that point they are undoing work, not preventing a mistake.
The determination was never expensive. It is a sum over a handful of child instances, nothing close to the kind of calculation that on save is meant to defer. The mistake was copying the order-total pattern because the code looked the same, instead of asking who depends on the result and when. In this scenario, the worker depends on it constantly, mid-transaction, because the whole point of showing a running weight is to catch the overload before it happens. That is squarely an on modify determination, triggered by the handling unit's weight field, so the total updates as units are added.
The same delivery gives a legitimate on save example right next to it: freight cost. Once the final weight and volume are known, the delivery calls out to a rating engine, internal or from a carrier, to calculate what the shipment will cost. This one belongs on save, for reasons that have nothing to do with the weight case. The calculation is genuinely expensive, since it means an external call. Nobody benefits from seeing it update on every handling unit added, because the number is not final until packing is finished. It only matters once, for billing, after the delivery is closed.
Here is a detail worth knowing before you build this: it is tempting to think you can trigger the freight determination only when the delivery reaches a specific status, say "Closed." The trigger condition is coarser than that. A field used as a trigger condition fires whenever that field changes value, not only when it reaches one particular value you care about. If the delivery moves through several statuses before closing, the determination runs on every one of those transitions. The fix is straightforward once you know to look for it: let the determination trigger on every status change, and inside the implementation, check whether the new status is actually the one you care about before doing any work. If it is not, the method simply returns. The trigger condition decides when the code runs. The implementation decides what it does once it is running. Keeping those two separate is what makes the field-as-trigger pattern usable for anything more specific than "any change at all."
Before declaring a determination, work through these in order:
Whatever you decide, do not let the apparent cost of the calculation make that decision for you. A cheap sum and an expensive external call can both belong on either side, depending entirely on who needs the result and when.
The mental model here is not really about ABAP RAP syntax. It is the same discipline any architect applies when deciding what happens now versus what happens later: identify who is waiting on a result, and build the system around that dependency instead of around how the code happens to look. RAP just makes the choice explicit and forces you to name it in the behavior definition. That is a good thing. It means the decision is visible, reviewable, and, with the right questions, easy to get right the first time.
This post was originally published 9/2026.