📝 Status: adopted; 2 statements still to be verified, marked [verify].
These rules build on SAP's Clean ABAP style guide and turn it into numbered, citable team rules, so reviews argue about one shared text instead of personal taste.
Facts about the language — what a statement does, whether it is obsolete, what a release supports — come from the ABAP Keyword Documentation. When a rule and the documentation disagree on a fact, the documentation wins and the rule gets fixed.
A shared rule set keeps code consistent when readers move between guides. The rules apply to:
- ABAPGuide
- GWGuide
- ABAP-Cookbook
- ABAP code in CDSGuide and CDS-Cookbook
- RAPGuide and HANAGuide, once they exist
Existing code is not rewritten just to comply. It is aligned when it is next changed, or in the planned pass for each guide.
📝 These rules govern how code is written. They do not widen a guide's scope. ABAPGuide still leaves out RAP, CDS, AMDP and ABAP Cloud beyond Chapter 21, even though some sections here mention those topics.
An undocumented deviation looks like a mistake. A documented one is a decision a reviewer can find. Whenever a rule here contradicts Clean ABAP, section 14 names the rule and gives the reason.
Rule numbers stay stable, so a citation stays valid after the document grows.
- In reviews, CLAUDE.md files and commit bodies, write
ABAP Development Rules 3.12or shortRule 3.12. - In Markdown, link to the heading anchor, for example
docs/ABAP-Development-Rules.md#312-check-existence-with-line_exists. - A rule that is withdrawn keeps its number and is marked Withdrawn. Its number is never reused.
Rule examples are kept to the minimum that shows the point.
📝 Contextual snippet — unless a rule says otherwise, every example assumes the declarations it refers to (
orders,order,itemsand so on) exist with suitable types. It also assumes Standard ABAP as the target (see 1.4).
Lifecycle:
ABAP CLOUD / MODERN CONTEXT. ABAP language versions decide which statements and which APIs a development object may use. For the on-premise vs cloud split, see 21-Classic-vs-Modern-ABAP.
The ABAP language version is a property of every ABAP program, and of other repository objects that carry it. It decides which language elements and which repository objects the code may use. The syntax check enforces it, so the same source can be valid in one object and rejected in another.
The ABAP Keyword Documentation lists three current versions:
- Standard ABAP: the unrestricted language for classic ABAP.
- ABAP for Cloud Development: the restricted version for ABAP Cloud. It allows only a small set of language elements and restricted access to repository objects, applies the strictest ABAP SQL check mode and enforces client isolation.
- ABAP for Key Users: a restricted version for key-user enhancements. It is outside the scope of these rules.
That language version only accepts SAP objects that are released for cloud development. Code that uses unreleased objects does not compile there, and on premise it gives up the stability contract that a release brings.
An object usable in a restricted language version is released with a release contract, in general the C1 contract. Its release state is either Released or Deprecated. Check that state before you design around an object. A deprecated object names its successor where one exists. For an object that is not released, look for the documented successor.
📝 Target: ABAP for Cloud Development.
" ✅ released CDS view as the data source
SELECT product, producttype
FROM i_product
WHERE product IN @product_range
INTO TABLE @DATA(products).
" ❌ direct read of a standard table, which is not to be released
SELECT matnr, mtart
FROM mara
WHERE matnr IN @product_range
INTO TABLE @DATA(products).In SAP's list of released objects, I_PRODUCT is released, and MARA is classified as not to be released, with I_PRODUCT among its successors. I_Product has the fields Product and ProductType used above.
A released API has a stability contract and keeps a later move to ABAP for Cloud Development open. An unreleased one can change in any upgrade.
This is a team rule, not a Clean ABAP statement. Where no released API exists, Standard ABAP may use the classic one. Chapter 21 explains why that is normal on premise.
A reader needs to know whether an example compiles in a cloud object before copying it.
- Put
> 📝 **Target:** ABAP for Cloud Development.or> 📝 **Target:** Standard ABAP.directly above any example whose validity depends on the language version. - An example without a target line is Standard ABAP.
- Examples that are valid in both versions need no target line.
"Cloud-ready" is a factual claim. Make it only after the object compiled under ABAP for Cloud Development, or after ATC with the released check variant ABAP_CLOUD_READINESS passed, and say which of the two you did.
The name should tell the reader what a thing means. The type is already visible to the compiler and the IDE, and prefixes go stale when a variable's scope or type changes.
Clean ABAP recommends avoiding encodings such as Hungarian notation and prefixes, using descriptive names, and using snake_case.
" ✅
DATA open_orders TYPE zsm_tt_order.
METHODS cancel_order IMPORTING order_id TYPE zsm_e_order_id.
" ❌
DATA gt_orders TYPE zsm_tt_order.
METHODS cancel IMPORTING iv_id TYPE zsm_e_order_id.Lifecycle:
CLASSIC BUT STILL RELEVANT. Prefixes such aslv_,lt_andiv_are everywhere in existing code, so readers must still be able to read them. They are not used in new code. See 20-Best-Practices.
The grammar of a name signals its role, so a call reads like a sentence: validator->check( orders ).
Clean ABAP recommends nouns for classes and verbs for methods, and plural names. Boolean variables and methods read as questions, for example is_blocked or has_open_items.
" ✅
DATA(blocked_customers) = customer_repository->read_blocked( ).
IF order_validator->is_valid( order ) = abap_true.
" ❌
DATA(customer_tab) = customer_handler->blocked( ).
IF order_validation->validity( order ) = abap_true.Abbreviations and filler words such as data, info and object make names longer without making them clearer.
ABAP's name-length limits sometimes force abbreviations. Clean ABAP recommends avoiding them, and using the same abbreviation everywhere when they are unavoidable.
" ✅
DATA(delivery_date) = order-requested_delivery_date.
" ❌
DATA(dlv_dt_info) = order-requested_delivery_date.Renaming is impossible, or it breaks the contract, when the name comes from somebody else's interface. Examples:
- SEGW-generated methods and types
- BAPI and function module parameters
- inherited methods
- interface methods
Only the names you introduce yourself follow 2.1.
" ✅ the redefinition keeps the generated names; local names follow 2.1
METHOD orderset_get_entityset.
DATA(open_orders) = order_repository->read_open( ).
et_entityset = CORRESPONDING #( open_orders ).
ENDMETHOD.A class, function group or program that mixes two naming styles is harder to read than one that sticks to either.
- New development objects follow 2.1 in full.
- A change inside an existing development object that uses prefixes throughout keeps that object's style. This includes new methods added to it.
- When the object is refactored as a whole, rename all of it to 2.1 at once.
In its section on refactoring legacy code, Clean ABAP advises against mixing styles within the same development object. It also notes that in legacy projects its prefix rule may be better left aside.
This exception is for projects that adopt these rules. The guides' own examples do not use it: they are migrated to 2.1 in full in each guide's planned pass.
A fixed infix per object type tells the reader what kind of object a name refers to, even outside the IDE.
zsm is the placeholder namespace used in the guides. Real projects substitute their own.
| Object type | Pattern | Example |
|---|---|---|
| Class | zcl_zsm_ |
zcl_zsm_order_validator |
| Interface | zif_zsm_ |
zif_zsm_order_repository |
| Exception class | zcx_zsm_ |
zcx_zsm_order_not_found |
| Message class | zsm_msg |
zsm_msg |
| Function module | zsm_fm_ |
zsm_fm_read_orders |
| Database table | zsm_t_ |
zsm_t_order |
| Structure | zsm_s_ |
zsm_s_order |
| Table type | zsm_tt_ |
zsm_tt_order |
| Data element | zsm_e_ |
zsm_e_order_id |
| CDS view | ZSM_I_ |
ZSM_I_Order |
| CDS projection view | ZSM_C_ |
ZSM_C_Order |
| CDS table function | ZSM_F_ |
ZSM_F_OrderAging |
| Report | zsm_r_ |
zsm_r_order_overview |
| Lock object | EZSM_ |
EZSM_ORDER |
| Checkpoint group | zsm_cp_ |
zsm_cp_order |
| Local class | lcl_ |
lcl_main |
| Local interface | lif_ |
lif_order_source |
| Local test class | ltc_ |
ltc_release |
| Local test helper | lth_ |
lth_order_builder |
📝 Lock object names must start with
E, so the lock object row putsEin front of the namespace instead of using a lower-case infix.
📝 Checkpoint groups are maintained in transaction
SAABand controlASSERT,BREAK-POINTandLOG-POINTwith the additionID(6.12, 13.6). [verify: the maximum length of a checkpoint group name]
📝 Local types live inside one program, so they carry a short type prefix without the namespace. These prefixes are common SAP practice, and Clean ABAP itself names local test classes
ltc_and test helperslth_. Section 14 records this as a deviation.
Guides that still use other prefixes are aligned in their planned pass.
Lifecycle:
CURRENT / RECOMMENDED. Expression syntax from the 7.40 generation onward is the default for new on-premise code. Individual additions arrived at different times. See 21-Classic-vs-Modern-ABAP.
⚠️ VERSION-DEPENDENT: constructor expressions and their additions. Not every expression or addition below exists in every release. Check the ABAP Keyword Documentation for your target before relying on one.
An inline declaration puts the type where the value comes from and keeps the variable's scope visibly small. Clean ABAP recommends inline declarations over up-front declarations.
" ✅
SELECT order_id, status
FROM zsm_t_order
WHERE customer_id = @customer_id
INTO TABLE @DATA(orders).
" ❌
DATA lt_orders TYPE STANDARD TABLE OF zsm_t_order.
SELECT * FROM zsm_t_order INTO TABLE lt_orders WHERE customer_id = customer_id.The type of an inline variable comes from the right-hand side. A text literal produces a c field exactly as long as the literal, which silently truncates longer values assigned later.
" ✅
DATA(message_text) = `Order released`.
DATA(total) = CONV zsm_e_amount( 0 ).
" ❌ message_text becomes c LENGTH 14; total becomes type i
DATA(message_text) = 'Order released'.
DATA(total) = 0.FINAL( ) documents that a value does not change, and the compiler enforces it.
⚠️ VERSION-DEPENDENT:FINALinline declarations. Available only in newer releases. Where it is missing, useDATA( ). Check the ABAP Keyword Documentation.
" ✅
FINAL(today) = cl_abap_context_info=>get_system_date( ).A single VALUE expression shows the whole content at once and needs no helper work area.
" ✅
DATA(items) = VALUE zsm_tt_item( ( item_no = 10 quantity = 2 )
( item_no = 20 quantity = 5 ) ).
" ❌
DATA item TYPE zsm_s_item.
DATA items TYPE zsm_tt_item.
item-item_no = 10.
item-quantity = 2.
APPEND item TO items.
item-item_no = 20.
item-quantity = 5.
APPEND item TO items.Use BASE to add lines to a table that already has content. Without it, VALUE builds a new table and the old lines are lost.
CORRESPONDING states the mapping in one expression. Without BASE, it starts from an initial target, so fields that the source does not fill are cleared. MOVE-CORRESPONDING behaves differently: it leaves unmatched target fields unchanged.
" ✅ target already holds values that must be kept
target = CORRESPONDING #( BASE ( target ) source MAPPING customer_id = kunnr ).
" ❌ in that situation: every target field not in source is reset
target = CORRESPONDING #( source MAPPING customer_id = kunnr ).NEW creates and assigns in one expression and works inline. Clean ABAP recommends NEW over CREATE OBJECT.
" ✅
DATA(validator) = NEW zcl_zsm_order_validator( order ).
" ❌
DATA validator TYPE REF TO zcl_zsm_order_validator.
CREATE OBJECT validator EXPORTING order = order.CONV removes helper variables that exist only to change a type. EXACT raises an exception instead of silently rounding or truncating. Examples are cx_sy_conversion_rounding for a calculation that would round, and a subclass of cx_sy_conversion_error for an invalid value.
" ✅
DATA(page_count) = pager->count_pages( CONV i( total_lines ) ).
" ❌
DATA lv_lines TYPE i.
lv_lines = total_lines.
DATA(page_count) = pager->count_pages( lv_lines ).The expression makes clear that every branch produces a value for the same target. Keep IF and CASE for branches that do work.
CONDhandles arbitrary conditions.SWITCHcompares one operand for equality.
This is a team rule. Clean ABAP does not prescribe it in a rule of its own.
" ✅
DATA(fee) = COND zsm_e_amount( WHEN order-express = abap_true THEN express_fee
ELSE standard_fee ).
" ❌
DATA fee TYPE zsm_e_amount.
IF order-express = abap_true.
fee = express_fee.
ELSE.
fee = standard_fee.
ENDIF.REDUCE states the start value, the iteration and the step together. Once the step needs several statements or side effects, a LOOP is easier to read and to debug. This is a team rule.
" ✅
DATA(total) = REDUCE zsm_e_amount( INIT sum = CONV zsm_e_amount( 0 )
FOR item IN items
NEXT sum = sum + item-amount ).FILTER is efficient because it works through a table key, and it only compiles when a suitable key exists. The requirement is described in 07-Internal-Tables. Without such a key, VALUE … FOR … WHERE does the same job.
In the basic form, FILTER uses the primary key when USING KEY is omitted. A table whose primary key is neither sorted nor hashed must therefore name a secondary key. The WHERE condition depends on the key:
- Hashed key: each key component is compared with
=. - Sorted key: an initial part of the key is covered, and any comparison operator is allowed.
" ✅ orders has a sorted secondary key by_status on status
DATA(open_orders) = FILTER #( orders USING KEY by_status WHERE status = status_open ).
" ✅ no suitable key
DATA(open_orders) = VALUE zsm_tt_order( FOR order IN orders
WHERE ( status = status_open )
( order ) ).A template shows the final text in reading order, and formatting options replace conversion code. Clean ABAP recommends | for assembling text and backticks for string literals.
" ✅
DATA(log_text) = |Order { order_id ALPHA = OUT } released by { user_name }|.
" ❌
CONCATENATE 'Order' order_id 'released by' user_name INTO log_text SEPARATED BY space.💡 Text a user reads in their logon language belongs in a message class or text symbols, not in a template literal. See 4.2.
line_exists states the intent directly and needs no sy-subrc. Clean ABAP recommends line_exists over READ TABLE and LOOP AT for this purpose.
" ✅
IF line_exists( orders[ order_id = order_id ] ).
...
ENDIF.
" ❌
READ TABLE orders TRANSPORTING NO FIELDS WITH KEY order_id = order_id.
IF sy-subrc = 0.
...
ENDIF.If no line matches, a table expression raises cx_sy_itab_line_not_found. When nothing catches it, missing data becomes a short dump. You have three ways to handle a miss:
- add
OPTIONAL, which gives an initial result; - add
DEFAULT, which gives a fallback value; - catch the exception where a miss is an error.
" ✅ a missing customer is allowed
DATA(customer) = VALUE #( customers[ customer_id = order-customer_id ] OPTIONAL ).
" ✅ fall back to a known value
DATA(setting) = VALUE #( plant_settings[ plant = plant ] DEFAULT default_setting ).
" ❌ a missing customer dumps
DATA(customer) = customers[ customer_id = order-customer_id ].📝
ASSIGN itab[ … ] TO <line>does not raise the exception. It setssy-subrcinstead, so checksy-subrcafter it.
Each table expression is a separate table read. Repeating one costs time and hides the fact that the same line is meant. Clean ABAP recommends avoiding unnecessary table reads.
" ✅
DATA(customer) = VALUE #( customers[ customer_id = customer_id ] OPTIONAL ).
DATA(address_line) = |{ customer-name }, { customer-city }|.
" ❌
DATA(address_line) = |{ customers[ customer_id = customer_id ]-name }, | &&
|{ customers[ customer_id = customer_id ]-city }|.A functional call can be used inside expressions and reads like a function. Clean ABAP recommends functional calls over CALL METHOD. Where possible, it also recommends dropping RECEIVING, dropping the EXPORTING keyword, and dropping the parameter name in single-parameter calls.
" ✅
DATA(is_valid) = validator->is_valid( order ).
" ❌
CALL METHOD validator->is_valid
EXPORTING
order = order
RECEIVING
result = is_valid.Obsolete statements remain only for compatibility. They have better replacements, and many of them are not available in restricted language versions. Clean ABAP also recommends avoiding obsolete language elements.
Lifecycle:
LEGACY / HISTORICAL REFERENCE. The constructs below may appear in examples only with this label and paired with the replacement. See 21-Classic-vs-Modern-ABAP.
| Obsolete construct (typical examples) | Write instead |
|---|---|
MOVE a TO b |
b = a |
REFRESH itab |
CLEAR itab |
Tables with header lines (WITH HEADER LINE, OCCURS) |
A table type plus a separate work area or field symbol |
Subroutines (FORM / PERFORM) |
Methods |
Host variables in ABAP SQL without the @ escape |
@variable with the strict syntax |
📝 The length in parentheses (
DATA text(10) TYPE c) is not classified as obsolete. The ABAP Keyword Documentation recommendsLENGTHfor legibility, so writeDATA text TYPE c LENGTH 10. Only length specifications for the fixed-length typesd,f,iandtare listed as obsolete.
This table lists typical cases only. For the full list, see the obsolete language elements section of the ABAP Keyword Documentation, and do not label a construct obsolete unless the documentation does.
CHECK behaves differently depending on where it stands. Inside a loop, it ends only the current loop pass. Outside a loop, it leaves the whole processing block. A reader has to know which case applies, and the keyword does not say what happens when the condition is false.
Clean ABAP records that there is no consensus on CHECK versus RETURN at the start of a method, and finds the long form easier to understand. It also advises against CHECK anywhere other than the initialization section of a method. In loops, it recommends IF with CONTINUE, because CONTINUE can only mean the loop.
📝 The ABAP Programming Guidelines allow
CHECKat the start of a procedure. The keyword documentation forCHECKin loops recommends usingCHECKinside loops only. Clean ABAP notes that its loop rule contradicts this. This is a style choice, not a fact about the language, so this rule follows Clean ABAP.
Before returning early, consider whether returning nothing is the right result. Clean ABAP points out that a method should usually fill its result or raise an exception (6.1).
" ✅
METHOD read_open_orders.
IF customer_ids IS INITIAL.
RETURN.
ENDIF.
...
ENDMETHOD.
LOOP AT orders INTO DATA(order).
IF order-status <> status_open.
CONTINUE.
ENDIF.
...
ENDLOOP.
" ❌ ends only the current loop pass; readers expect it to leave the method
LOOP AT orders INTO DATA(order).
CHECK order-status = status_open.
...
ENDLOOP.Access through an empty reference or field symbol ends the program. The ABAP Keyword Documentation describes these cases:
- Dereferencing a data reference that contains the null reference raises the uncatchable exception
DATREF_NOT_ASSIGNED. - Accessing an attribute through an object reference that contains the null reference raises the uncatchable exception
OBJECTS_OBJREF_NOT_ASSIGNED. - Calling an instance method through such a reference raises the catchable exception
CX_SY_REF_IS_INITIAL. - A field symbol must have a memory area assigned before it is used as an operand; otherwise an exception is raised.
IS BOUND is true only if a reference can be dereferenced or points to an object. It therefore also detects a data reference to a table line that has since been deleted. IS ASSIGNED is true if a memory area is assigned to the field symbol. After ASSIGN, check sy-subrc instead (see the note in 3.13).
Check only where the reference or field symbol can actually be empty, for example a lazily created object or a reference passed in as optional. A dependency stored by the constructor (5.4, 5.12) needs no check at every use. This is a team rule.
" ✅ the cache is created on first use
IF order_cache IS NOT BOUND.
order_cache = NEW zcl_zsm_order_cache( ).
ENDIF.
IF order_ref IS BOUND.
DATA(order) = order_ref->*.
ENDIF.
" ❌ if order_ref is initial, the dereference raises an uncatchable exception
DATA(order) = order_ref->*.A macro has no context of its own and cannot be executed step by step in the ABAP Debugger (see 23-Debugging-Troubleshooting). Errors in larger macros are therefore very hard to analyze.
The ABAP Programming Guidelines allow macros only in exceptional cases, and recommend methods or expressions instead. Many typical macros only fill internal tables, and a VALUE expression (3.4) replaces them. The guidelines also say that no new macros should be defined in type pools or in the table TRMAC. Macros are not classified as obsolete.
This rule is stricter than the guidelines: new code contains no macros. Existing macros are replaced when the code around them is changed (0.2). Clean ABAP has no section on macros. This is a team rule.
" ✅
DATA(items) = VALUE zsm_tt_item( ( item_no = 10 quantity = 2 )
( item_no = 20 quantity = 5 ) ).
" ❌
DEFINE add_item.
APPEND VALUE #( item_no = &1 quantity = &2 ) TO items.
END-OF-DEFINITION.
add_item 10 2.
add_item 20 5.A named constant tells the reader what a value means, and it changes in one place. Clean ABAP recommends constants instead of magic numbers, and descriptive names for constants as well.
Literals whose meaning is obvious from the code may stay, for example 0, 1, an empty string or test data.
" ✅
IF order-fee_type = zcl_zsm_order_fees=>fee_type-express.
" ❌
IF order-fee_type = 'EXP'.A literal is not translated. Text that a user reads belongs in a message class (zsm_msg) or in text symbols, so it follows the logon language.
" ✅
MESSAGE e001(zsm_msg) WITH order_id.
" ❌
MESSAGE |Order { order_id } not found| TYPE 'E'.Constants placed next to the logic that uses them are found where they are needed. A structured constant groups related values under one name.
Avoid two patterns:
- a global "all constants" include;
- an interface that holds only constants and is implemented just to reach them without qualification.
Clean ABAP puts native ENUM types first (see 4.4). For enumeration patterns written by hand, its enumerations sub-page prefers classes to interfaces. If neither is used, it recommends at least grouping the constants, as in the example below.
" ✅
CLASS zcl_zsm_order_fees DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
CONSTANTS:
BEGIN OF fee_type,
express TYPE zsm_e_fee_type VALUE 'EXP',
standard TYPE zsm_e_fee_type VALUE 'STD',
END OF fee_type.
ENDCLASS.
" ❌
CONSTANTS gc_exp TYPE c LENGTH 3 VALUE 'EXP'. " in a shared includeThe compiler then accepts only the declared values, and invalid states cannot be assigned. Clean ABAP recommends ENUM over constants interfaces.
⚠️ VERSION-DEPENDENT: enumeration types (BEGIN OF ENUM). Not available in older releases. Where they are missing, use grouped constants as in 4.3. Check the ABAP Keyword Documentation.
" ✅
TYPES:
BEGIN OF ENUM order_status,
created,
released,
completed,
END OF ENUM order_status.
DATA status TYPE order_status.
status = released.
" ❌
DATA status TYPE c LENGTH 1.
status = 'R'.Without a base type, the internal values are an implementation detail. A database field or an interface needs stable values, and a base type with explicit VALUE additions provides them.
The ABAP Keyword Documentation sets these rules:
- The base type is a flat elementary type of at most 16 bytes. Without
BASE TYPE, it isi. VALUEis given for all values or for none, and exactly one value isVALUE IS INITIAL.CONV base_type( … )returns the stored value, andCONV enum_type( … )converts a valid stored value back.
" ✅
TYPES:
BEGIN OF ENUM order_status BASE TYPE zsm_e_status_code,
undefined VALUE IS INITIAL,
created VALUE 'C',
released VALUE 'R',
END OF ENUM order_status.
order_row-status_code = CONV zsm_e_status_code( status ).abap_bool with abap_true and abap_false is the shared Boolean convention in ABAP. Comparing against these constants says "this is a Boolean". Comparing with 'X', space or IS INITIAL hides that.
Clean ABAP recommends abap_bool for Booleans and abap_true / abap_false for comparisons.
For database fields and other Dictionary types, use the released data element abap_boolean. The data elements boole_d and xfeld are deprecated in the released-API list, with abap_boolean named as their successor.
" ✅
DATA is_blocked TYPE abap_bool.
IF is_blocked = abap_true.
...
ENDIF.
" ❌
DATA lv_blocked TYPE c LENGTH 1.
IF lv_blocked = 'X'.
...
ENDIF.
IF lv_blocked IS NOT INITIAL.
...
ENDIF.xsdbool turns a logical expression into abap_true or abap_false in one line. Clean ABAP recommends xsdbool for setting Boolean variables.
Do not use boolc for this. It returns a string, which compares unreliably with abap_false.
" ✅
DATA(is_overdue) = xsdbool( invoice-due_date < today AND invoice-paid = abap_false ).
" ❌
IF invoice-due_date < today AND invoice-paid = abap_false.
is_overdue = abap_true.
ELSE.
is_overdue = abap_false.
ENDIF.Two flags allow four combinations, some of which are invalid, and adding a third state means adding another flag. Clean ABAP advises using Booleans with care. An enumeration (4.4) states the real set of values.
" ✅
DATA status TYPE order_status.
" ❌ is_released = abap_false and is_completed = abap_true is possible
DATA is_released TYPE abap_bool.
DATA is_completed TYPE abap_bool.Lifecycle:
CURRENT / RECOMMENDED. ABAP Objects is the default for new code. Function modules and reports are still needed as entry points. See 10-Objects and 21-Classic-vs-Modern-ABAP.
Classes give you encapsulation, interfaces and test seams that function groups cannot offer. A wrapper keeps the procedural call protocol in one place instead of in every caller.
Clean ABAP recommends object orientation over procedural programming. Where a procedural object is unavoidable, it recommends that the object do little more than call a class. Unavoidable cases include:
- an RFC-enabled function module
- an update function module
- the report behind a transaction
" ✅ callers use one typed method; the BAPI protocol stays inside the wrapper
DATA(order_id) = sales_order_api->create( order ).
" ❌ every caller repeats the BAPI call and its RETURN handling
CALL FUNCTION 'BAPI_SALESORDER_CREATEFROMDAT2'
EXPORTING
order_header_in = header
IMPORTING
salesdocument = order_id
TABLES
return = messages
order_items_in = items.A class that is open for subclassing makes a promise about its protected members and its behaviour, and that promise has to be kept. FINAL states that no such promise exists. Clean ABAP recommends FINAL for classes not designed for inheritance.
Test doubles then come from interfaces (5.4), not from subclasses.
" ✅
CLASS zcl_zsm_order_validator DEFINITION PUBLIC FINAL CREATE PUBLIC.
" ❌ open for inheritance by accident
CLASS zcl_zsm_order_validator DEFINITION PUBLIC CREATE PUBLIC.A static method cannot be replaced by a test double and cannot be reached through an interface. Its callers are therefore welded to the implementation.
Clean ABAP recommends objects over static classes and instance methods over static ones. It accepts plain, stateless type utilities as the exception. Static factory methods that return an instance are also fine.
" ✅
DATA(fee) = fee_calculator->calculate( order ).
" ❌
DATA(fee) = zcl_zsm_fee_calculator=>calculate( order ).When a class receives its collaborators as interface references, tests can hand in doubles and production can swap implementations without changing the class.
Clean ABAP recommends that public instance methods belong to an interface. In its testing section, it recommends passing dependencies to the constructor rather than through setters.
" ✅
CLASS zcl_zsm_order_service DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_zsm_order_service.
METHODS constructor
IMPORTING repository TYPE REF TO zif_zsm_order_repository.
PRIVATE SECTION.
DATA repository TYPE REF TO zif_zsm_order_repository.
ENDCLASS.
" ❌ the dependency is created inside and cannot be replaced
METHOD zif_zsm_order_service~release.
DATA(repository) = NEW zcl_zsm_order_db_repository( ).
...
ENDMETHOD.Small objects combined through references can be understood, reused and changed one at a time. An inheritance hierarchy ties all its levels together. Clean ABAP recommends composition over inheritance.
" ✅ the service uses a notifier
DATA notifier TYPE REF TO zif_zsm_notifier.
" ❌ the service inherits from a notifier only to reuse its methods
CLASS zcl_zsm_order_service DEFINITION PUBLIC INHERITING FROM zcl_zsm_notifier.Every public member is part of the class's contract and is hard to remove later.
Clean ABAP recommends making members PRIVATE by default, using PROTECTED only when it is needed, and using READ-ONLY sparingly. Expose behaviour through methods (ideally through an interface, 5.4), not through attributes.
" ✅
PUBLIC SECTION.
INTERFACES zif_zsm_order_service.
PRIVATE SECTION.
DATA repository TYPE REF TO zif_zsm_order_repository.
" ❌
PUBLIC SECTION.
DATA repository TYPE REF TO zif_zsm_order_repository.
DATA last_error TYPE string.A method that reads like a list of named steps can be understood without opening each step.
Clean ABAP recommends methods that:
- do one thing;
- descend only one level of abstraction;
- stay small.
" ✅
METHOD zif_zsm_order_service~release.
DATA(order) = repository->read( order_id ).
validator->check_releasable( order ).
repository->set_status( order_id = order_id
status = released ).
notifier->order_released( order ).
ENDMETHOD.The ❌ version of this method would contain the SELECT, the validation rules, the UPDATE and the mail text in one body. It is left out because of its length.
Each additional parameter multiplies the combinations a caller has to understand and a test has to cover. Clean ABAP recommends few IMPORTING parameters, ideally fewer than three.
When the method needs more data, group parameters that belong together into a structure. Split the method instead of adding OPTIONAL parameters.
" ✅
METHODS create
IMPORTING order TYPE zsm_s_order
RETURNING VALUE(result) TYPE zsm_e_order_id.
" ❌
METHODS create
IMPORTING customer_id TYPE zsm_e_customer_id
order_type TYPE zsm_e_order_type
sales_org TYPE zsm_e_sales_org
delivery_date TYPE zsm_e_delivery_date
priority TYPE zsm_e_priority OPTIONAL
RETURNING VALUE(result) TYPE zsm_e_order_id.A RETURNING parameter allows functional calls (3.15) and makes it clear that the method produces one result.
Clean ABAP recommends:
RETURNINGoverEXPORTING;- naming the returning parameter
result; - not combining
RETURNING,EXPORTINGandCHANGINGin one signature.
It also notes that returning large tables is usually acceptable.
" ✅
METHODS read_open
RETURNING VALUE(result) TYPE zsm_tt_order.
" ❌
METHODS read_open
EXPORTING orders TYPE zsm_tt_order.A CHANGING parameter hides the fact that the caller's data is modified, and it cannot be used in expressions. Clean ABAP recommends using CHANGING sparingly and only where it fits.
The case where it fits is adjusting a value in place that the caller already holds. Everything else returns a new value.
" ✅
DATA(priced_items) = pricing->price( items ).
" ❌
pricing->price( CHANGING items = items ).A flag that selects between two behaviours means the method does two things, and every call site needs a comment to explain the abap_true. Clean ABAP recommends splitting the method instead.
" ✅
printer->print( document ).
printer->preview( document ).
" ❌
printer->output( document = document
preview = abap_true ).A constructor runs on every instantiation and cannot be skipped or replaced in a test. It should only store dependencies and check its input.
Reading data, posting documents or calling remote systems belong in methods that the caller invokes on purpose. This is a team rule; Clean ABAP has no rule of its own for it.
" ✅
METHOD constructor.
me->repository = repository.
ENDMETHOD.
" ❌
METHOD constructor.
me->repository = repository.
orders = repository->read_open( ).
send_reminders( orders ).
ENDMETHOD.Lifecycle:
CURRENT / RECOMMENDED. Class-based exceptions are the current mechanism. For the statements themselves, see 18-Debugging.
A class-based exception carries:
- a type that the caller can catch selectively;
- a translatable text;
- typed attributes;
- a cause chain.
A return code carries none of these. Clean ABAP recommends class-based exceptions and exceptions over return codes.
Lifecycle:
CLASSIC BUT STILL RELEVANT. Classic exceptions (EXCEPTIONSin a function module interface, evaluated throughsy-subrc) appear at every call of an existing function module. Handle them as in 6.9.
The ABAP Keyword Documentation says non-class-based exceptions should no longer be defined in new developments. Its programming guidelines call them obsolete on the raising side. Do not define new ones, except in RFC-enabled function modules: remote calls support only classic exception handling.
" ✅
METHODS read
IMPORTING order_id TYPE zsm_e_order_id
RETURNING VALUE(result) TYPE zsm_s_order
RAISING zcx_zsm_order_not_found.
" ❌
METHODS read
IMPORTING order_id TYPE zsm_e_order_id
EXPORTING order TYPE zsm_s_order
return_code TYPE sysubrc.Own superclasses let a caller catch "anything from this component" in one clause. Subclasses let it tell specific situations apart.
Clean ABAP recommends own superclasses, and subclasses for situations that callers need to distinguish. Each category you use (6.3) needs its own abstract root, because a class can inherit from only one of the three categories. Naming one root per category is a team rule.
" ✅
CLASS zcx_zsm_static_error DEFINITION PUBLIC ABSTRACT
INHERITING FROM cx_static_check CREATE PUBLIC.
CLASS zcx_zsm_order_not_found DEFINITION PUBLIC FINAL
INHERITING FROM zcx_zsm_static_error CREATE PUBLIC.
" ❌ every exception inherits directly from a framework class
CLASS zcx_zsm_order_not_found DEFINITION PUBLIC
INHERITING FROM cx_static_check CREATE PUBLIC.The category decides whether the compiler forces callers to deal with the exception. Choosing it is a design decision, not a default.
| Category | Use it when | Clean ABAP |
|---|---|---|
CX_STATIC_CHECK |
The caller can reasonably handle the error, for example invalid input or a missing record that has a fallback. | Recommended for manageable exceptions |
CX_NO_CHECK |
Nobody can reasonably recover, for example a missing must-have configuration or a dependency that cannot be resolved. | Recommended for usually unrecoverable situations |
CX_DYNAMIC_CHECK |
Rarely. Only when the caller fully controls whether the error can occur, and the reason is written in the class's ABAP Doc. | Described as rarely needed |
A CX_STATIC_CHECK exception must be declared in RAISING, and callers must catch or forward it.
A CX_NO_CHECK exception does not need to be declared, because it can always propagate. It may still be declared explicitly to document that it can occur. Clean ABAP states that it cannot be declared; the ABAP Keyword Documentation takes precedence here (0.1).
A violated internal assumption is a programming error, not an exception category: use ASSERT (6.12).
T100 messages are translated, can be found with a where-used list, and can be passed on unchanged into BAPIRET2 tables or the application log.
Implement if_t100_dyn_msg in your exception classes, and keep the texts in zsm_msg.
The MESSAGE addition of RAISE EXCEPTION accepts any class that implements if_t100_message. Its full functionality, including placeholders filled with WITH, requires if_t100_dyn_msg, which includes if_t100_message.
" ✅
CLASS zcx_zsm_static_error DEFINITION PUBLIC ABSTRACT
INHERITING FROM cx_static_check CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_t100_dyn_msg.
ENDCLASS.
" ❌ text built in code and passed to an own string attribute: not translated, not findable
RAISE EXCEPTION NEW zcx_zsm_order_not_found( message_text = |Order { order_id } missing| ).NEW keeps the raise short. The MESSAGE addition binds the message class, number and placeholders at the raise point, where a where-used list finds them.
MESSAGE cannot be combined with NEW. RAISE EXCEPTION NEW cx( … ) is the variant with an existing object reference, and the ABAP Keyword Documentation allows MESSAGE only together with TYPE.
Clean ABAP recommends RAISE EXCEPTION NEW over RAISE EXCEPTION TYPE. It notes that code which uses the MESSAGE addition heavily may stay with the TYPE form.
⚠️ VERSION-DEPENDENT:RAISE EXCEPTION NEWand theMESSAGEaddition. Both depend on the release. Check the ABAP Keyword Documentation.
" ✅ no message needed
RAISE EXCEPTION NEW zcx_zsm_order_not_found( previous = sql_error ).
" ✅ with a T100 message
RAISE EXCEPTION TYPE zcx_zsm_order_not_found
MESSAGE e002(zsm_msg) WITH order_id.
" ❌
RAISE EXCEPTION TYPE zcx_zsm_order_not_found
EXPORTING
previous = sql_error.A broad CATCH also catches programming errors and turns them into handled cases. Catch the classes you can actually handle, ordered from the most specific to the most general.
cx_root is caught only at an outermost boundary, such as a job step, an RFC entry point or a request handler. There it is logged and either re-raised or the processing ends cleanly. This is a team rule; see 18-Debugging.
" ✅
TRY.
order_service->release( order_id ).
CATCH zcx_zsm_order_not_found INTO DATA(not_found).
log->add( not_found ).
ENDTRY.
" ❌
TRY.
order_service->release( order_id ).
CATCH cx_root.
ENDTRY.An empty CATCH makes a failure disappear without a trace, and the next symptom shows up far from its cause.
Every CATCH must do at least one of three things:
- handle the error;
- convert it (6.8);
- log it.
This is a team rule.
" ✅
CATCH zcx_zsm_mail_failed INTO DATA(mail_error).
" the order is released even if the notification fails; the log tells support
log->add( mail_error ).
" ❌
CATCH zcx_zsm_mail_failed.The original exception holds the technical detail that support needs. previous keeps it reachable from the new one.
Clean ABAP recommends wrapping exceptions from other components in your own types instead of letting them pass through your signatures.
" ✅
CATCH cx_sy_open_sql_db INTO DATA(sql_error).
RAISE EXCEPTION NEW zcx_zsm_order_not_saved( previous = sql_error ).
" ❌ the cause is lost
CATCH cx_sy_open_sql_db.
RAISE EXCEPTION NEW zcx_zsm_order_not_saved( ).Classic results are read once, in the wrapper (5.1), so callers deal with only one error mechanism. Clean ABAP recommends exceptions over return codes and wrapping foreign errors.
The same applies to BAPIs: check the RETURN table in the wrapper and raise when it contains a message of type E (error) or A (abort), the documented error types. Treat X as an error too; that is a defensive extra check, not a documented value. For how to evaluate the table, see 15-BAPIs.
" ✅
CALL FUNCTION 'ZSM_FM_READ_ORDERS'
EXPORTING
customer_id = customer_id
IMPORTING
orders = orders
EXCEPTIONS
not_found = 1
OTHERS = 2.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_zsm_order_not_found
MESSAGE e003(zsm_msg) WITH customer_id.
ENDIF.
" ❌ the return code travels on to the caller
result_code = sy-subrc.An exception says that something went wrong. Using one for an expected case hides the real errors and costs runtime. Clean ABAP states that exceptions are for errors, not for regular cases.
" ✅
IF line_exists( orders[ order_id = order_id ] ).
...
ENDIF.
" ❌
TRY.
DATA(order) = orders[ order_id = order_id ].
...
CATCH cx_sy_itab_line_not_found.
" not found is a normal case here
ENDTRY.What a MESSAGE statement does depends on the context it runs in: a dialog, a background job, an RFC call or an HTTP request. Code below the UI cannot know which context it will run in.
This is a team rule. Lower layers raise exceptions (6.5), and the UI layer turns them into messages. MESSAGE … INTO only fills the system fields and a variable without sending anything, so it may be used anywhere, for example to fill a log. Clean ABAP uses this form so that messages stay findable.
" ✅ below the UI
MESSAGE e004(zsm_msg) WITH order_id INTO DATA(message_text).
log->add_system_message( ).
" ❌ below the UI
MESSAGE e004(zsm_msg) WITH order_id.6.12 State the internal assumptions of a program with ASSERT; raise exceptions for situations a caller or user can act on
An assumption that the program itself guarantees, such as a total that must match its items, is not an error a caller can handle. If it is false, the program has a defect, and continuing would only spread wrong data. The ABAP Programming Guidelines recommend assertions for these checks. ASSERT ends the program with the runtime error ASSERTION_FAILED when its condition is false. With ID, a checkpoint group (2.6) controls it from outside the program.
The split between the three ways to stop or guard a program is a team rule:
| Situation | Use |
|---|---|
| An internal assumption is violated — a programming error | ASSERT |
| A runtime situation that nobody in the call chain can fix, such as missing must-have configuration | an exception of category CX_NO_CHECK (6.3), logged by the boundary handler (6.6, 6.8) |
| A deliberate termination whose short dump must carry the cause chain | RAISE SHORTDUMP with a PREVIOUS exception |
Anything a caller or user can act on, such as invalid input, raises an exception (6.3). The condition of an assertion has no side effects; the ABAP Keyword Documentation requires this for functional methods in it. ASSERT and RAISE SHORTDUMP are allowed in Standard ABAP and in ABAP for Cloud Development.
" ✅ the method built both tables itself; a mismatch is a defect here
ASSERT lines( package_totals ) = lines( packages ).
" ❌ input from the caller is not an internal assumption
ASSERT order_id IS NOT INITIAL.Lifecycle:
CURRENT / RECOMMENDED. The explanations behind these rules are in 08-Open-SQL. This section only states the rules.
7.1 Write strict ABAP SQL: a comma-separated field list, @ host variables, INTO after the query clauses
The strict syntax is checked more thoroughly and reads like the SQL it produces. Unescaped host variables are an obsolete form (3.16). See 08-Open-SQL.
INTO follows the WHERE, GROUP BY, HAVING and ORDER BY clauses. Only UP TO n ROWS, OFFSET and the other ABAP-specific additions, such as BYPASSING BUFFER, come after it. The ABAP Keyword Documentation requires these additions after INTO whenever INTO is the last clause, and its newer strict modes enforce that position for INTO.
" ✅
SELECT order_id, status
FROM zsm_t_order
WHERE customer_id = @customer_id
ORDER BY order_id
INTO TABLE @DATA(orders)
UP TO 100 ROWS.
" ❌
SELECT order_id status FROM zsm_t_order INTO TABLE orders
WHERE customer_id = customer_id.A released CDS view is a stable interface, while a table layout can change. Apply 1.2 when targeting ABAP for Cloud Development and 1.3 in Standard ABAP. Rule 1.2 shows an example.
Each statement is a round trip to the database, so the cost grows with the number of lines instead of staying constant. Read the data in one statement before the loop: a join, a subquery, FOR ALL ENTRIES (7.4) or an internal table as the data source.
" ✅
SELECT item~order_id, item~item_no, item~quantity
FROM zsm_t_order_item AS item
INNER JOIN zsm_t_order AS header ON header~order_id = item~order_id
WHERE header~customer_id = @customer_id
INTO TABLE @DATA(items).
" ❌
LOOP AT orders INTO DATA(order).
SELECT item_no, quantity
FROM zsm_t_order_item
WHERE order_id = @order-order_id
APPENDING TABLE @items.
ENDLOOP.An empty driver table removes the whole WHERE condition, and duplicate rows cause redundant work. Where the driver can be expressed in SQL, a join or subquery is better. Where it exists only in ABAP, you can also use the internal table as a data source. See 08-Open-SQL.
⚠️ VERSION-DEPENDENT: internal tables as data source (FROM @itab). Available in newer releases only, with restrictions. Check the ABAP Keyword Documentation.
" ✅ FOR ALL ENTRIES with a checked, de-duplicated driver
SORT order_ids BY order_id.
DELETE ADJACENT DUPLICATES FROM order_ids COMPARING order_id.
IF order_ids IS NOT INITIAL.
SELECT order_id, item_no, quantity
FROM zsm_t_order_item
FOR ALL ENTRIES IN @order_ids
WHERE order_id = @order_ids-order_id
INTO TABLE @DATA(items).
ENDIF.
" ✅ the internal table as data source, where the release supports it
SELECT item~order_id, item~item_no, item~quantity
FROM @order_ids AS wanted
INNER JOIN zsm_t_order_item AS item ON item~order_id = wanted~order_id
INTO TABLE @DATA(items).
" ❌ an empty order_ids reads every row of the table
SELECT order_id, item_no, quantity
FROM zsm_t_order_item
FOR ALL ENTRIES IN @order_ids
WHERE order_id = @order_ids-order_id
INTO TABLE @DATA(items).Without the full key, the row you get is any one of the rows that match. Which one may change between executions. When any row will do, say so with UP TO 1 ROWS and an ORDER BY. Existence checks are the exception (7.6), because there any row answers the question.
" ✅
SELECT SINGLE status
FROM zsm_t_order
WHERE order_id = @order_id
INTO @DATA(status).
" ❌ several orders per customer: which one is returned is undefined
SELECT SINGLE status
FROM zsm_t_order
WHERE customer_id = @customer_id
INTO @DATA(status).The database only has to find one row, and no columns are transferred. COUNT( * ) counts every match even when you only need to know whether there is one.
" ✅
SELECT SINGLE @abap_true
FROM zsm_t_order
WHERE customer_id = @customer_id
INTO @DATA(has_orders).
" ❌
SELECT COUNT( * )
FROM zsm_t_order
WHERE customer_id = @customer_id
INTO @DATA(order_count).
IF order_count > 0.
...
ENDIF.A field list transfers less data, documents what the code uses, and still works when someone appends columns to the table.
" ✅
SELECT order_id, status FROM zsm_t_order
WHERE customer_id = @customer_id
INTO TABLE @DATA(orders).
" ❌
SELECT * FROM zsm_t_order
WHERE customer_id = @customer_id
INTO TABLE @DATA(orders).ABAP SQL restricts reads and writes to the logon client automatically. Naming the client column, or switching the handling off, opens access to other clients' data.
Use USING CLIENT or USING [ALL] CLIENTS [IN] only for an explicit cross-client requirement, and state that requirement in a comment.
CLIENT SPECIFIED is obsolete. It is forbidden in strict mode, and USING replaces it. ABAP for Cloud Development enforces client isolation.
" ✅
SELECT order_id FROM zsm_t_order
WHERE customer_id = @customer_id
INTO TABLE @DATA(orders).
" ❌
SELECT order_id FROM zsm_t_order CLIENT SPECIFIED
WHERE mandt = @sy-mandt AND customer_id = @customer_id
INTO TABLE @DATA(orders).Standard tables are kept consistent by application logic, locks, change documents and follow-up processing. A direct UPDATE bypasses all of these. See 15-BAPIs.
" ✅
UPDATE zsm_t_order SET status = @new_status WHERE order_id = @order_id.
" ✅ standard data: through the BAPI wrapper (5.1)
sales_order_api->change_delivery_date( order_id = order_id
date = new_date ).
" ❌ direct DML on a standard table
UPDATE vbak SET vdatu = @new_date WHERE vbeln = @order_id.COMMIT WORK closes the whole SAP LUW, including work done by callers and other components. So only the code that owns the business transaction may decide when it ends. See 08-Open-SQL.
" ✅ the report owns the transaction
TRY.
order_service->release( order_id ).
COMMIT WORK.
CATCH zcx_zsm_static_error INTO DATA(error).
ROLLBACK WORK.
MESSAGE error TYPE 'E'.
ENDTRY.
" ❌ inside a reusable method
METHOD zif_zsm_order_service~release.
repository->set_status( order_id = order_id
status = released ).
COMMIT WORK.
ENDMETHOD.BAPIs follow their own commit protocol, and the caller ends it with the matching BAPI. That call also belongs to the transaction owner, not to the wrapper. See 15-BAPIs.
" ✅ at the transaction owner, after the wrapper reported success
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
EXPORTING
wait = abap_true.
" ❌ in the wrapper, or a plain COMMIT WORK after a BAPI
COMMIT WORK.Database locks end with each database LUW, while a business transaction can span several of them. The SAP enqueue mechanism covers that gap.
Follow this pattern:
- Lock before reading data that you intend to change.
- Release the lock after the commit or the rollback.
- Report a foreign lock as a business message.
See 08-Open-SQL.
📝 Contextual snippet — assumes a lock object
EZSM_ORDERonzsm_t_order, named as in 2.6. The generated function module has one parameter per key field of the lock object, plusmode_<table>for the lock mode and_scopefor the lock duration. A foreign lock raises the exceptionforeign_lock.
" ✅
CALL FUNCTION 'ENQUEUE_EZSM_ORDER'
EXPORTING
order_id = order_id
EXCEPTIONS
foreign_lock = 1
system_failure = 2
OTHERS = 3.
IF sy-subrc <> 0.
" USING MESSAGE passes the lock message from the sy-msg* fields to the exception
RAISE EXCEPTION TYPE zcx_zsm_order_locked USING MESSAGE.
ENDIF.For each row of the left side, a LEFT OUTER JOIN returns at least one result row. Where no row of the right side matches, the columns of the right side contain null values.
According to the ABAP Keyword Documentation, every relational expression except IS [NOT] NULL has an unknown result when an operand is the null value. A comparison with a right-side column in the WHERE condition therefore removes exactly those rows the outer join added, and the result is the same as an inner join.
- Put restrictions on the optional side into the
ONcondition. - In
WHERE, test the optional side only withIS NULLorIS NOT NULL, and only when that is the intent, for example to find orders without items.
This is a team rule.
⚠️ VERSION-DEPENDENT: comparisons in ON conditions. Which operands anONcondition may contain, for example host variables or columns of the left side only, depends on the release and the strict mode of the syntax check. Check the ABAP Keyword Documentation.
" ✅ every order of the customer, with its open items where there are any
SELECT header~order_id, item~item_no
FROM zsm_t_order AS header
LEFT OUTER JOIN zsm_t_order_item AS item
ON item~order_id = header~order_id
AND item~status = @status_open
WHERE header~customer_id = @customer_id
INTO TABLE @DATA(order_items).
" ❌ orders without an open item disappear: the result is that of an inner join
SELECT header~order_id, item~item_no
FROM zsm_t_order AS header
LEFT OUTER JOIN zsm_t_order_item AS item
ON item~order_id = header~order_id
WHERE header~customer_id = @customer_id
AND item~status = @status_open
INTO TABLE @DATA(order_items).The ABAP Keyword Documentation describes the difference:
- Without
AND WAIT, the update is asynchronous. The program continues immediately afterCOMMIT WORK, andsy-subrcis always 0. - With
AND WAIT, the program continues only after the update work process has executed the high-priority update function modules.sy-subrcis 0 if the update succeeded and 4 if it failed.
Waiting costs the user time, so it needs a reason:
- the next statement reads the data the update writes;
- the program must react when the update fails.
In both cases, evaluate sy-subrc. Otherwise, commit without waiting. The decision belongs to the transaction owner (7.10).
The same applies to the WAIT parameter of BAPI_TRANSACTION_COMMIT (7.11). With WAIT set, the function module executes COMMIT WORK AND WAIT and reports a failed commit as a type E message in its RETURN parameter; without it, it executes COMMIT WORK.
This is a team rule.
" ✅ the next step reads the order the update writes
COMMIT WORK AND WAIT.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_zsm_order_not_saved
MESSAGE e012(zsm_msg) WITH order_id.
ENDIF.
DATA(saved_order) = order_repository->read( order_id ).
" ❌ nothing after the commit depends on the update; the user waits for nothing
COMMIT WORK AND WAIT.
MESSAGE s013(zsm_msg) WITH order_id.According to the ABAP Keyword Documentation, UPDATE … SET without a WHERE condition changes all rows of the target, in a client-dependent table all rows of the current client. DELETE FROM without a condition deletes all rows. Both statements set sy-dbcnt to the number of rows changed or deleted.
- Every mass
UPDATEandDELETEcarries aWHEREcondition that restricts it to the intended rows. - Compare
sy-dbcntwith the expected number where it is known, and treat a mismatch as an error before the transaction owner commits (7.10). - Mass changes apply only to your own tables (7.9).
This is a team rule.
" ✅
UPDATE zsm_t_order SET status = @status_cancelled
WHERE customer_id = @customer_id
AND status = @status_open.
IF sy-dbcnt <> lines( open_orders ).
RAISE EXCEPTION TYPE zcx_zsm_order_not_saved
MESSAGE e014(zsm_msg) WITH customer_id.
ENDIF.
" ❌ no WHERE: every order of the client is cancelled
UPDATE zsm_t_order SET status = @status_cancelled.Lifecycle:
CURRENT / RECOMMENDED. These rules apply in every language version. ABAP SQL on database tables performs no implicit authorization check, so most of this section is about checks that you have to write yourself.
AUTHORITY-CHECK only sets sy-subrc. If the code does not evaluate it, the check has no effect.
The check belongs before the read or the change, in the layer that owns the data access, so that every caller passes through it.
" ✅ generic table access: check the standard table authorization
AUTHORITY-CHECK OBJECT 'S_TABU_NAM'
ID 'ACTVT' FIELD activity_display
ID 'TABLE' FIELD table_name.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_zsm_not_authorized
MESSAGE e010(zsm_msg) WITH table_name.
ENDIF.
" ❌ the result is never evaluated
AUTHORITY-CHECK OBJECT 'S_TABU_NAM'
ID 'ACTVT' FIELD activity_display
ID 'TABLE' FIELD table_name.
SELECT * FROM (table_name) INTO TABLE @<rows>.Authorization objects are specific to each application and each project. An invented object looks authoritative and gets copied.
Use a standard object only when the example is about exactly that object. Otherwise, put a marker comment in the place where the check belongs. CDSGuide uses the same marker. This is a team rule.
" ✅
" >>> Authorization check for the sales organization (order-sales_org) belongs here.
SELECT order_id, status FROM zsm_t_order
WHERE sales_org = @sales_org
INTO TABLE @DATA(orders).
" ❌ an object that does not exist, presented as if it did
AUTHORITY-CHECK OBJECT 'Z_ORDER_AUTH' ID 'ACTVT' FIELD '03'.The addition switches off CDS access control for the data source it is written on, so the read returns rows the user would otherwise not see.
The justification belongs in a comment directly at the statement. It names why the restriction does not apply here, and which check replaces it. This is a team rule. See CDSGuide, Bypassing Access Control.
⚠️ VERSION-DEPENDENT:WITH PRIVILEGED ACCESS. Availability depends on the release. Check the ABAP Keyword Documentation.
" ✅
" Privileged: the totals job aggregates across all sales organizations and
" outputs no single document; access is limited by the job's own role.
SELECT FROM zsm_i_order WITH PRIVILEGED ACCESS
FIELDS salesorg, SUM( netamount ) AS total
GROUP BY salesorg
INTO TABLE @DATA(totals).
" ❌ no reason given
SELECT FROM zsm_i_order WITH PRIVILEGED ACCESS
FIELDS *
INTO TABLE @DATA(orders).A dynamic token is code. A token taken from input lets that input change the statement, which is how injection happens.
Prefer static SQL with host variables. Inside a dynamic token, too, naming an ABAP data object (= @value) is safer than building its value in as a literal.
Where names come from input, check them with cl_abap_dyn_prg:
check_whitelist_tabagainst an allow list;check_column_namefor a column;check_table_name_strfor a table.
Any literal that you do build into a token must be escaped with cl_abap_dyn_prg=>quote.
" ✅ the column comes from a fixed list; the value stays a host variable
DATA(column) = cl_abap_dyn_prg=>check_whitelist_tab( val = to_upper( requested_column )
whitelist = allowed_columns ).
DATA(condition) = |{ column } = @filter_value|.
SELECT order_id FROM zsm_t_order
WHERE (condition)
INTO TABLE @DATA(orders).
" ❌ input becomes part of the statement
DATA(condition) = |{ requested_column } = '{ user_input }'|.📝 Contextual snippet —
allowed_columnsis of typestring_hashed_table. A name that is not on the list raisescx_abap_not_in_whitelist.
Values written into code end up in transports and in version history. They are also identical in every system, which is wrong for a target that differs between development and production.
Connection details belong in an RFC destination (Standard ABAP) or a communication arrangement (ABAP Cloud). The name of the destination comes from configuration.
" ✅
DATA(destination) = configuration->order_system_destination( ).
CALL FUNCTION 'ZSM_FM_READ_ORDERS' DESTINATION destination
EXPORTING
customer_id = customer_id
IMPORTING
orders = orders
EXCEPTIONS
system_failure = 1
communication_failure = 2
OTHERS = 3.
" ❌
cl_http_client=>create_by_url( EXPORTING url = 'https://<host>/orders'
IMPORTING client = DATA(client) ).
client->authenticate( username = '<user>'
password = '<password>' ).According to the ABAP Keyword Documentation, cl_abap_random and its typed variants call the Mersenne Twister, a pseudo-random number generator. Their output follows from the seed and can be predicted, so it must not protect anything.
Use a cryptographically secure generator. In Standard ABAP, the function module GENERATE_SEC_RANDOM creates random bytes through the kernel's security service. LENGTH sets the number of bytes (default 16), RANDOM returns them as an xstring, and the classic exceptions are INVALID_LENGTH, NO_MEMORY and INTERNAL_ERROR. [verify: the secure random API for ABAP for Cloud Development]
" ✅ Standard ABAP
DATA random_bytes TYPE xstring.
CALL FUNCTION 'GENERATE_SEC_RANDOM'
EXPORTING
length = 32
IMPORTING
random = random_bytes
EXCEPTIONS
invalid_length = 1
no_memory = 2
internal_error = 3
OTHERS = 4.
IF sy-subrc <> 0.
RAISE EXCEPTION NEW zcx_zsm_random_failed( ).
ENDIF.
" ❌ predictable
DATA(one_time_code) = cl_abap_random_int=>create( seed = seed
min = 100000
max = 999999 )->get_next( ).A path taken from input can point anywhere the application server can reach (directory traversal). A logical file name restricts it to a maintained directory. Logical file names are maintained in transactions FILE and SF01.
The ABAP Keyword Documentation names two function modules:
FILE_VALIDATE_NAMEchecks a physical file name against a logical file name or path.FILE_GET_NAMEbuilds physical names from logical ones. A program that only uses it usually needs no further validation.
Check file-access authorization as well. FILE_VALIDATE_NAME raises the classic exceptions LOGICAL_FILENAME_NOT_FOUND and VALIDATION_FAILED.
" ✅
CALL FUNCTION 'FILE_VALIDATE_NAME'
EXPORTING
logical_filename = export_file_name
CHANGING
physical_filename = file_path
EXCEPTIONS
OTHERS = 1.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_zsm_invalid_path
MESSAGE e011(zsm_msg) WITH file_path.
ENDIF.
" ❌ the path is used as entered
OPEN DATASET file_path FOR OUTPUT IN TEXT MODE ENCODING UTF-8.Logs are read by more people than the data they describe, and they are kept longer. Log the identifiers and the message that support needs, not the payload.
" ✅
log->add_text( |Payment for order { order_id } rejected| ).
" ❌
log->add_text( |Payment rejected: IBAN { payment-iban }, token { api_token }| ).The ABAP Keyword Documentation describes the additions WITH AUTHORITY-CHECK and WITHOUT AUTHORITY-CHECK of CALL TRANSACTION:
WITH AUTHORITY-CHECKchecks the current user's authorization before the call. The check uses the authorization objectS_TCODEand any authorization object entered in the definition of the transaction code (transactionSE93). Missing authorization raises the catchable exceptionCX_SY_AUTHORIZATION_ERROR. The documentation calls this the recommended way to check authorization. It replaces earlier checks withAUTHORITY-CHECK, the function moduleAUTHORITY_CHECK_TCODE, or the tableTCDCOUPLES.WITHOUT AUTHORITY-CHECKstates that no check is necessary, and suppresses the corresponding message of the extended program check.- No addition: the documentation classifies
CALL TRANSACTIONwithout one of the two additions as obsolete (3.16).
Write WITH AUTHORITY-CHECK by default. Use WITHOUT AUTHORITY-CHECK only with a comment at the statement that explains why the check is not needed. This is a team rule.
⚠️ VERSION-DEPENDENT:WITH|WITHOUT AUTHORITY-CHECK. Availability of the additions depends on the release. Check the ABAP Keyword Documentation.
" ✅
TRY.
CALL TRANSACTION 'ZSM_ORDER' WITH AUTHORITY-CHECK.
CATCH cx_sy_authorization_error INTO DATA(authorization_error).
MESSAGE authorization_error TYPE 'E'.
ENDTRY.
" ❌ obsolete form; whether the user's authorization matters is not stated
CALL TRANSACTION 'ZSM_ORDER'.A remote-enabled function module can be called from another system or program. A check in the caller's UI is not part of that path.
According to the ABAP Keyword Documentation, an automatic authorization check runs for remote calls only if the profile parameter auth/rfc_authority_check is set to 1. The documentation of that profile parameter names the authorization object of the check: S_RFC. That check decides whether the function module may be called at all. It says nothing about the business data the function module reads or changes.
- The RFC function module, or the class it delegates to (5.1), checks the business authorizations itself, before it reads or changes data (8.1).
- A failed check is reported through the function module's classic exceptions or its return table. RFC supports only classic exceptions (6.1).
This is a team rule.
" ✅ the class behind the RFC function module checks before it reads
METHOD read_for_customer.
" >>> Authorization check for the customer's sales organization belongs here (8.2).
SELECT order_id, status FROM zsm_t_order
WHERE customer_id = @customer_id
INTO TABLE @result.
ENDMETHOD.
" ❌ the RFC function module relies on a check in the calling UI
FUNCTION zsm_fm_read_orders.
SELECT order_id, status FROM zsm_t_order
WHERE customer_id = @customer_id
INTO TABLE @orders.
ENDFUNCTION.Lifecycle:
CURRENT / RECOMMENDED. For tools and techniques, see 19-Performance. Database-side optimisation is the topic of HANAGuide (planned).
Intuition about where the time goes is usually wrong, and an optimisation without a measurement cannot show that it helped. Use runtime analysis (SAT, or the ABAP profiler in ADT) and the SQL trace (ST05) on realistic data volumes. This is a team rule.
Filtering, aggregating and joining where the data already is avoids moving rows that are thrown away later.
Use the WHERE, GROUP BY, aggregate functions and joins of ABAP SQL. Deeper pushdown (CDS, AMDP) is outside ABAPGuide and belongs to HANAGuide (planned).
" ✅
SELECT customer_id, SUM( net_amount ) AS total
FROM zsm_t_order
WHERE status = @status_completed
GROUP BY customer_id
INTO TABLE @DATA(totals).
" ❌ every row travels to ABAP only to be summed
SELECT customer_id, net_amount, status FROM zsm_t_order
INTO TABLE @DATA(orders).
LOOP AT orders INTO DATA(order) WHERE status = status_completed.
...
ENDLOOP.Access through a sorted or hashed key avoids a full scan, and the table kind documents how the table is used. Clean ABAP recommends choosing the right table type and avoiding DEFAULT KEY.
A secondary key costs memory and maintenance. Add one only for reads that actually happen often. See 07-Internal-Tables.
" ✅
TYPES orders_by_id TYPE SORTED TABLE OF zsm_s_order
WITH UNIQUE KEY order_id
WITH NON-UNIQUE SORTED KEY by_customer COMPONENTS customer_id.
" ❌ the default key is all character-like fields, rarely what you mean
TYPES orders_by_id TYPE STANDARD TABLE OF zsm_s_order WITH DEFAULT KEY.An inner loop over a whole table multiplies the run time by the table's size.
- Use keyed access for the inner part: a table expression, or
LOOP … WHEREon a sorted or secondary key. - Use
LOOP … GROUP BYwhen you process lines in groups.
" ✅ the inner loop uses the sorted key on order_id
LOOP AT orders INTO DATA(order).
LOOP AT items INTO DATA(item) USING KEY by_order WHERE order_id = order-order_id.
...
ENDLOOP.
ENDLOOP.
" ❌ the inner loop scans all items for every order
LOOP AT orders INTO DATA(order).
LOOP AT items INTO DATA(item).
IF item-order_id = order-order_id.
...
ENDIF.
ENDLOOP.
ENDLOOP.INTO copies every row, while a field symbol or reference works on the row in place. Clean ABAP describes when to use each target. It recommends field symbols for reading and changing rows, and references when the reference must outlive the loop.
" ✅
LOOP AT orders ASSIGNING FIELD-SYMBOL(<order>).
<order>-status = released.
ENDLOOP.
" ❌
LOOP AT orders INTO DATA(order).
order-status = released.
MODIFY orders FROM order.
ENDLOOP.See 3.14. Repeated table expressions on the same line are a common hidden cost inside loops.
Each pass of the loop fetches from the database cursor, and the loop body runs while the cursor is open. Read into a table with INTO TABLE.
For volumes too large to hold in memory, PACKAGE SIZE with ENDSELECT processes the data in blocks, and that use is accepted. See 19-Performance.
Lifecycle:
LEGACY / HISTORICAL REFERENCE. Row-by-rowSELECT … ENDSELECTis listed in 21-Classic-vs-Modern-ABAP withSELECT … INTO TABLEas the replacement.
" ✅
SELECT order_id, status FROM zsm_t_order
WHERE customer_id = @customer_id
INTO TABLE @DATA(orders).
" ❌
SELECT order_id, status FROM zsm_t_order
WHERE customer_id = @customer_id
INTO @DATA(order).
APPEND order TO orders.
ENDSELECT.COLLECT relies on unique entries with respect to the primary table key and on a stable key administration. Hashed tables and sorted tables have such an administration of their own, and their key decides which line the values are added to.
A standard table only gets a temporary hash administration for COLLECT, which other changes to the table invalidate. Every following COLLECT then searches linearly, and the primary key of a standard table is never unique. A sorted table with a non-unique key works correctly only as long as nothing but COLLECT fills it. The ABAP Programming Guidelines therefore say: only use COLLECT for hashed tables or sorted tables with a unique key, and not for standard tables any more.
All components outside the primary key must be numeric; their values are added up. See 06-Loops.
" ✅
TYPES quantities_by_material TYPE HASHED TABLE OF zsm_s_material_quantity
WITH UNIQUE KEY matnr.
DATA quantities TYPE quantities_by_material.
COLLECT VALUE zsm_s_material_quantity( matnr = item-matnr quantity = item-quantity ) INTO quantities.
" ❌ standard table: temporary hash administration, linear search after other changes
DATA quantities TYPE STANDARD TABLE OF zsm_s_material_quantity WITH DEFAULT KEY.Lifecycle:
CURRENT / RECOMMENDED. ABAP Unit is the test framework for ABAP code. These rules summarise; 22-ABAP-Unit teaches them step by step, with a complete example.
A test fixes the intended behaviour, so the next change can be made without fear of silently breaking it.
Classes built along section 5 (interfaces, constructor injection, small methods) are testable by design. Clean ABAP recommends writing testable code, and warns against obsessing about coverage numbers.
Requiring tests for every new class is a team rule.
Local test classes live in the test include of the class under test. They are found and run together with that class.
HARMLESS and SHORT describe a proper unit test: it changes no persistent data and finishes quickly. Clean ABAP recommends local test classes for unit tests and names them by purpose with ltc_ (its own convention for test classes).
The default risk level and duration are a team rule. A higher risk level or longer duration needs a comment that explains it.
" ✅
CLASS ltc_release DEFINITION FINAL FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT.
PRIVATE SECTION.
DATA cut TYPE REF TO zif_zsm_order_service.
DATA repository TYPE REF TO zif_zsm_order_repository.
METHODS setup.
METHODS releases_open_order FOR TESTING RAISING cx_static_check.
METHODS rejects_completed_order FOR TESTING RAISING cx_static_check.
ENDCLASS.
" ❌ no reason given for the risk level
CLASS ltc_test DEFINITION FOR TESTING
RISK LEVEL DANGEROUS
DURATION LONG.A failing test should point to one broken behaviour. A method that checks several behaviours stops at the first failure and hides the rest.
Clean ABAP recommends that the "when" part be exactly one call, and that assertions be few and focused.
" ✅
METHODS releases_open_order FOR TESTING RAISING cx_static_check.
METHODS rejects_completed_order FOR TESTING RAISING cx_static_check.
" ❌
METHODS test_release FOR TESTING RAISING cx_static_check. " releases, rejects, notifiesThe test list then reads as a specification, and a failure message says what broke. Clean ABAP recommends names that reflect the given and the expected outcome.
Within the 30-character limit for method names, an ABAP Doc comment can add what the name cannot hold.
" ✅
METHODS rejects_completed_order FOR TESTING RAISING cx_static_check.
" ❌
METHODS test_release_2 FOR TESTING RAISING cx_static_check.The reader can see the setup, the single action and the expected outcome as three separate parts. Clean ABAP recommends the given-when-then structure, and extracting helper methods when a part grows long.
" ✅
METHOD rejects_completed_order.
" given
cl_abap_testdouble=>configure_call( repository )->returning( completed_order ).
repository->read( completed_order-order_id ).
" when
TRY.
cut->release( completed_order-order_id ).
cl_abap_unit_assert=>fail( msg = 'Release of a completed order must fail' ).
CATCH zcx_zsm_order_not_releasable INTO DATA(error).
" then
cl_abap_unit_assert=>assert_equals( act = error->order_id
exp = completed_order-order_id ).
ENDTRY.
ENDMETHOD.📝 Contextual snippet — assumes the test class from 10.2, a test double from 10.6, a structure constant
completed_order, and anorder_idattribute on the exception.
A double that is passed through the constructor (5.4) isolates the class under test without changing its code.
cl_abap_testdouble creates and configures such doubles without hand-written classes. Clean ABAP recommends dependency inversion for injecting test doubles, and suggests considering the ABAP test double framework.
" ✅
METHOD setup.
repository = CAST zif_zsm_order_repository(
cl_abap_testdouble=>create( 'ZIF_ZSM_ORDER_REPOSITORY' ) ).
cut = NEW zcl_zsm_order_service( repository ).
ENDMETHOD.
" ❌ the real repository reads the database
METHOD setup.
cut = NEW zcl_zsm_order_service( NEW zcl_zsm_order_db_repository( ) ).
ENDMETHOD.A test seam embeds test logic in production code and ties the test to private details. It is a bridge towards a refactoring, not a design. Clean ABAP recommends test seams only as a temporary workaround.
" ✅ in a legacy method that cannot be given a dependency yet
TEST-SEAM read_orders.
SELECT order_id, status FROM zsm_t_order
WHERE customer_id = @customer_id
INTO TABLE @orders.
END-TEST-SEAM.
" ❌ a new class that uses a test seam instead of an injected repository (5.4)Tests against real data break when that data changes, and they can change it themselves.
The ABAP SQL test environment and the CDS test environment redirect reads to test doubles that the test fills itself. Clean ABAP points to the available test isolation tools.
⚠️ VERSION-DEPENDENT: ABAP SQL and CDS test environments. Availability and API details depend on the release. Check the ABAP Keyword Documentation. Bothcl_osql_test_environmentandcl_cds_test_environmentare released for ABAP for Cloud Development. The instances they create implementif_osql_test_environmentandif_cds_test_environment, which provideinsert_test_data(parameteri_data),clear_doublesanddestroy. The factory methodcl_osql_test_environment=>createtakes the tables to replace ini_dependency_listand returns the environment inr_result. The class isCREATE PRIVATE FOR TESTING, so only test code can use it.
" ✅
CLASS-METHODS class_setup.
METHOD class_setup.
sql_environment = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( 'ZSM_T_ORDER' ) ) ).
ENDMETHOD.
METHOD setup.
sql_environment->clear_doubles( ).
sql_environment->insert_test_data( test_orders ).
ENDMETHOD.
" ❌ depends on whatever the development system contains
METHOD finds_open_orders.
DATA(orders) = cut->read_open( ).
cl_abap_unit_assert=>assert_not_initial( orders ).
ENDMETHOD.A specific assertion such as assert_equals reports what it expected and what it got. A generic one only reports that something failed.
Clean ABAP recommends:
- the right assertion type for the check;
- asserting content, not quantity;
failfor expected exceptions.
" ✅
cl_abap_unit_assert=>assert_equals( act = released_order-status
exp = released ).
" ❌
cl_abap_unit_assert=>assert_true( xsdbool( released_order-status = released ) ).Tests that only run when someone remembers to start them stop protecting anything. ABAP Unit runs as part of the quality gates in 13.1. Where a CI pipeline exists, run them there as well. This is a team rule.
A good name or an extracted method stays correct as the code changes. A comment does not.
Clean ABAP recommends:
- expressing yourself in code rather than in comments;
- not using comments as an excuse for bad names;
- using methods instead of comments to segment code.
" ✅
IF is_releasable( order ).
" ❌
" status must be open and no credit block
IF order-status = created AND order-credit_block = abap_false.The code already says what happens. Only a comment can say why a non-obvious decision was taken.
Clean ABAP recommends:
- comments that explain the why;
"rather than*;- placing the comment before the statement it refers to.
" ✅
" the partner system rejects lines without a delivery date, so drop them early
DELETE items WHERE delivery_date IS INITIAL.
" ❌
* delete items without delivery date
DELETE items WHERE delivery_date IS INITIAL.Version control keeps the old code. Commented-out code only adds noise and goes stale. Clean ABAP recommends deleting code instead of commenting it out, and leaving versioning to the tools.
The guides show rejected alternatives as ❌ examples, labelled as such. That is documentation, not commented-out production code, and it follows each repository's CLAUDE.md.
" ✅
DATA(fee) = fee_calculator->calculate( order ).
" ❌
DATA(fee) = fee_calculator->calculate( order ).
* DATA(fee) = order-amount * fee_rate.
* fee = fee + surcharge.11.4 Document public classes, interfaces and methods with ABAP Doc, including @parameter and @raising
ABAP Doc appears in the ADT element information wherever the method is called, so the caller learns the contract without opening the implementation.
The rule covers global interfaces and the public section of global classes. Section 14 records how this differs from Clean ABAP.
" ✅
"! Releases an open order for delivery.
"! @parameter order_id | Order to release
"! @raising zcx_zsm_order_not_found | No order with this ID exists
"! @raising zcx_zsm_order_not_releasable | The order is not in status created
METHODS release
IMPORTING order_id TYPE zsm_e_order_id
RAISING zcx_zsm_order_not_found
zcx_zsm_order_not_releasable.
" ❌ a plain comment instead of ABAP Doc; the exceptions are not described
" release the order
METHODS release
IMPORTING order_id TYPE zsm_e_order_id
RAISING zcx_zsm_order_not_found
zcx_zsm_order_not_releasable.Pragmas are checked by the compiler and are tied to a specific check. Clean ABAP recommends pragmas over pseudo comments.
A suppressed finding without a reason cannot be judged in review, so a comment explains why the finding does not apply. The reason requirement is a team rule.
" ✅
" the variable is unused on purpose: the INTO form keeps the message findable (6.11)
MESSAGE e004(zsm_msg) WITH order_id INTO DATA(message_text) ##NEEDED.
" ❌
MESSAGE e004(zsm_msg) WITH order_id INTO DATA(message_text). "#EC NEEDEDA ticket survives team changes and carries the discussion. A personal ID points to someone who may have left, and it puts personal data into the code. Section 14 records how this differs from Clean ABAP. This is a team rule.
" ✅
" TODO ticket 4711: replace with the released API once it is available
" ❌
" TODO JD: replace this💡 Most of this section is enforced by the ABAP Formatter (12.3). A team that shares formatter settings rarely discusses formatting in review.
Upper-case keywords separate the language from the names at a glance. This is the convention in all the guides.
Clean ABAP deliberately leaves keyword case to the team. Choosing upper case is a team rule.
" ✅
LOOP AT orders ASSIGNING FIELD-SYMBOL(<order>).
" ❌
loop at ORDERS assigning field-symbol(<ORDER>).One statement per line keeps diffs, breakpoints and the debugger's line display precise (23-Debugging-Troubleshooting). Clean ABAP recommends no more than one statement per line.
" ✅
CLEAR total.
CLEAR count.
" ❌
CLEAR total. CLEAR count.The formatter makes indentation mechanical and identical for everyone.
Clean ABAP recommends running the formatter (the pretty printer in SAP GUI) before activating, with the team's shared settings. In large unformatted legacy objects, it suggests formatting only the changed lines, or formatting the whole object in a separate transport.
A chain implies that the declared items belong together, and it makes every edit a matter of commas and colons. Clean ABAP recommends against chaining up-front declarations.
When a declaration is needed up front at all (3.1), write one statement per item.
" ✅
DATA total TYPE zsm_e_amount.
DATA count TYPE i.
" ❌
DATA: total TYPE zsm_e_amount,
count TYPE i.📝 Structured constants (4.3),
BEGIN OF ENUM(4.4) andTYPES … BEGIN OFstay in the colon form. There the chain holds the parts of one structure, not unrelated variables.
Long lines force horizontal scrolling and hide the end of a statement. Clean ABAP recommends a maximum line length of 120 characters, and mentions that ADT's print margin can be set to it.
Consistent line breaks show at a glance where one parameter ends and the next begins. Clean ABAP recommends:
- keeping a single-parameter call on one line;
- putting each of several parameters on its own line, behind the call;
- aligning the
=signs; - if the line becomes too long, breaking after the opening parenthesis and indenting the parameters by four spaces (keywords such as
EXPORTINGby two).
" ✅
DATA(order_id) = sales_order_api->create( order ).
repository->set_status( order_id = order_id
status = released ).
" ❌
repository->set_status( order_id = order_id status = released ).
repository->set_status(
order_id = order_id
status = released ).Release here means releasing the transport. These three checks catch most defects before they leave the development system.
A shared check variant makes the result the same for everyone. This is a team rule.
A finding that is merely ignored comes back in every run and teaches the team to overlook the list.
An exemption with a reason can be reviewed and later revisited. The same applies to pragmas (11.5). This is a team rule.
Code that targets ABAP for Cloud Development (1.2) or is meant to stay cloud-ready (1.3) needs the matching ATC check, and the check must run before anyone calls the code cloud-ready (1.5). This is a team rule.
abaplint checks ABAP outside a system, for example in a pull request. It needs complete objects as abapGit serializes them, because it resolves definitions across files.
Align its configuration (abaplint.json in the repository root) with these rules. Where a rule here has an abaplint counterpart, enable it. Where they disagree, these rules win and the configuration records the difference.
| Section | abaplint rules |
|---|---|
| 2 Naming | no_prefixes, object_naming |
| 3 Modern syntax | prefer_inline, use_new, prefer_string_template, use_line_exists, functional_writing, omit_receiving, obsolete_statement |
| 4 Booleans | prefer_abap_bool, prefer_abap_bool_values, prefer_xsdbool |
| 12 Formatting | keyword_case, max_one_statement, line_length, keep_single_parameter_on_one_line, line_break_multiple_parameters, align_parameters |
This is a team rule.
"ATC-checked" and "abaplint-checked" are factual claims about a specific run. Make them only after that run, and record them the way each repository records its review status.
Contextual snippets (0.5) are never claimed as checked: they are not complete objects, and no check can have run on them. This is a team rule.
An always-active breakpoint stops every dialog user who reaches it. According to the ABAP Keyword Documentation, always-active breakpoints are meant only for tests and are not allowed in production programs, and the extended program check reports BREAK-POINT without ID as an error. BREAK followed by a user name is a predefined macro, not a statement; it also puts a user name into the code.
BREAK-POINT ID and ASSERT ID, controlled by a checkpoint group (2.6), may stay in released code; the ABAP Keyword Documentation names these two as the test statements that production programs may contain. The rule also covers LOG-POINT, although its title names only breakpoints: LOG-POINT always requires ID, the documentation describes it as a tool for tests, and it is removed before release. For logging without a code change, use dynamic logpoints (transaction SDLP or ADT). BREAK-POINT and LOG-POINT are not allowed in ABAP for Cloud Development at all. Excluding the BREAK macro is a team rule.
" ✅ inactive until the checkpoint group activates it
BREAK-POINT ID zsm_cp_order.
" ❌ always active
BREAK-POINT.
" ❌ a test tool left in released code
LOG-POINT ID zsm_cp_order.Rules 0–13 were compared against the current Clean ABAP text. The table lists only real contradictions. Points where Clean ABAP leaves the choice to the team, such as keyword case, are team additions. They are indexed after the table, followed by the rules taken from the ABAP Programming Guidelines.
| Rule | Clean ABAP says | We do | Why |
|---|---|---|---|
| 2.6 | Avoid encodings, including prefixes such as cl_ and if_. Its sub-page on encodings accepts them for global Dictionary objects only as a compromise. |
Every development object carries a type infix after the namespace: zcl_zsm_, zsm_tt_, zsm_s_ and so on. |
Global objects share one Dictionary namespace. The infix shows the object type wherever only the name is visible: transport lists, where-used lists, SE11. One scheme across all guides. |
| 2.6 (local types) | Avoid encodings. It says nothing specific about local classes and interfaces, but names its own local test classes ltc_ and test helpers lth_. |
Local classes take lcl_, local interfaces lif_, local test classes ltc_ and local test helpers lth_. |
Consistent with Clean ABAP's own ltc_ / lth_ practice. A short local prefix makes local types recognisable inside a program. |
| 6.11 | For totally unrecoverable situations, dump. Where RAISE SHORTDUMP is not available, use a type X message. |
No MESSAGE statement below the UI layer, including type X. Unrecoverable situations raise a CX_NO_CHECK exception (6.3); violated internal assumptions use ASSERT, and a deliberate termination with a cause chain uses RAISE SHORTDUMP (6.12). |
An exception reaches the boundary handler (6.6), which logs it with its cause chain (6.8). A type X message ends the program before anything can be logged. How runtime errors and short dumps are read is shown in 23-Debugging-Troubleshooting. |
| 11.4 | Write ABAP Doc only for public APIs meant for other teams or applications, and do not enforce it everywhere. | ABAP Doc for every global interface and every public section of a global class. | The public section is kept minimal (5.6), so the cost stays small. In the guides, every public method is an API for readers who copy it. |
| 11.6 | Add your nickname, initials or user to TODO, FIXME and XXX comments. |
A ticket reference instead of a personal ID. | The repository rules forbid user names in content. A ticket outlives the people working on it. |
These rules are decisions of the team: they have no basis in Clean ABAP or the ABAP Programming Guidelines, go beyond them, or decide a point that Clean ABAP leaves open. Each is marked as a team rule where it is stated.
| Area | Rules |
|---|---|
| Language version | 1.3 |
| Modern syntax | 3.8, 3.9, 3.18, 3.19 (no new macros at all) |
| Classes and methods | 5.12 |
| Error handling | 6.2 (one abstract root per category), 6.6, 6.7, 6.11, 6.12 (the split between ASSERT, CX_NO_CHECK and RAISE SHORTDUMP) |
| Database access and SAP LUW | 7.13, 7.14, 7.15 |
| Security | 8.2, 8.3, 8.9, 8.10 |
| Performance | 9.1 |
| Testing | 10.1, 10.2 (default risk level and duration), 10.10 |
| Comments | 11.5 (reason for each pragma), 11.6 |
| Formatting | 12.1 |
| Quality gates | 13.1, 13.2, 13.3, 13.4, 13.5, 13.6 (excluding the BREAK macro) |
| AI-assisted development | 15.1–15.5 |
These rules have no Clean ABAP counterpart and are not team rules. They follow a recommendation of the ABAP Programming Guidelines.
| Rule | Guideline recommendation |
|---|---|
| 3.19 | Macros only in exceptional cases; methods or expressions instead; no new macros in type pools or TRMAC. The ban on all new macros is the team part. |
| 6.12 | Use assertions to check the consistency of a program's state. The split between ASSERT, CX_NO_CHECK and RAISE SHORTDUMP is the team part. |
| 9.8 | COLLECT only for hashed tables or sorted tables with a unique key. |
📝 Generated code is held to the same rules as hand-written code. This section adds only what is specific to working with an assistant. All rules in it are team rules.
A number points to one maintained text. A paraphrase in each repository drifts apart.
Write, for example, Follow docs/ABAP-Development-Rules.md; naming per Rule 2.6, transaction ownership per Rule 7.10. Use the citation form from 0.4.
An assistant writes plausible code quickly, including plausible names that do not exist. The checklist targets exactly those failures.
| Check | Rule |
|---|---|
| Names follow Clean ABAP naming and the object naming table | 2.1–2.6 |
No COMMIT WORK in reusable units |
7.10, 7.11 |
| Authorization check present, or the marker in guide examples | 8.1, 8.2 |
No invented objects, APIs, parameters or authorization objects: every name exists in the system or is a labelled zsm placeholder |
2.6, 8.2 |
| Only released APIs where the cloud language version is targeted | 1.2 |
No release or support-package claims without verification; release-dependent content labelled VERSION-DEPENDENT |
1.1, 0.1 |
| The available checks ran before anything is claimed as checked | 13.1, 13.5 |
Without it, the assistant has to guess between Standard ABAP and ABAP for Cloud Development, and the two accept different statements and APIs (1.1, 1.4).
The list shows where the review effort belongs. In the guides, each item becomes a marker in the text or a question to resolve before publishing.
A prompt leaves the system it came from. Use neutral placeholders instead of real data.
Customer data includes:
- customer and employer names;
- system IDs, hosts and IP addresses;
- user names;
- business documents and Customizing values.