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
| Concept | Description | ABAP Cloud Mapping |
|---|---|---|
| Domain Core | Pure business logic without dependencies | ABAP classes with interfaces |
| Inbound Port | Interface for incoming requests | Interface for use cases |
| Outbound Port | Interface for outgoing calls | Interface for repositories/services |
| Driving Adapter | Calls the application | RAP Behavior Handler, HTTP Handler |
| Driven Adapter | Called by the application | Repository implementation, HTTP Client |
Why Hexagonal Architecture in ABAP Cloud?
Advantages
- Testability: Domain logic can be tested without database or external services
- Interchangeability: Adapters can be replaced without changing business logic
- Independence: The domain knows no technical details (RAP, HTTP, DB)
- Maintainability: Clear boundaries make changes easier
- Framework Independence: Migration to new technologies becomes simpler
When Is It Worth the Effort?
| Scenario | Recommendation |
|---|---|
| Complex business logic with many rules | Hexagonal Architecture |
| Simple CRUD operations | Standard RAP (Managed) |
| Integration of multiple external services | Hexagonal Architecture |
| Prototype or MVP | Standard RAP |
| Long-lived enterprise application | Hexagonal Architecture |
| High test coverage required | Hexagonal 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 servicesCLASS 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 CalculatorCLASS 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 InterfaceINTERFACE 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 CasesCLASS 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 AdapterCLASS 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 InjectionCLASS 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 InterfaceINTERFACE 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 RepositoryCLASS 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 HTTPCLASS 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
| Aspect | Traditional ABAP | Hexagonal Architecture |
|---|---|---|
| Database access | SELECT directly in business logic | Via Repository Interface |
| External services | HTTP calls in business logic | Via Service Interface |
| Testability | Difficult (DB dependency) | Easy (Mock Repositories) |
| Framework coupling | Strong (RAP used directly) | Loose (RAP only as adapter) |
| Changeability | Changes affect many places | Changes locally contained |
| Reusability | Low | High (Domain can be reused) |
Traditional ABAP (Anti-Pattern)
" Everything mixed - hard to testMETHOD 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 testMETHOD 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 TestsCLASS 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 DBCLASS 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
| Layer | Responsibility | ABAP Cloud Artifacts |
|---|---|---|
| Domain | Pure business logic | Classes without dependencies |
| Application | Use Case orchestration | Service classes with interfaces |
| Inbound Adapter | Incoming requests | RAP Handler, HTTP Handler |
| Outbound Adapter | External systems | Repository, HTTP Client |
| Ports | Abstraction | Interfaces |
Hexagonal Architecture requires more initial effort but pays off in complex, long-lived applications through better testability, maintainability, and flexibility.
Related Topics
- Domain-Driven Design with RAP - DDD concepts in ABAP Cloud
- Design Patterns for RAP - Proven architecture patterns
- Clean ABAP - Clean, maintainable ABAP code
- Test Doubles and Mocking - Developing testable applications
- RAP Basics - Fundamentals of RESTful ABAP Programming