Domain-Driven Design (DDD) is an approach to software development that puts the business domain at the center. RAP (RESTful ABAP Programming) provides ideal conditions for implementing DDD principles: Business Objects, Entities, and Compositions directly mirror DDD concepts.
Why DDD with RAP?
| DDD Concept | RAP Implementation | Advantage |
|---|---|---|
| Bounded Context | Software Component / Package | Clear separation of business areas |
| Aggregate | Business Object (Root Entity) | Consistency boundaries defined |
| Entity | CDS View Entity | Identity and lifecycle |
| Value Object | CDS Structure / Embedded View | Immutable value objects |
| Domain Event | RAP Business Event | Loose coupling between contexts |
| Repository | RAP Runtime (EML) | Abstraction of persistence |
DDD Core Concepts
Bounded Context
A Bounded Context is a logical boundary within which a specific domain model applies. In ABAP Cloud, this typically corresponds to a Software Component or a package tree.
┌─────────────────────────────────────────────────────────────┐│ ENTERPRISE ││ ┌─────────────────────┐ ┌─────────────────────────────┐ ││ │ SALES CONTEXT │ │ LOGISTICS CONTEXT │ ││ │ ┌───────────────┐ │ │ ┌───────────────────────┐ │ ││ │ │ Order (BO) │ │ │ │ Shipment (BO) │ │ ││ │ │ Customer │──┼────┼──│ DeliveryAddress │ │ ││ │ │ OrderItem │ │ │ │ ShipmentItem │ │ ││ │ └───────────────┘ │ │ └───────────────────────┘ │ ││ │ ┌───────────────┐ │ │ ┌───────────────────────┐ │ ││ │ │ Product (ref) │◄─┼────┼──│ Inventory (BO) │ │ ││ │ └───────────────┘ │ │ │ StockLevel │ │ ││ └─────────────────────┘ │ └───────────────────────┘ │ ││ └─────────────────────────────┘ │└─────────────────────────────────────────────────────────────┘Aggregate and Root Entity
An Aggregate is a group of related objects with a Root Entity. In RAP, the Root Entity corresponds to the Business Object with define root view entity:
" ROOT ENTITY = Aggregate Root@AbapCatalog.viewEnhancementCategory: [#NONE]@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Order - Root Entity (Aggregate)'
define root view entity ZI_Order as select from zorder composition [0..*] of ZI_OrderItem as _Items association [0..1] to ZI_Customer as _Customer on $projection.CustomerId = _Customer.CustomerId{ key order_id as OrderId, customer_id as CustomerId, order_date as OrderDate, @Semantics.amount.currencyCode: 'CurrencyCode' total_amount as TotalAmount, @Semantics.currencyCode: true currency_code as CurrencyCode, status as Status,
" Administrative fields @Semantics.user.createdBy: true created_by as CreatedBy, @Semantics.systemDateTime.createdAt: true created_at as CreatedAt,
" Associations _Items, _Customer}Child Entity (Part of the Aggregate)
Entities within an Aggregate are only reached through the Root Entity:
" CHILD ENTITY - Part of the Order Aggregate@AbapCatalog.viewEnhancementCategory: [#NONE]@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Order Item - Child Entity'
define view entity ZI_OrderItem as select from zorder_item association to parent ZI_Order as _Order on $projection.OrderId = _Order.OrderId association [1..1] to ZI_Product as _Product on $projection.ProductId = _Product.ProductId{ key order_id as OrderId, key item_id as ItemId, product_id as ProductId, quantity as Quantity, @Semantics.amount.currencyCode: 'CurrencyCode' unit_price as UnitPrice, @Semantics.currencyCode: true currency_code as CurrencyCode,
" Calculated field quantity * unit_price as LineTotal,
_Order, _Product}Value Object
Value Objects have no identity of their own and are defined by their attributes. In RAP, they are modeled as embedded structures or calculated fields:
" Value Object as CDS Structure@EndUserText.label: 'Address Value Object'define structure zs_address { street : abap.char(60); house_number: abap.char(10); postal_code : abap.char(10); city : abap.char(40); country : land1;}
" Usage in Entitydefine root view entity ZI_Customer as select from zcustomer{ key customer_id as CustomerId, customer_name as CustomerName,
" Value Object: Address street as AddressStreet, house_number as AddressHouseNumber, postal_code as AddressPostalCode, city as AddressCity, country as AddressCountry}Implementing Aggregate Rules in RAP
Rule 1: Access Only Through Aggregate Root
The Behavior Definition enforces that Child Entities are only manipulated through the Root Entity:
managed implementation in class zbp_i_order unique;strict ( 2 );
define behavior for ZI_Order alias Orderpersistent table zorderlock masterauthorization master ( instance )etag master LastChangedAt{ create; update; delete;
" Items are created/changed through Order association _Items { create; }
" Ensure aggregate invariants validation validateOrderConsistency on save { create; update; }
" Domain Events event OrderCreated; event OrderConfirmed;}
define behavior for ZI_OrderItem alias OrderItempersistent table zorder_itemlock dependent by _Orderauthorization dependent by _Order{ update; delete;
" No direct creation - only through Order field ( readonly ) OrderId;
association _Order;}Rule 2: Protect Invariants
Aggregate invariants are checked in Validations:
CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler. PRIVATE SECTION. METHODS validateOrderConsistency FOR VALIDATE ON SAVE IMPORTING keys FOR Order~validateOrderConsistency.ENDCLASS.
CLASS lhc_order IMPLEMENTATION. METHOD validateOrderConsistency. " Check aggregate invariants READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_orders).
" Load items READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order BY \_Items ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_items).
LOOP AT lt_orders INTO DATA(ls_order). " Invariant 1: At least one item required DATA(lt_order_items) = FILTER #( lt_items WHERE OrderId = ls_order-OrderId ).
IF lines( lt_order_items ) = 0. APPEND VALUE #( %tky = ls_order-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = 'Order must contain at least one item' ) ) TO reported-order. APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order. CONTINUE. ENDIF.
" Invariant 2: Total amount must match items DATA(lv_calculated_total) = REDUCE decfloat34( INIT sum = CONV decfloat34( 0 ) FOR item IN lt_order_items NEXT sum = sum + ( item-Quantity * item-UnitPrice ) ).
IF ls_order-TotalAmount <> lv_calculated_total. APPEND VALUE #( %tky = ls_order-%tky %element-TotalAmount = if_abap_behv=>mk-on %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = 'Total amount does not match items' ) ) TO reported-order. APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order. ENDIF. ENDLOOP. ENDMETHOD.ENDCLASS.Rule 3: Transactional Consistency
An Aggregate is always saved as a whole:
METHOD create_order_with_items. " Deep Insert - Create aggregate as a whole MODIFY ENTITIES OF zi_order ENTITY Order CREATE FIELDS ( CustomerId OrderDate TotalAmount CurrencyCode Status ) WITH VALUE #( ( %cid = 'ORDER_1' CustomerId = iv_customer_id OrderDate = cl_abap_context_info=>get_system_date( ) TotalAmount = lv_total CurrencyCode = 'EUR' Status = 'NEW' ) )
CREATE BY \_Items FIELDS ( ProductId Quantity UnitPrice CurrencyCode ) WITH VALUE #( ( %cid_ref = 'ORDER_1' %target = VALUE #( ( %cid = 'ITEM_1' ProductId = 'PROD001' Quantity = 2 UnitPrice = '100.00' CurrencyCode = 'EUR' ) ( %cid = 'ITEM_2' ProductId = 'PROD002' Quantity = 1 UnitPrice = '250.00' CurrencyCode = 'EUR' ) ) ) ) MAPPED DATA(lt_mapped) FAILED DATA(lt_failed) REPORTED DATA(lt_reported).
" All or nothing - transactional IF lt_failed IS NOT INITIAL. " Rollback automatic through RAP Runtime RAISE EXCEPTION TYPE cx_order_creation_failed. ENDIF.
COMMIT ENTITIES.ENDMETHOD.Implementing Domain Services
Domain Services encapsulate business logic that doesn’t belong to a single entity:
" Interface for Domain ServiceINTERFACE zif_pricing_service. METHODS calculate_order_total IMPORTING it_items TYPE ztt_order_items iv_customer_tier TYPE zdd_customer_tier RETURNING VALUE(rs_result) TYPE zs_pricing_result.
METHODS apply_discount IMPORTING iv_subtotal TYPE decfloat34 iv_discount_code TYPE zdd_discount_code RETURNING VALUE(rv_total) TYPE decfloat34.ENDINTERFACE.
" ImplementationCLASS zcl_pricing_service DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES zif_pricing_service.
PRIVATE SECTION. METHODS get_customer_discount IMPORTING iv_tier TYPE zdd_customer_tier RETURNING VALUE(rv_discount) TYPE decfloat34.ENDCLASS.
CLASS zcl_pricing_service IMPLEMENTATION. METHOD zif_pricing_service~calculate_order_total. " Calculate subtotal DATA(lv_subtotal) = REDUCE decfloat34( INIT sum = CONV decfloat34( 0 ) FOR item IN it_items NEXT sum = sum + ( item-quantity * item-unit_price ) ).
" Apply customer discount DATA(lv_discount) = get_customer_discount( iv_customer_tier ). DATA(lv_discount_amount) = lv_subtotal * lv_discount / 100.
rs_result-subtotal = lv_subtotal. rs_result-discount_percentage = lv_discount. rs_result-discount_amount = lv_discount_amount. rs_result-total = lv_subtotal - lv_discount_amount. ENDMETHOD.
METHOD zif_pricing_service~apply_discount. " Validate and apply discount code SELECT SINGLE discount_percentage FROM zdiscount_codes WHERE code = @iv_discount_code AND valid_from <= @cl_abap_context_info=>get_system_date( ) AND valid_to >= @cl_abap_context_info=>get_system_date( ) INTO @DATA(lv_percentage).
IF sy-subrc = 0. rv_total = iv_subtotal * ( 1 - lv_percentage / 100 ). ELSE. rv_total = iv_subtotal. ENDIF. ENDMETHOD.
METHOD get_customer_discount. CASE iv_tier. WHEN 'GOLD'. rv_discount = 15. WHEN 'SILVER'. rv_discount = 10. WHEN 'BRONZE'. rv_discount = 5. WHEN OTHERS. rv_discount = 0. ENDCASE. ENDMETHOD.ENDCLASS.Using Domain Service in RAP Action
CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler. PRIVATE SECTION. METHODS recalculateTotal FOR MODIFY IMPORTING keys FOR ACTION Order~recalculateTotal RESULT result.ENDCLASS.
CLASS lhc_order IMPLEMENTATION. METHOD recalculateTotal. " Load Order and Items READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_orders).
READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order BY \_Items ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_items).
READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order BY \_Customer FIELDS ( CustomerTier ) WITH CORRESPONDING #( keys ) RESULT DATA(lt_customers).
" Instantiate Domain Service DATA(lo_pricing) = NEW zcl_pricing_service( ).
LOOP AT lt_orders INTO DATA(ls_order). " Get items and customer data DATA(lt_order_items) = FILTER #( lt_items WHERE OrderId = ls_order-OrderId ). DATA(ls_customer) = VALUE #( lt_customers[ CustomerId = ls_order-CustomerId ] OPTIONAL ).
" Call Domain Service DATA(ls_pricing) = lo_pricing->zif_pricing_service~calculate_order_total( it_items = CORRESPONDING #( lt_order_items ) iv_customer_tier = ls_customer-CustomerTier ).
" Update Aggregate MODIFY ENTITIES OF zi_order IN LOCAL MODE ENTITY Order UPDATE FIELDS ( TotalAmount DiscountAmount ) WITH VALUE #( ( %tky = ls_order-%tky TotalAmount = ls_pricing-total DiscountAmount = ls_pricing-discount_amount ) ). ENDLOOP.
" Return result READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order ALL FIELDS WITH CORRESPONDING #( keys ) RESULT result. ENDMETHOD.ENDCLASS.Domain Events in RAP
Domain Events signal important business occurrences and enable loose coupling between Bounded Contexts:
Defining Events
" Behavior Definition with Eventsmanaged implementation in class zbp_i_order unique;
define behavior for ZI_Order alias Order{ " ... Standard operations ...
" Declare Domain Events event OrderCreated parameter zs_order_created_event; event OrderConfirmed parameter zs_order_confirmed_event; event OrderShipped parameter zs_order_shipped_event; event OrderCancelled;}Event Parameter as Structure
" Event Payload Definition@EndUserText.label: 'Order Created Event'define abstract entity ZA_OrderCreatedEvent{ OrderId : abap.numc(10); CustomerId : abap.numc(10); OrderDate : abap.dats; TotalAmount : abap.dec(15,2); CurrencyCode : abap.cuky; ItemCount : abap.int4;}Raising Events
CLASS lhc_order IMPLEMENTATION. METHOD confirmOrder. " Change status MODIFY ENTITIES OF zi_order IN LOCAL MODE ENTITY Order UPDATE FIELDS ( Status ConfirmedAt ) WITH VALUE #( FOR key IN keys ( %tky = key-%tky Status = 'CONFIRMED' ConfirmedAt = utclong_current( ) ) ) FAILED failed REPORTED reported.
" Read current data READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_orders).
" Raise Domain Event LOOP AT lt_orders INTO DATA(ls_order). RAISE ENTITY EVENT zi_order~OrderConfirmed FROM VALUE #( ( %key = ls_order-%key %param = VALUE zs_order_confirmed_event( order_id = ls_order-OrderId customer_id = ls_order-CustomerId confirmed_at = ls_order-ConfirmedAt total_amount = ls_order-TotalAmount currency_code = ls_order-CurrencyCode ) ) ). ENDLOOP.
" Result result = CORRESPONDING #( lt_orders ). ENDMETHOD.ENDCLASS.Event Handler in Another Bounded Context
CLASS zcl_logistics_event_handler DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_rap_event_handler.ENDCLASS.
CLASS zcl_logistics_event_handler IMPLEMENTATION. METHOD if_rap_event_handler~handle_event. CASE iv_event_name. WHEN 'ORDERCONFIRMED'. " Read event data DATA ls_event TYPE zs_order_confirmed_event. ls_event = CORRESPONDING #( is_event_data ).
" Create Shipment in Logistics Context MODIFY ENTITIES OF zi_shipment ENTITY Shipment CREATE FIELDS ( OrderId CustomerId Status ) WITH VALUE #( ( %cid = 'SHIP_1' OrderId = ls_event-order_id CustomerId = ls_event-customer_id Status = 'PENDING' ) ) MAPPED DATA(lt_mapped) FAILED DATA(lt_failed) REPORTED DATA(lt_reported).
IF lt_failed IS INITIAL. COMMIT ENTITIES. ENDIF. ENDCASE. ENDMETHOD.ENDCLASS.Ubiquitous Language
A central DDD concept is the Ubiquitous Language - a shared vocabulary for business and development:
CDS Naming Conventions
" Entity names correspond to business termsdefine root view entity ZI_SalesOrder " Sales Orderdefine view entity ZI_SalesOrderItem " Sales Order Itemdefine view entity ZI_DeliveryNote " Delivery Notedefine view entity ZI_Invoice " Invoicedefine root view entity ZI_Customer " Customer
" Field names are self-explanatory@EndUserText.label: 'Sales Order'define root view entity ZI_SalesOrder{ key order_number as OrderNumber, " Order Number customer_number as CustomerNumber, " Customer Number order_date as OrderDate, " Order Date requested_delivery as RequestedDelivery, " Requested Delivery Date net_value as NetValue, " Net Value tax_amount as TaxAmount, " Tax Amount gross_value as GrossValue, " Gross Value payment_terms as PaymentTerms, " Payment Terms shipping_method as ShippingMethod, " Shipping Method order_status as OrderStatus " Order Status}Domain-Specific Data Types
" Custom data elements for business terms@EndUserText.label: 'Order Number'@AbapCatalog.dataMaintenance: #ALLOWEDdefine type zdd_order_number : abap.numc(10);
@EndUserText.label: 'Customer Number'define type zdd_customer_number : abap.numc(10);
@EndUserText.label: 'Order Status'define type zdd_order_status : abap.char(2);
" Usage in table and CDSdefine table zsales_order { key client : abap.clnt not null; key order_number : zdd_order_number not null; customer_number : zdd_customer_number; order_status : zdd_order_status;}Bounded Context Integration
Anti-Corruption Layer
An Anti-Corruption Layer protects your Bounded Context from foreign data models:
" Interface to external systemINTERFACE zif_external_product_service. METHODS get_product IMPORTING iv_external_id TYPE string RETURNING VALUE(rs_product) TYPE zs_external_product.ENDINTERFACE.
" Anti-Corruption LayerCLASS zcl_product_acl DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. METHODS constructor IMPORTING io_external_service TYPE REF TO zif_external_product_service.
METHODS get_product_for_order IMPORTING iv_product_id TYPE zdd_product_id RETURNING VALUE(rs_product) TYPE zs_order_product RAISING cx_product_not_found.
PRIVATE SECTION. DATA mo_external_service TYPE REF TO zif_external_product_service.
METHODS translate_product IMPORTING is_external TYPE zs_external_product RETURNING VALUE(rs_internal) TYPE zs_order_product.ENDCLASS.
CLASS zcl_product_acl IMPLEMENTATION. METHOD constructor. mo_external_service = io_external_service. ENDMETHOD.
METHOD get_product_for_order. " Fetch external product DATA(ls_external) = mo_external_service->get_product( CONV #( iv_product_id ) ).
IF ls_external IS INITIAL. RAISE EXCEPTION TYPE cx_product_not_found. ENDIF.
" Translate to own domain model rs_product = translate_product( ls_external ). ENDMETHOD.
METHOD translate_product. " Map to own structure rs_internal-product_id = is_external-sku. rs_internal-name = is_external-title. rs_internal-price = is_external-list_price. rs_internal-currency = is_external-currency_iso.
" Map external categories to internal CASE is_external-category_code. WHEN 'ELEC'. rs_internal-product_group = 'ELECTRONICS'. WHEN 'FURN'. rs_internal-product_group = 'FURNITURE'. WHEN OTHERS. rs_internal-product_group = 'OTHER'. ENDCASE. ENDMETHOD.ENDCLASS.Context Map
A Context Map documents the relationships between Bounded Contexts:
┌─────────────────────────────────────────────────────────────────┐│ CONTEXT MAP │├─────────────────────────────────────────────────────────────────┤│ ││ ┌──────────────┐ Events ┌──────────────────┐ ││ │ SALES │ ──────────────────────► │ LOGISTICS │ ││ │ Context │ OrderConfirmed │ Context │ ││ │ │ OrderCancelled │ │ ││ │ [Upstream] │ │ [Downstream] │ ││ └──────────────┘ └──────────────────┘ ││ │ │ ││ │ API Call │ ││ ▼ │ ││ ┌──────────────┐ │ ││ │ PRODUCT │ ◄────────────────────────────────┘ ││ │ Context │ ACL (Read-Only) ││ │ │ ││ │ [Upstream] │ ││ └──────────────┘ ││ │ ││ │ Conformist ││ ▼ ││ ┌──────────────┐ ││ │ EXTERNAL │ ││ │ Catalog │ ││ │ (SAP S/4) │ ││ └──────────────┘ │└─────────────────────────────────────────────────────────────────┘Best Practices for DDD with RAP
1. Set Aggregate Boundaries Correctly
" GOOD: Order is Aggregate Root with Items as Childrendefine root view entity ZI_Order composition [0..*] of ZI_OrderItem as _Items " Part of the Aggregate
" BAD: Aggregate too largedefine root view entity ZI_Order composition [0..*] of ZI_OrderItem as _Items composition [0..*] of ZI_Invoice as _Invoices " Own Aggregate! composition [0..*] of ZI_Shipment as _Shipments " Own Aggregate!2. Consistency Within the Aggregate
" Determination updates derived values in the Aggregatedetermination calculateTotals on modify { field Quantity, UnitPrice; " On item changes}
METHOD calculateTotals. " Sum all items DATA(lv_total) = REDUCE decfloat34( INIT sum = CONV decfloat34( 0 ) FOR item IN lt_items NEXT sum = sum + item-LineTotal ).
" Update Root Entity MODIFY ENTITIES OF zi_order IN LOCAL MODE ENTITY Order UPDATE FIELDS ( TotalAmount ) WITH VALUE #( ( %tky = ls_order-%tky TotalAmount = lv_total ) ).ENDMETHOD.3. Abstract Repositories
" Repository Interface for AggregateINTERFACE zif_order_repository. METHODS find_by_id IMPORTING iv_order_id TYPE zdd_order_id RETURNING VALUE(rs_order) TYPE zs_order_aggregate RAISING cx_not_found.
METHODS find_by_customer IMPORTING iv_customer_id TYPE zdd_customer_id RETURNING VALUE(rt_orders) TYPE ztt_order_aggregates.
METHODS save IMPORTING is_order TYPE zs_order_aggregate RAISING cx_save_failed.ENDINTERFACE.
" RAP-based ImplementationCLASS zcl_order_repository DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES zif_order_repository.ENDCLASS.
CLASS zcl_order_repository IMPLEMENTATION. METHOD zif_order_repository~find_by_id. READ ENTITIES OF zi_order ENTITY Order ALL FIELDS WITH VALUE #( ( OrderId = iv_order_id ) ) RESULT DATA(lt_orders) FAILED DATA(lt_failed).
IF lt_failed IS NOT INITIAL OR lt_orders IS INITIAL. RAISE EXCEPTION TYPE cx_not_found. ENDIF.
" Load items READ ENTITIES OF zi_order ENTITY Order BY \_Items ALL FIELDS WITH VALUE #( ( OrderId = iv_order_id ) ) RESULT DATA(lt_items).
" Assemble Aggregate rs_order-header = CORRESPONDING #( lt_orders[ 1 ] ). rs_order-items = CORRESPONDING #( lt_items ). ENDMETHOD.ENDCLASS.4. Business Methods Instead of Technical
" GOOD: Business Actionaction confirmOrder result [1] $self;action shipOrder parameter ZA_ShipmentDetails result [1] $self;action cancelOrder parameter ZA_CancellationReason result [1] $self;
" BAD: Technical Actionaction setStatusConfirmed result [1] $self;action updateShippingData parameter ZA_ShippingData result [1] $self;Related Topics
- RAP Basics - Foundation for Business Object development
- Design Patterns for RAP - Factory, Strategy, and more
- RAP Business Events - Event-Driven Architecture
- Clean ABAP - Code quality and naming conventions
Conclusion
Domain-Driven Design and RAP complement each other excellently:
- Aggregate = Business Object: The RAP architecture with Root and Child Entities directly corresponds to the Aggregate concept
- Bounded Contexts = Software Components: Package structures define clear boundaries
- Domain Events = RAP Business Events: Loose coupling between contexts
- Repository = EML/RAP Runtime: Persistence is abstracted by the framework
DDD requires more analysis initially but pays off through better maintainability, clearer communication between business and development, and more flexible architecture. RAP provides the technical foundation to elegantly implement these concepts.