Skip to content

ABAP Cloud and Clean Core: A Guide to Modification-Free S/4HANA Development

The three-tier extensibility model, the real restrictions of the ABAP Cloud language version, released APIs and release contracts, the Tier 2 wrapper pattern, and how to measure your existing custom code.

Mustafa Önder Mustafa Önder  ·  19 September 2026  ·  21 min read

Contents

  1. What Is Clean Core, and Why Now?
  2. The Three-Tier Extensibility Model
  3. The ABAP Cloud Language Version: What Does It Close Off?
  4. From Classic ABAP to ABAP Cloud: An Equivalents Table
  5. In Practice: Four Typical Conversions
  6. Released APIs and Release Contracts
  7. Finding the Right API
  8. Tier 2: Legitimate Access to Unreleased Objects
  9. Extension Points: BAdIs, CDS Extensions, RAP
  10. Measuring Existing Code: ATC and Custom Code Migration
  11. Five Common Misconceptions
  12. A Practical Roadmap
  13. Conclusion

"Clean core" has been SAP's most repeated phrase of recent years. It comes up so often in corporate presentations that for many ABAP developers it has turned into an empty marketing term. Yet there is something concrete and technical underneath: a definition of developing without breaking the core, enforced at the language level.

In this article I treat clean core not as a slogan but as an engineering problem: what exactly the ABAP Cloud language version closes off, what replaces each thing it closes, how to read release contracts, what the legitimate route is when you need an unreleased object, and how to measure how much of your existing custom code already fits the model.

A note on the code examples

The class and method signatures below reflect current S/4HANA releases. Because SAP extends these APIs from release to release, I recommend verifying the signature in ADT on your own system. The SAP object and field names in the extension examples are illustrative; the real names are in the documentation of the object you are extending.

What Is Clean Core, and Why Now?

In the classic ABAP world there were many ways to meet a requirement, and most of them touched SAP's standard objects directly: running a SELECT on a standard table, adding code to a standard include, modifying an SAP object, calling an unreleased function module. None of these was forbidden at the time, and all of them worked.

The bill arrived at upgrade time. When SAP changed a table's structure, updated a function module's behaviour or reorganised an include, your custom code that depended on it broke — quietly or loudly. SPAU/SPDD lists were the least predictable line item of any upgrade project, and that cost grew with how heavily the system had been customised.

Clean core names the problem and sets a principle: your custom code must not touch anything except the interfaces SAP has explicitly promised to keep stable. An object carrying that promise is a "released API"; the promise itself is a "release contract". As long as you stay within that boundary, your code keeps working no matter what SAP changes internally.

The practical reason it has become urgent is the cloud: S/4HANA Cloud Public Edition has no modifications and no access to classic ABAP. SAP learned the model in the cloud and carried the same model over to on-premise and Private Cloud. For a team planning an S/4HANA migration, the question is no longer "should we go clean core?" but "which code goes into which tier?".

The Three-Tier Extensibility Model

SAP's model is not an unworkable demand that "everything must be clean". It defines three tiers and states which code may live in which tier, along with that tier's cost:

Contents, upgrade stability and appropriate use of each tier in the three-tier extensibility model
TierWhat it containsUpgrade stabilityWhen it fits
Tier 1
Cloud development
Code written in the ABAP Cloud language version that uses only released APIs. Key User Extensibility (custom fields/logic) also belongs here.High — guaranteed by SAP's contract.The default. Anything new should be attempted here first.
Tier 2
Cloud API enablement
Thin wrappers written in classic ABAP. They isolate access to an unreleased object in one place and expose a clean interface to Tier 1.Medium — there is risk, but it is concentrated in one place and documented.When a need has no Tier 1 equivalent yet. Expected to be temporary.
Tier 3
Classic extensions
The existing classic ABAP code base: dynpro programs, classic reports, modifications, direct access to standard tables.Low — this is where upgrades break things.Code that already exists. Should not be the choice for new development.

The crux of the model is Tier 2. Many teams read clean core as all-or-nothing and abandon the whole approach at the first unreleased object they meet. Tier 2 exists precisely for that moment: you are not eliminating the unclean access, you are confining it to a single point. When SAP later releases an API for that function, the only thing you change is one class.

The ABAP Cloud Language Version: What Does It Close Off?

ABAP Cloud is not a separate language; it is a restricted language version of ABAP (officially ABAP for Cloud Development in ADT). It is not switched on by a pragma in the source: it is a property of the object, managed in practice at package level. When you create a package in ADT and choose ABAP for Cloud Development as its "ABAP Language Version", the objects created in it are compiled under those rules.

That means the discipline is enforced by the compiler, not by habit. Write something forbidden and you do not get a warning — your code does not compile. The full list evolves with each release, but these are the ones you will actually run into:

Closed: direct data access

You cannot SELECT from unreleased SAP DDIC tables. MARA, VBAK, BKPF and the like all fall under this. The replacement is the CDS views SAP has released (such as I_Product and I_SalesDocument).

Direct database access via native SQL (EXEC SQL) is closed as well. The same rule applies to types: you can use only released DDIC types and built-in ABAP types.

Closed: procedural and legacy constructs

FORM/PERFORM subroutines, SUBMIT, CALL TRANSACTION and batch input (BDC) are unavailable. Language elements declared obsolete are closed too. The natural unit of new code is the class.

TEST-SEAM / TEST-INJECTION are also excluded, so that shortcut for testing legacy code does not exist here. I explained in detail in the ABAP Unit article why that is actually a good thing: needing a seam is a sign the dependency was never separated.

Closed: everything tied to SAP GUI

Dynpro screens, classic reports with selection screens, WRITE list output, CL_GUI_ALV_GRID and other GUI controls, SAPscript and Smart Forms are all unavailable.

The UI replacement is a single route: Fiori via RAP and OData V4. For most teams this is the most expensive part of moving to ABAP Cloud — not a language restriction but a change of UI technology.

Changed: talking to remote systems

Remote calls are not closed off; they change form. Instead of a destination name hard-coded from SM59, you obtain the connection through released provider classes: cl_http_destination_provider for HTTP and cl_rfc_destination_provider for RFC. Addresses, authentication and environment differences move out of the code and into configuration.

Still open: the modern core of ABAP

AUTHORITY-CHECK keeps working, ABAP SQL (on released objects) is fully available, and all of CDS and RAP is open. You can use modern ABAP syntax in full. Because ABAP Cloud closes off obsolete elements, it naturally pushes you towards the modern style.

From Classic ABAP to ABAP Cloud: An Equivalents Table

What wastes the most time during the move is hunting down the replacement for each closed construct one by one. Here are the ones most often needed in real projects, in one place:

ABAP Cloud language version equivalents of classic ABAP constructs
NeedClassic ABAPABAP Cloud equivalent
Reading master/transaction dataSELECT ... FROM maraReleased CDS view (I_Product etc.)
LockingCALL FUNCTION 'ENQUEUE_EZ...'cl_abap_lock_object_factory
HTTP call to an external servicecl_http_client=>create_by_urlcl_http_destination_provider + cl_web_http_client_manager
RFC callCALL FUNCTION ... DESTINATION 'SM59_NAME'Obtaining the destination via cl_rfc_destination_provider
JSON conversion/ui2/cl_jsonxco_cp_json (XCO library)
Background jobJOB_OPEN / JOB_SUBMIT / JOB_CLOSEApplication Job + cl_apj_rt_api
User interfaceDynpro, ALV Grid, classic reportRAP + OData V4 + Fiori Elements
Output/formsSAPscript, Smart FormsAdobe Forms (form templates) and output management
Reporting errors to the userMESSAGE ... TYPE 'E' (to the screen)Exception classes; T100-based messages in RAP
Code injection for testsTEST-SEAMInterface + dependency injection

In Practice: Four Typical Conversions

The table gives direction, but the real work is in the code. Let's open up the four conversions you will meet most often.

1. From reading a table to a released CDS view

" Does NOT compile in ABAP Cloud — MARA is not a released object
SELECT SINGLE matnr, mtart, meins
  FROM mara
  WHERE matnr = @material_id
  INTO @DATA(material).

" The right way: the interface view SAP has released
SELECT SINGLE Product, ProductType, BaseUnit
  FROM I_Product
  WHERE Product = @material_id
  INTO @DATA(product).

It is misleading to see this as a simple rename. The released view names its fields by business meaning (BaseUnit, not meins), and more importantly SAP promises to keep the view's field list and semantics intact; even if the underlying table structure changes, the view stays stable for you. The naming difference in the example is no accident either: in S/4HANA's data model, the "material" concept is treated as a "product".

2. From an enqueue function module to the lock object API

" Classic
CALL FUNCTION 'ENQUEUE_EZ_SALES_ORDER'
  EXPORTING  salesorder     = order_id
  EXCEPTIONS foreign_lock   = 1
             system_failure = 2.

" ABAP Cloud
TRY.
    DATA(lock) = cl_abap_lock_object_factory=>get_instance(
                   iv_name = 'EZ_SALES_ORDER' ).

    lock->enqueue(
      it_parameter = VALUE #(
        ( name = 'SALESORDER' value = REF #( order_id ) ) ) ).

    " ... business logic ...

    lock->dequeue(
      it_parameter = VALUE #(
        ( name = 'SALESORDER' value = REF #( order_id ) ) ) ).

  CATCH cx_abap_lock_failure INTO DATA(lock_error).
    " Lock not acquired — arrives as an exception, not as sy-subrc
ENDTRY.

The real gain here is that error handling moves from checking sy-subrc to an exception class. The possibility of carrying on without noticing that the lock failed is removed at the language level.

3. From the classic HTTP client to a destination-based client

TRY.
    " The target is built from a URL or from a destination defined in the system
    DATA(destination) = cl_http_destination_provider=>create_by_url(
                          i_url = 'https://api.example.com/v1/orders' ).

    DATA(client) = cl_web_http_client_manager=>create_by_http_destination(
                     i_destination = destination ).

    DATA(request) = client->get_http_request( ).
    request->set_header_field( i_name  = 'Accept'
                               i_value = 'application/json' ).

    DATA(response) = client->execute( if_web_http_client=>get ).

    IF response->get_status( )-code <> 200.
      RAISE EXCEPTION NEW zcx_api_error( ).
    ENDIF.

    DATA(payload) = response->get_text( ).
    client->close( ).

  CATCH cx_http_dest_provider_error cx_web_http_client_error INTO DATA(http_error).
    " Connection or destination error
ENDTRY.

For brevity the example passes the URL directly. In production code it is better to work with a destination defined in the system; certificates, authentication and environment differences then leave the code entirely.

4. JSON conversion: the XCO library

/ui2/cl_json had become the de facto standard in many projects, but it is not released for ABAP Cloud. The replacement is the XCO library:

" From JSON text to an ABAP structure
DATA order TYPE zif_order_api=>ty_order.

xco_cp_json=>data->from_string( payload )->write_to( REF #( order ) ).

" From an ABAP structure to JSON text
DATA(json_text) = xco_cp_json=>data->from_abap( order )->to_string( ).

XCO is much more than JSON; it offers many released helpers such as UUID generation, string handling, date/time conversion and reading repository metadata. It is the library a team moving to ABAP Cloud should get to know early.

Released APIs and Release Contracts

"Released" is not a single thing. SAP assigns one or more release contracts to every released object, and the contract tells you for what purpose you may use it. Calling something "released" without reading its contract is misleading: an object may be released for remote calls and still be unusable from your ABAP Cloud code.

SAP release contracts, their names in ADT and the usage guarantee each one provides
ContractName in ADTWhat it promises
C0ExtendThat the object can be extended in a stable way. Adding fields to a structure or a CDS view is protected by this contract.
C1Use System-InternallyUse from within the same system. It carries two visibility flags: Use in Cloud Development (ABAP Cloud code) and Use in Key User Apps (key user extensions). The first is the one that matters for your ABAP Cloud code.
C2Use as Remote APICalls from outside the system: remote APIs such as OData and SOAP. It does not imply use from within the stack.
C3Manage Configuration ContentManaging configuration content. You will rarely meet it in day-to-day development.
C4Use in ABAP-Managed Database ProceduresUse from within AMDP. If you write database procedures, it determines which objects you may access.

The practical consequence: when looking for an API, the question is not "is it released?" but "is it released for the purpose I need?". If you are writing ABAP Cloud code, what you are looking for is contract C1 with the Use in Cloud Development flag. Many APIs you see on the SAP Business Accelerator Hub are for remote consumption (C2); try to call them from within the stack and the compiler will refuse.

Finding the Right API

This is where teams new to ABAP Cloud lose the most time. You need to combine three methods:

  • The Released Objects tree in ADT. In the Project Explorer, released objects are listed by type under your system project. Search by business object or field name to see which CDS views and classes are open to you. For day-to-day work this is the fastest route.
  • The object's API State. If you already have an object name, opening it in ADT and checking the API state in the Properties view tells you definitively which contract and which visibility it was released with. It is the only reliable answer to "may I use this object?".
  • SAP Business Accelerator Hub. The catalogue itself, especially for remote integration (C2) APIs. If you plan to use an API you found there from within ABAP Cloud, always verify its contract in ADT.

There is also a negative method, and it is surprisingly effective: start writing the code in an ABAP Cloud package and list every place the compiler objects to. The compiler tells you, object by object, what you cannot use. That list is also the inventory of your Tier 2 wrapper needs.

Tier 2: Legitimate Access to Unreleased Objects

Sooner or later you will face this: the function you need has no released equivalent. For example, an old module contains a pricing calculation that only a classic function module performs, and rewriting it is not reasonable.

There are two wrong reactions: (a) giving up on clean core and writing everything in a classic package, and (b) rewriting the function from scratch in ABAP Cloud and splitting the business logic across two places. The right answer is a Tier 2 wrapper.

The three parts of the pattern

1. Your own interface. In the ABAP Cloud package, write an interface that defines your need independently of SAP's structures. This interface is your contract, and it uses only released or built-in types.

2. An implementation in a classic package. In a separate package whose language version is Standard ABAP, write the class that implements that interface and touches the unreleased object. The unclean access lives only here.

3. Releasing your own object. In ADT you can assign an API state to your own objects too. Give the wrapper class contract C1 with Use in Cloud Development visibility and it becomes callable from your ABAP Cloud packages.

" --- Tier 1 package (ABAP Cloud): our own contract ---
" Built-in types only — no dependency on unreleased DDIC types
INTERFACE zif_legacy_pricing PUBLIC.

  TYPES: BEGIN OF ty_condition,
           condition_type TYPE c LENGTH 4,
           amount         TYPE p LENGTH 13 DECIMALS 2,
           currency       TYPE c LENGTH 5,
         END OF ty_condition.

  METHODS read_condition
    IMPORTING order_id         TYPE c
    RETURNING VALUE(condition) TYPE ty_condition
    RAISING   zcx_pricing_error.

ENDINTERFACE.
" --- Tier 2 package (Standard ABAP): isolated unclean access ---
" This class is released in ADT as C1 / "Use in Cloud Development".
CLASS zcl_legacy_pricing_adapter DEFINITION
  PUBLIC FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    INTERFACES zif_legacy_pricing.

ENDCLASS.

CLASS zcl_legacy_pricing_adapter IMPLEMENTATION.

  METHOD zif_legacy_pricing~read_condition.
    DATA legacy_condition TYPE zlegacy_s_condition.

    " Access to the unreleased object happens ONLY here —
    " in this example an old Z function module
    CALL FUNCTION 'Z_LEGACY_PRICING_READ'
      EXPORTING
        iv_vbeln     = order_id
      IMPORTING
        es_condition = legacy_condition
      EXCEPTIONS
        not_found    = 1
        OTHERS       = 2.

    IF sy-subrc <> 0.
      RAISE EXCEPTION NEW zcx_pricing_error( ).
    ENDIF.

    " Return our own type — the legacy structure does not leak out
    condition = VALUE #( condition_type = legacy_condition-kschl
                         amount         = legacy_condition-kbetr
                         currency       = legacy_condition-waers ).
  ENDMETHOD.

ENDCLASS.

This pattern has a bigger side benefit than it appears: because your Tier 1 code now depends on an interface, it becomes testable. In a test you supply a test double instead of the real wrapper and verify the business logic without touching any SAP object. Clean core and testability are two outcomes of the same design move.

Three rules for keeping Tier 2 disciplined

Separate package, explicit name. Keep wrappers in their own package (for example ZCC_ADAPTER) so a developer adding code there knows what they are doing.

Keep them thin. A wrapper only translates: it calls, converts types and wraps errors in exceptions. The moment business logic enters a wrapper, Tier 2 silently turns into Tier 3.

Keep a register. For each wrapper, write down why it is needed and which released API would make it unnecessary. SAP releases new APIs with every release; without that list you will never remove a single wrapper.

Extension Points: BAdIs, CDS Extensions, RAP

Writing your own applications is half of clean core. The other half is changing the behaviour of SAP's standard applications — what used to be done with modifications or implicit enhancements. In ABAP Cloud, the way to do that runs through extension points SAP has explicitly opened.

Changing behaviour with a BAdI

If a released BAdI exists, its implementation can be written in an ABAP Cloud package. The difference from a modification: SAP promises to keep that call point and its interface; even if it changes the implementation inside, your code keeps getting called.

CLASS zcl_order_check_badi DEFINITION
  PUBLIC FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    INTERFACES if_badi_interface.
    " Illustrative name — the real interface name is in the BAdI definition
    INTERFACES if_ex_order_check.

  PRIVATE SECTION.
    CONSTANTS c_approval_limit TYPE p LENGTH 13 DECIMALS 2 VALUE '50000.00'.

ENDCLASS.

CLASS zcl_order_check_badi IMPLEMENTATION.

  METHOD if_ex_order_check~check.
    " Our own business rule — no SAP object is modified
    IF order-net_value > c_approval_limit.
      APPEND VALUE #( severity = 'E'
                      text     = 'Approval limit exceeded' ) TO messages.
    ENDIF.
  ENDMETHOD.

ENDCLASS.

Extending a CDS view

You can add your own fields to an SAP CDS view — but only if that view has been released for extension (C0). Not every released view is extensible; use (C1) and extension (C0) are separate promises. In a view entity extension, the name of the extension is the name of the DDL source; it is not written again after with:

" DDL source: ZX_SALESDOCUMENT_EXT
extend view entity I_SalesDocument with
{
  _Extension.ZZ1_ProjectCode,
  _Extension.ZZ1_ApprovalLevel
}

The ZZ1_-prefixed fields in the example are fields added with the key user Custom Fields app. Which view can be extended with which fields, and through which association, is defined in that view's extensibility information.

RAP behaviour extensions

To add your own validation, determination or action to one of SAP's RAP-based business objects, you use a behaviour extension. The condition is again the same: the business object must have been opened for extension.

" Simplified header — for SAP business objects, extensions are usually
" defined through the object's released interface
extension implementation in class zbp_salesorder_ext unique;

extend behavior for SalesOrder
{
  validation zz_ValidateProjectCode on save { field ZZ1_ProjectCode; }
}

The common logic of all three: you put your change not inside SAP's code but into the slot SAP opened for you. If there is no slot, the change cannot be made — which looks like a restriction but is the price of upgrade stability. If the extension point you need does not exist, requesting it from SAP is, in the long run, the shorter road compared with modifying.

Measuring Existing Code: ATC and Custom Code Migration

Everything so far has been about new code. The real question is usually this: how much of our existing custom code fits this model? That is not a question for guesswork; it gets measured.

1. Cloud readiness checks with ATC

SAP provides ready-made ATC check variants that test ABAP Cloud compliance. Their names vary by release (for example ABAP_CLOUD_READINESS); list the variants available on your system in ADT and confirm. When you run such a variant against your custom packages, each finding arrives as "which object cannot be used, and why".

The result of the first run is usually demoralising; in a large code base, hundreds or even thousands of findings are not surprising. What matters is not the total but how the findings are distributed.

2. The Custom Code Migration app

SAP's Custom Code Migration Fiori app aggregates ATC findings by object and package and can combine them with usage data. That is its most valuable feature: you see which custom code actually runs.

In most code bases a significant share never runs at all. Deleting dead code instead of migrating it is the fastest and technically cheapest way to shrink the scope of the move.

3. Turning findings into decisions

For each object there are three options, and the decision is economic rather than technical: delete (unused), migrate (rewrite in Tier 1), leave (keep it in Tier 3, do not touch).

The third is a legitimate decision and often the right one. There is no return in moving a report that runs once a year and that nobody touches to ABAP Cloud.

Five Common Misconceptions

Most arguments on this topic come not from technical disagreement but from people attaching different meanings to the same term.

  • "Clean core means no custom code." It does not. Clean core means custom code does not touch SAP's core. Ten thousand lines of custom code written in ABAP Cloud are fully compliant with clean core.
  • "It doesn't concern us on-premise." It is true that it is not mandatory. But upgrade costs are just as real on-premise, and SAP is opening new extension points according to this model. Every new object you write in classic ABAP today is a debt to be migrated later.
  • "We have to migrate everything right away." The model says the opposite. The existence of Tier 3 is an acknowledgement that existing code can stay. What matters is where new code gets written.
  • "ABAP Cloud is more restricted, so it is weaker." Most restrictions close off things you should be leaving behind anyway: dynpro, procedural code, direct access to standard tables. What remains is the whole of modern ABAP.
  • "Key user extensibility is enough; we don't need developers." The key user layer is very efficient for simple field and rule additions, but its limits are clear. Real business logic, integration and complex data models require developer extensibility. The two are not rivals but two tools of the same model.

A Practical Roadmap

A sequence you can start on from day one, without announcing a large transformation programme:

Step 1 — Measure

Run the ATC cloud readiness variant against your custom packages and add usage data with Custom Code Migration. Output: an inventory with the number of findings per object and real usage information.

Step 2 — Clean out dead code

Remove objects that never run from scope and delete them. This step directly lowers the cost of every step after it and carries the lowest technical risk.

Step 3 — Set the rule for new code

From today, new development starts in ABAP Cloud packages. This single decision stops the debt from growing and lets the team learn the model on real work. It is also the most effective form of training.

Step 4 — Build the wrapper register

In your first ABAP Cloud project, make a Tier 2 decision for every object the compiler objects to. Collect the wrappers in a single package and record the reasons.

Step 5 — Order migration by value

Migrate existing code not wholesale but by how often it is touched. Frequently changing, business-critical objects first; reports that run once a year never. If you are touching an object for maintenance anyway, that is the cheapest moment to migrate it.

Step 6 — Prevent backsliding

Bind the ATC check variant to transport release. Otherwise the model is only as valid as the discipline of the most hurried person on the team. You can use the same mechanism for unit tests as well.

Conclusion

Clean core is not a regulation that restricts your freedom to write code; it is a trade that moves upgrade cost to development time. Every shortcut that used to look cheap got billed in the upgrade project, and that bill was never known in advance. ABAP Cloud brings the cost forward and makes it predictable: you think a little more today, and much less at upgrade time.

The model is workable thanks to Tier 2. Had it said "everything will be clean", it could not have been applied in any enterprise system. It works in real projects because, instead of banning unclean access, it proposes isolating and documenting it.

The starting point is not a large programme but a single decision: open the next new object in an ABAP Cloud package. The first three restrictions you run into will tell you more about your code base than weeks of analysis.

Get support with a cloud readiness analysis of your custom code base and an ABAP Cloud migration plan.

Request an Initial Consultation
← Back to Blog

Contact

Get in touch for your projects.