Hexagonal Architecture in ABAP Cloud: Ports & Adapters Pattern

Category
Best Practices
Published
Author
Johannes

Hexagonal Architecture (also known as Ports & Adapters) is an architectural pattern that isolates core business logic from external dependencies. In ABAP Cloud, this pattern enables testable, maintainable, and flexible applications.

What Is Hexagonal Architecture?

Hexagonal Architecture was developed by Alistair Cockburn and solves a fundamental problem: How do I separate business logic from technical details?

┌─────────────────────────────────────────────┐
│ DRIVING ADAPTERS │
│ (UI, REST API, Tests, Batch Jobs) │
└──────────────────┬──────────────────────────┘
┌──────────────────────────────────────────────┐
│ INBOUND PORTS │
│ (Interfaces / Use Cases) │
└──────────────────┬───────────────────────────┘
┌──────────────────▼───────────────────────────┐
│ │
│ DOMAIN CORE │
│ (Business Logic) │
│ │
└──────────────────┬───────────────────────────┘
┌──────────────────▼───────────────────────────┐
│ OUTBOUND PORTS │
│ (Repository, Service Interfaces) │
└──────────────────┬───────────────────────────┘
┌──────────────────────────────────────────────┐
│ DRIVEN ADAPTERS │
│ (Database, External APIs, File System) │
└──────────────────────────────────────────────┘

Core Concepts

ConceptDescriptionABAP Cloud Mapping
Domain CorePure business logic without dependenciesABAP classes with interfaces
Inbound PortInterface for incoming requestsInterface for use cases
Outbound PortInterface for outgoing callsInterface for repositories/services
Driving AdapterCalls the applicationRAP Behavior Handler, HTTP Handler
Driven AdapterCalled by the applicationRepository implementation, HTTP Client

Why Hexagonal Architecture in ABAP Cloud?

Advantages

  1. Testability: Domain logic can be tested without database or external services
  2. Interchangeability: Adapters can be replaced without changing business logic
  3. Independence: The domain knows no technical details (RAP, HTTP, DB)
  4. Maintainability: Clear boundaries make changes easier
  5. Framework Independence: Migration to new technologies becomes simpler

When Is It Worth the Effort?

ScenarioRecommendation
Complex business logic with many rulesHexagonal Architecture
Simple CRUD operationsStandard RAP (Managed)
Integration of multiple external servicesHexagonal Architecture
Prototype or MVPStandard RAP
Long-lived enterprise applicationHexagonal Architecture
High test coverage requiredHexagonal Architecture

Mapping to ABAP Cloud Concepts

┌─────────────────────────────────────────────────────────────────┐
│ ABAP CLOUD HEXAGONAL │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ DRIVING ADAPTERS │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────────────────┐ │ │
│ │ │ RAP │ │ HTTP │ │ Background Job │ │ │
│ │ │ Behavior │ │ Handler │ │ (Application Job) │ │ │
│ │ └─────┬──────┘ └─────┬──────┘ └──────────┬─────────────┘ │ │
│ └───────┼──────────────┼───────────────────┼───────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ INBOUND PORTS (Interfaces) │ │
│ │ zif_order_service, zif_invoice_service │ │
│ └────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼─────────────────────────────────┐ │
│ │ APPLICATION LAYER (Use Cases) │ │
│ │ zcl_create_order_use_case, zcl_cancel_order_use_case │ │
│ └────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼─────────────────────────────────┐ │
│ │ DOMAIN LAYER │ │
│ │ zcl_order (Entity), zcl_order_line (Entity) │ │
│ │ zcl_price_calculator (Domain Service) │ │
│ └────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼─────────────────────────────────┐ │
│ │ OUTBOUND PORTS (Interfaces) │ │
│ │ zif_order_repository, zif_customer_service │ │
│ └────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼─────────────────────────────────┐ │
│ │ DRIVEN ADAPTERS │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────────────────┐ │ │
│ │ │ DB │ │ HTTP │ │ SAP ERP │ │ │
│ │ │ Repository │ │ Client │ │ Integration │ │ │
│ │ └────────────┘ └────────────┘ └────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Domain Layer: The Business Logic

The Domain Layer contains pure business logic without any dependency on technical frameworks.

Domain Entity

" Domain Entity: Order
" No dependencies on RAP, database, or external services
CLASS zcl_order DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_order_data,
order_id TYPE sysuuid_x16,
customer_id TYPE sysuuid_x16,
status TYPE string,
total_amount TYPE decfloat34,
currency TYPE waers,
created_at TYPE timestampl,
END OF ty_order_data,
BEGIN OF ty_order_line,
line_id TYPE i,
product_id TYPE sysuuid_x16,
quantity TYPE i,
unit_price TYPE decfloat34,
amount TYPE decfloat34,
END OF ty_order_line,
ty_order_lines TYPE STANDARD TABLE OF ty_order_line WITH KEY line_id.
" Factory Method
CLASS-METHODS create
IMPORTING
iv_customer_id TYPE sysuuid_x16
iv_currency TYPE waers
RETURNING
VALUE(ro_order) TYPE REF TO zcl_order
RAISING
zcx_order_error.
" Reconstruct from persistence
CLASS-METHODS reconstitute
IMPORTING
is_data TYPE ty_order_data
it_lines TYPE ty_order_lines
RETURNING
VALUE(ro_order) TYPE REF TO zcl_order.
" Business Methods
METHODS add_line
IMPORTING
iv_product_id TYPE sysuuid_x16
iv_quantity TYPE i
iv_unit_price TYPE decfloat34
RAISING
zcx_order_error.
METHODS submit
RAISING
zcx_order_error.
METHODS cancel
IMPORTING
iv_reason TYPE string
RAISING
zcx_order_error.
" Getters
METHODS get_data
RETURNING
VALUE(rs_data) TYPE ty_order_data.
METHODS get_lines
RETURNING
VALUE(rt_lines) TYPE ty_order_lines.
METHODS is_editable
RETURNING
VALUE(rv_editable) TYPE abap_bool.
PRIVATE SECTION.
DATA ms_data TYPE ty_order_data.
DATA mt_lines TYPE ty_order_lines.
DATA mv_next_line_id TYPE i.
METHODS calculate_total.
CONSTANTS:
c_status_draft TYPE string VALUE 'DRAFT',
c_status_submitted TYPE string VALUE 'SUBMITTED',
c_status_cancelled TYPE string VALUE 'CANCELLED'.
ENDCLASS.
CLASS zcl_order IMPLEMENTATION.
METHOD create.
" Business Rule: Customer ID is mandatory
IF iv_customer_id IS INITIAL.
RAISE EXCEPTION TYPE zcx_order_error
MESSAGE e001(zorder) WITH 'Customer ID is required'.
ENDIF.
ro_order = NEW zcl_order( ).
ro_order->ms_data = VALUE #(
order_id = cl_system_uuid=>create_uuid_x16_static( )
customer_id = iv_customer_id
currency = iv_currency
status = c_status_draft
created_at = utclong_current( )
).
ro_order->mv_next_line_id = 1.
ENDMETHOD.
METHOD reconstitute.
ro_order = NEW zcl_order( ).
ro_order->ms_data = is_data.
ro_order->mt_lines = it_lines.
ro_order->mv_next_line_id = REDUCE #(
INIT max = 0
FOR line IN it_lines
NEXT max = COND #( WHEN line-line_id > max THEN line-line_id ELSE max )
) + 1.
ENDMETHOD.
METHOD add_line.
" Business Rule: Can only add lines to draft orders
IF ms_data-status <> c_status_draft.
RAISE EXCEPTION TYPE zcx_order_error
MESSAGE e002(zorder) WITH 'Cannot add lines to non-draft order'.
ENDIF.
" Business Rule: Quantity must be positive
IF iv_quantity <= 0.
RAISE EXCEPTION TYPE zcx_order_error
MESSAGE e003(zorder) WITH 'Quantity must be positive'.
ENDIF.
APPEND VALUE #(
line_id = mv_next_line_id
product_id = iv_product_id
quantity = iv_quantity
unit_price = iv_unit_price
amount = iv_quantity * iv_unit_price
) TO mt_lines.
mv_next_line_id = mv_next_line_id + 1.
calculate_total( ).
ENDMETHOD.
METHOD submit.
" Business Rule: Cannot submit empty order
IF mt_lines IS INITIAL.
RAISE EXCEPTION TYPE zcx_order_error
MESSAGE e004(zorder) WITH 'Cannot submit empty order'.
ENDIF.
" Business Rule: Only draft orders can be submitted
IF ms_data-status <> c_status_draft.
RAISE EXCEPTION TYPE zcx_order_error
MESSAGE e005(zorder) WITH 'Only draft orders can be submitted'.
ENDIF.
ms_data-status = c_status_submitted.
ENDMETHOD.
METHOD cancel.
" Business Rule: Cannot cancel already cancelled orders
IF ms_data-status = c_status_cancelled.
RAISE EXCEPTION TYPE zcx_order_error
MESSAGE e006(zorder) WITH 'Order is already cancelled'.
ENDIF.
ms_data-status = c_status_cancelled.
ENDMETHOD.
METHOD calculate_total.
ms_data-total_amount = REDUCE #(
INIT sum = CONV decfloat34( 0 )
FOR line IN mt_lines
NEXT sum = sum + line-amount
).
ENDMETHOD.
METHOD get_data.
rs_data = ms_data.
ENDMETHOD.
METHOD get_lines.
rt_lines = mt_lines.
ENDMETHOD.
METHOD is_editable.
rv_editable = xsdbool( ms_data-status = c_status_draft ).
ENDMETHOD.
ENDCLASS.

Domain Service

For logic that doesn’t belong to a single entity:

" Domain Service: Price Calculator
CLASS zcl_price_calculator DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_price_result,
net_amount TYPE decfloat34,
tax_amount TYPE decfloat34,
gross_amount TYPE decfloat34,
tax_rate TYPE decfloat34,
END OF ty_price_result.
METHODS calculate
IMPORTING
iv_base_price TYPE decfloat34
iv_quantity TYPE i
iv_country TYPE land1
iv_product_type TYPE string
RETURNING
VALUE(rs_result) TYPE ty_price_result.
PRIVATE SECTION.
METHODS get_tax_rate
IMPORTING
iv_country TYPE land1
iv_product_type TYPE string
RETURNING
VALUE(rv_rate) TYPE decfloat34.
ENDCLASS.
CLASS zcl_price_calculator IMPLEMENTATION.
METHOD calculate.
DATA(lv_tax_rate) = get_tax_rate(
iv_country = iv_country
iv_product_type = iv_product_type
).
rs_result-net_amount = iv_base_price * iv_quantity.
rs_result-tax_rate = lv_tax_rate.
rs_result-tax_amount = rs_result-net_amount * lv_tax_rate.
rs_result-gross_amount = rs_result-net_amount + rs_result-tax_amount.
ENDMETHOD.
METHOD get_tax_rate.
" Pure business logic - no external dependencies
rv_rate = SWITCH #( iv_country
WHEN 'DE' THEN COND #(
WHEN iv_product_type = 'FOOD' THEN '0.07'
ELSE '0.19' )
WHEN 'AT' THEN '0.20'
WHEN 'CH' THEN '0.081'
ELSE '0.19'
).
ENDMETHOD.
ENDCLASS.

Application Layer: Use Cases

The Application Layer orchestrates domain logic and coordinates data flow.

Inbound Port (Interface)

" Inbound Port: Order Service Interface
INTERFACE zif_order_service
PUBLIC.
TYPES:
BEGIN OF ty_create_order_request,
customer_id TYPE sysuuid_x16,
currency TYPE waers,
END OF ty_create_order_request,
BEGIN OF ty_add_line_request,
order_id TYPE sysuuid_x16,
product_id TYPE sysuuid_x16,
quantity TYPE i,
END OF ty_add_line_request,
BEGIN OF ty_order_response,
order_id TYPE sysuuid_x16,
status TYPE string,
total_amount TYPE decfloat34,
currency TYPE waers,
END OF ty_order_response.
METHODS create_order
IMPORTING
is_request TYPE ty_create_order_request
RETURNING
VALUE(rs_response) TYPE ty_order_response
RAISING
zcx_order_error.
METHODS add_order_line
IMPORTING
is_request TYPE ty_add_line_request
RAISING
zcx_order_error.
METHODS submit_order
IMPORTING
iv_order_id TYPE sysuuid_x16
RAISING
zcx_order_error.
METHODS cancel_order
IMPORTING
iv_order_id TYPE sysuuid_x16
iv_reason TYPE string
RAISING
zcx_order_error.
ENDINTERFACE.

Use Case Implementation

" Application Service implementing the Use Cases
CLASS zcl_order_service DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_order_service.
METHODS constructor
IMPORTING
io_order_repository TYPE REF TO zif_order_repository
io_product_repository TYPE REF TO zif_product_repository
io_price_calculator TYPE REF TO zcl_price_calculator.
PRIVATE SECTION.
DATA mo_order_repository TYPE REF TO zif_order_repository.
DATA mo_product_repository TYPE REF TO zif_product_repository.
DATA mo_price_calculator TYPE REF TO zcl_price_calculator.
ENDCLASS.
CLASS zcl_order_service IMPLEMENTATION.
METHOD constructor.
mo_order_repository = io_order_repository.
mo_product_repository = io_product_repository.
mo_price_calculator = io_price_calculator.
ENDMETHOD.
METHOD zif_order_service~create_order.
" Create new order using domain logic
DATA(lo_order) = zcl_order=>create(
iv_customer_id = is_request-customer_id
iv_currency = is_request-currency
).
" Persist via repository
mo_order_repository->save( lo_order ).
" Return response
DATA(ls_data) = lo_order->get_data( ).
rs_response = VALUE #(
order_id = ls_data-order_id
status = ls_data-status
total_amount = ls_data-total_amount
currency = ls_data-currency
).
ENDMETHOD.
METHOD zif_order_service~add_order_line.
" Load order from repository
DATA(lo_order) = mo_order_repository->find_by_id( is_request-order_id ).
IF lo_order IS NOT BOUND.
RAISE EXCEPTION TYPE zcx_order_error
MESSAGE e010(zorder) WITH 'Order not found'.
ENDIF.
" Get product price from repository
DATA(lo_product) = mo_product_repository->find_by_id( is_request-product_id ).
IF lo_product IS NOT BOUND.
RAISE EXCEPTION TYPE zcx_order_error
MESSAGE e011(zorder) WITH 'Product not found'.
ENDIF.
" Add line using domain logic
lo_order->add_line(
iv_product_id = is_request-product_id
iv_quantity = is_request-quantity
iv_unit_price = lo_product->get_price( )
).
" Persist changes
mo_order_repository->save( lo_order ).
ENDMETHOD.
METHOD zif_order_service~submit_order.
DATA(lo_order) = mo_order_repository->find_by_id( iv_order_id ).
IF lo_order IS NOT BOUND.
RAISE EXCEPTION TYPE zcx_order_error
MESSAGE e010(zorder) WITH 'Order not found'.
ENDIF.
lo_order->submit( ).
mo_order_repository->save( lo_order ).
ENDMETHOD.
METHOD zif_order_service~cancel_order.
DATA(lo_order) = mo_order_repository->find_by_id( iv_order_id ).
IF lo_order IS NOT BOUND.
RAISE EXCEPTION TYPE zcx_order_error
MESSAGE e010(zorder) WITH 'Order not found'.
ENDIF.
lo_order->cancel( iv_reason = iv_reason ).
mo_order_repository->save( lo_order ).
ENDMETHOD.
ENDCLASS.

Adapter for RAP (Inbound Port)

The RAP Behavior Handler acts as a Driving Adapter that receives requests and forwards them to the Application Layer.

RAP as Inbound Adapter

" RAP Behavior Handler as Inbound Adapter
CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
DATA mo_order_service TYPE REF TO zif_order_service.
METHODS get_order_service
RETURNING
VALUE(ro_service) TYPE REF TO zif_order_service.
METHODS create FOR MODIFY
IMPORTING entities FOR CREATE order.
METHODS submit FOR MODIFY
IMPORTING keys FOR ACTION order~submit.
METHODS cancel FOR MODIFY
IMPORTING keys FOR ACTION order~cancel.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD get_order_service.
IF mo_order_service IS NOT BOUND.
" Dependency Injection via Factory
mo_order_service = zcl_service_factory=>get_order_service( ).
ENDIF.
ro_service = mo_order_service.
ENDMETHOD.
METHOD create.
DATA(lo_service) = get_order_service( ).
LOOP AT entities ASSIGNING FIELD-SYMBOL(<entity>).
TRY.
DATA(ls_response) = lo_service->create_order(
VALUE #(
customer_id = <entity>-CustomerID
currency = <entity>-Currency
)
).
" Map response to RAP result
APPEND VALUE #(
%cid = <entity>-%cid
%key = VALUE #( OrderID = ls_response-order_id )
OrderID = ls_response-order_id
) TO mapped-order.
CATCH zcx_order_error INTO DATA(lx_error).
APPEND VALUE #(
%cid = <entity>-%cid
%msg = lx_error
%element-CustomerID = if_abap_behv=>mk-on
) TO reported-order.
APPEND VALUE #( %cid = <entity>-%cid ) TO failed-order.
ENDTRY.
ENDLOOP.
ENDMETHOD.
METHOD submit.
DATA(lo_service) = get_order_service( ).
LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>).
TRY.
lo_service->submit_order( iv_order_id = <key>-OrderID ).
APPEND VALUE #( %tky = <key>-%tky ) TO result.
CATCH zcx_order_error INTO DATA(lx_error).
APPEND VALUE #(
%tky = <key>-%tky
%msg = lx_error
) TO reported-order.
APPEND VALUE #( %tky = <key>-%tky ) TO failed-order.
ENDTRY.
ENDLOOP.
ENDMETHOD.
METHOD cancel.
DATA(lo_service) = get_order_service( ).
LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>).
TRY.
lo_service->cancel_order(
iv_order_id = <key>-OrderID
iv_reason = <key>-%param-Reason
).
APPEND VALUE #( %tky = <key>-%tky ) TO result.
CATCH zcx_order_error INTO DATA(lx_error).
APPEND VALUE #(
%tky = <key>-%tky
%msg = lx_error
) TO reported-order.
APPEND VALUE #( %tky = <key>-%tky ) TO failed-order.
ENDTRY.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Service Factory for Dependency Injection

" Factory for Service Creation with Dependency Injection
CLASS zcl_service_factory DEFINITION
PUBLIC
FINAL
CREATE PRIVATE.
PUBLIC SECTION.
CLASS-METHODS get_order_service
RETURNING
VALUE(ro_service) TYPE REF TO zif_order_service.
" For testing: inject mock implementations
CLASS-METHODS set_order_service
IMPORTING
io_service TYPE REF TO zif_order_service.
CLASS-METHODS reset.
PRIVATE SECTION.
CLASS-DATA go_order_service TYPE REF TO zif_order_service.
ENDCLASS.
CLASS zcl_service_factory IMPLEMENTATION.
METHOD get_order_service.
IF go_order_service IS BOUND.
ro_service = go_order_service.
RETURN.
ENDIF.
" Create real implementations
DATA(lo_order_repo) = NEW zcl_order_repository_db( ).
DATA(lo_product_repo) = NEW zcl_product_repository_db( ).
DATA(lo_price_calc) = NEW zcl_price_calculator( ).
ro_service = NEW zcl_order_service(
io_order_repository = lo_order_repo
io_product_repository = lo_product_repo
io_price_calculator = lo_price_calc
).
ENDMETHOD.
METHOD set_order_service.
go_order_service = io_service.
ENDMETHOD.
METHOD reset.
CLEAR go_order_service.
ENDMETHOD.
ENDCLASS.

Adapter for External Services (Outbound Port)

Outbound Port Definition

" Outbound Port: Order Repository Interface
INTERFACE zif_order_repository
PUBLIC.
METHODS find_by_id
IMPORTING
iv_order_id TYPE sysuuid_x16
RETURNING
VALUE(ro_order) TYPE REF TO zcl_order.
METHODS find_by_customer
IMPORTING
iv_customer_id TYPE sysuuid_x16
RETURNING
VALUE(rt_orders) TYPE zcl_order=>ty_order_list.
METHODS save
IMPORTING
io_order TYPE REF TO zcl_order
RAISING
zcx_persistence_error.
METHODS delete
IMPORTING
iv_order_id TYPE sysuuid_x16
RAISING
zcx_persistence_error.
ENDINTERFACE.

Database Adapter Implementation

" Driven Adapter: Database Repository
CLASS zcl_order_repository_db DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_order_repository.
ENDCLASS.
CLASS zcl_order_repository_db IMPLEMENTATION.
METHOD zif_order_repository~find_by_id.
SELECT SINGLE * FROM zorder
WHERE order_id = @iv_order_id
INTO @DATA(ls_order).
IF sy-subrc <> 0. RETURN. ENDIF.
SELECT * FROM zorder_item
WHERE order_id = @iv_order_id
INTO TABLE @DATA(lt_lines).
" Map DB records to Domain Entity
ro_order = zcl_order=>reconstitute(
is_data = CORRESPONDING #( ls_order )
it_lines = CORRESPONDING #( lt_lines )
).
ENDMETHOD.
METHOD zif_order_repository~save.
DATA(ls_data) = io_order->get_data( ).
DATA(lt_lines) = io_order->get_lines( ).
" Map Domain Entity to DB records and persist
MODIFY zorder FROM @( CORRESPONDING zorder( ls_data ) ).
DELETE FROM zorder_item WHERE order_id = @ls_data-order_id.
INSERT zorder_item FROM TABLE @( CORRESPONDING #( lt_lines ) ).
ENDMETHOD.
ENDCLASS.

HTTP Adapter for External APIs

" Driven Adapter: External Customer Service via HTTP
CLASS zcl_customer_service_http DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_customer_service.
ENDCLASS.
CLASS zcl_customer_service_http IMPLEMENTATION.
METHOD zif_customer_service~get_customer.
" HTTP Client via Communication Arrangement
DATA(lo_dest) = cl_http_destination_provider=>create_by_comm_arrangement(
comm_scenario = 'ZCUSTOMER_API'
service_id = 'ZCUSTOMER_REST'
).
DATA(lo_client) = cl_web_http_client_manager=>create_by_http_destination( lo_dest ).
DATA(lo_response) = lo_client->execute( if_web_http_client=>get ).
/ui2/cl_json=>deserialize(
EXPORTING json = lo_response->get_text( )
CHANGING data = rs_customer
).
ENDMETHOD.
ENDCLASS.

Comparison with Traditional ABAP Architecture

AspectTraditional ABAPHexagonal Architecture
Database accessSELECT directly in business logicVia Repository Interface
External servicesHTTP calls in business logicVia Service Interface
TestabilityDifficult (DB dependency)Easy (Mock Repositories)
Framework couplingStrong (RAP used directly)Loose (RAP only as adapter)
ChangeabilityChanges affect many placesChanges locally contained
ReusabilityLowHigh (Domain can be reused)

Traditional ABAP (Anti-Pattern)

" Everything mixed - hard to test
METHOD create_order.
" Direct DB access in business logic
SELECT SINGLE * FROM zcustomer
WHERE customer_id = @iv_customer_id
INTO @DATA(ls_customer).
IF ls_customer-status <> 'ACTIVE'.
" Direct message handling
MESSAGE e001(zorder).
RETURN.
ENDIF.
" HTTP call directly in method
DATA(lo_client) = cl_http_client=>create_by_destination( 'PRICING_API' ).
" ... HTTP logic ...
" DB Insert directly
INSERT INTO zorder VALUES @ls_order.
ENDMETHOD.

Hexagonal Architecture (Clean)

" Cleanly separated - easy to test
METHOD create_order.
" Validation via Service Interface
IF NOT mo_customer_service->validate_customer( iv_customer_id ).
RAISE EXCEPTION TYPE zcx_order_error.
ENDIF.
" Domain logic
DATA(lo_order) = zcl_order=>create(
iv_customer_id = iv_customer_id
iv_currency = iv_currency
).
" Persistence via Repository Interface
mo_order_repository->save( lo_order ).
ENDMETHOD.

Testability Through Mock Repositories

" Mock Repository for Unit Tests
CLASS lcl_mock_order_repository DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES zif_order_repository.
DATA mt_saved_orders TYPE TABLE OF REF TO zcl_order.
DATA mo_return_order TYPE REF TO zcl_order.
ENDCLASS.
CLASS lcl_mock_order_repository IMPLEMENTATION.
METHOD zif_order_repository~find_by_id.
ro_order = mo_return_order.
ENDMETHOD.
METHOD zif_order_repository~save.
APPEND io_order TO mt_saved_orders.
ENDMETHOD.
ENDCLASS.
" Unit Test - Domain logic testable without DB
CLASS ltcl_order_service DEFINITION FOR TESTING DURATION SHORT.
PRIVATE SECTION.
METHODS test_create_order FOR TESTING.
ENDCLASS.
CLASS ltcl_order_service IMPLEMENTATION.
METHOD test_create_order.
DATA(lo_mock_repo) = NEW lcl_mock_order_repository( ).
DATA(lo_service) = NEW zcl_order_service(
io_order_repository = lo_mock_repo
io_price_calculator = NEW zcl_price_calculator( )
).
DATA(ls_response) = lo_service->zif_order_service~create_order(
VALUE #( customer_id = cl_system_uuid=>create_uuid_x16_static( ) currency = 'EUR' )
).
" Assertions
cl_abap_unit_assert=>assert_not_initial( ls_response-order_id ).
cl_abap_unit_assert=>assert_equals( act = lines( lo_mock_repo->mt_saved_orders ) exp = 1 ).
ENDMETHOD.
ENDCLASS.

Summary

LayerResponsibilityABAP Cloud Artifacts
DomainPure business logicClasses without dependencies
ApplicationUse Case orchestrationService classes with interfaces
Inbound AdapterIncoming requestsRAP Handler, HTTP Handler
Outbound AdapterExternal systemsRepository, HTTP Client
PortsAbstractionInterfaces

Hexagonal Architecture requires more initial effort but pays off in complex, long-lived applications through better testability, maintainability, and flexibility.