Years ago, during an SAP project, I had to develop a complex pricing engine for the production planning module. The customer's request was clear: dozens of different pricing rules, distinct tax calculations for different countries, and business logic that could change every quarter. I started with procedural ABAP—the result? Hundreds of lines of CASE statements and a growing maintenance nightmare with every new rule introduced. Since that day, OOP design patterns have been a lifesaver for me.
In this article, we will explore two fundamental design patterns—Factory Method and Strategy Pattern—that reveal the true power of Object-Oriented Programming in ABAP, using tangible, real-world examples. We will discuss when, why, and how you should use these patterns in the SAP development ecosystem, complete with ready-to-use ABAP code snippets.
"Design patterns are proven solutions to previously solved problems. Instead of reinventing the wheel, incorporate the legacy of workshop practice into your own."
Why Should You Use OOP Design Patterns in ABAP?
A significant portion of ABAP developers still write code using a procedural approach. This is understandable—SAP's roots run deep in FMs (Function Modules) and Report programs. However, with the transition to S/4HANA, the complexity of business logic and the maintenance burden now make OOP a necessity.
What design patterns bring to the table:
- Flexibility: You can add new behaviors without breaking existing code.
- Testability: Abstracting dependencies makes writing unit tests much easier.
- Readability: Team members can understand your intent directly from the code structure.
- SAP BTP Alignment: It naturally aligns with Clean Core principles.
Factory Method Pattern: Abstract Object Creation
The Problem: The Risks of Directly Using the NEW Operator
Consider this scenario: You are writing different processor classes for various document types (invoice, order, return). A naive approach would look like this:

" BAD APPROACH - Tightly coupling dependencies
CASE lv_doc_type.
WHEN 'INVOICE'.
lo_processor = NEW zcl_invoice_processor( ).
WHEN 'ORDER'.
lo_processor = NEW zcl_order_processor( ).
WHEN 'RETURN'.
lo_processor = NEW zcl_return_processor( ).
ENDCASE.The problem here is that every time a new document type is introduced, you have to search for this CASE statement and modify it. You are violating the Open/Closed Principle.
The Solution: Factory Method
First, let's define an interface:
" Interface definition
INTERFACE zif_doc_processor.
METHODS:
process
IMPORTING
is_document TYPE zs_document
RETURNING
VALUE(rv_result) TYPE string,
validate
IMPORTING
is_document TYPE zs_document
RETURNING
VALUE(rv_is_valid) TYPE abap_bool.
ENDINTERFACE.Next, the concrete implementation for each document type:
" Invoice processor
CLASS zcl_invoice_processor DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES zif_doc_processor.
ENDCLASS.
CLASS zcl_invoice_processor IMPLEMENTATION.
METHOD zif_doc_processor~process.
" Invoice processing logic
rv_result = |Invoice { is_document-doc_number } processed|.
ENDMETHOD.
METHOD zif_doc_processor~validate.
" Invoice validation logic
rv_is_valid = xsdbool( is_document-amount > 0 ).
ENDMETHOD.
ENDCLASS.And now, the Factory class:
" Factory class
CLASS zcl_doc_processor_factory DEFINITION PUBLIC FINAL.
PUBLIC SECTION.
CLASS-METHODS:
create
IMPORTING
iv_doc_type TYPE string
RETURNING
VALUE(ro_processor) TYPE REF TO zif_doc_processor
RAISING
zcx_unknown_doc_type.
PRIVATE SECTION.
" Processor registry - new types are added here
CLASS-DATA:
gt_registry TYPE TABLE OF REF TO zif_doc_processor.
ENDCLASS.
CLASS zcl_doc_processor_factory IMPLEMENTATION.
METHOD create.
CASE iv_doc_type.
WHEN 'INVOICE'.
ro_processor = NEW zcl_invoice_processor( ).
WHEN 'ORDER'.
ro_processor = NEW zcl_order_processor( ).
WHEN 'RETURN'.
ro_processor = NEW zcl_return_processor( ).
WHEN OTHERS.
RAISE EXCEPTION TYPE zcx_unknown_doc_type
EXPORTING
mv_doc_type = iv_doc_type.
ENDCASE.
ENDMETHOD.
ENDCLASS.The usage is now incredibly clean:
" Usage - Client code
TRY.
DATA(lo_processor) = zcl_doc_processor_factory=>create(
iv_doc_type = ls_header-doc_type
).
IF lo_processor->validate( is_document = ls_document ).
DATA(lv_result) = lo_processor->process( is_document = ls_document ).
WRITE: / lv_result.
ENDIF.
CATCH zcx_unknown_doc_type INTO DATA(lx_error).
" Error handling - unknown document type
MESSAGE lx_error->get_text( ) TYPE 'E'.
ENDTRY.Now, whenever you need to add a new document type, you just write a new class and add a single line to the factory. The client code remains completely untouched.
Strategy Pattern: Encapsulate Changing Behavior
The Problem: Business Logic Changes, Code Shouldn't
A scenario I frequently encounter in SAP projects: Tax calculation logic varies based on the country, customer type, or product category. Writing IF/CASE statements for every combination is not only dangerous but makes the code impossible to maintain.
The Strategy Pattern allows you to define a family of algorithms, encapsulate each one in a separate class, and make them interchangeable.
Real-World: Pricing Strategies
" Strategy interface
INTERFACE zif_pricing_strategy.
METHODS:
calculate_price
IMPORTING
iv_base_price TYPE p DECIMALS 2
iv_quantity TYPE i
is_customer TYPE zs_customer
RETURNING
VALUE(rv_final_price) TYPE p DECIMALS 2.
get_strategy_name
RETURNING
VALUE(rv_name) TYPE string.
ENDINTERFACE.Concrete strategies:
" Standard pricing
CLASS zcl_standard_pricing DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES zif_pricing_strategy.
ENDCLASS.
CLASS zcl_standard_pricing IMPLEMENTATION.
METHOD zif_pricing_strategy~calculate_price.
rv_final_price = iv_base_price * iv_quantity.
ENDMETHOD.
METHOD zif_pricing_strategy~get_strategy_name.
rv_name = 'Standard Pricing'.
ENDMETHOD.
ENDCLASS.
" VIP customer discounted pricing
CLASS zcl_vip_pricing DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES zif_pricing_strategy.
PRIVATE SECTION.
CONSTANTS: c_discount_rate TYPE p DECIMALS 2 VALUE '0.15'. " 15% discount
ENDCLASS.
CLASS zcl_vip_pricing IMPLEMENTATION.
METHOD zif_pricing_strategy~calculate_price.
DATA(lv_gross) = iv_base_price * iv_quantity.
rv_final_price = lv_gross * ( 1 - c_discount_rate ).
ENDMETHOD.
METHOD zif_pricing_strategy~get_strategy_name.
rv_name = 'VIP Customer Pricing (15% Discount)'.
ENDMETHOD.
ENDCLASS.
" Bulk purchase pricing
CLASS zcl_bulk_pricing DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES zif_pricing_strategy.
PRIVATE SECTION.
CONSTANTS:
c_bulk_threshold TYPE i VALUE 100,
c_bulk_discount TYPE p DECIMALS 2 VALUE '0.20'. " 20% discount
ENDCLASS.
CLASS zcl_bulk_pricing IMPLEMENTATION.
METHOD zif_pricing_strategy~calculate_price.
DATA(lv_gross) = iv_base_price * iv_quantity.
IF iv_quantity >= c_bulk_threshold.
rv_final_price = lv_gross * ( 1 - c_bulk_discount ).
ELSE.
rv_final_price = lv_gross.
ENDIF.
ENDMETHOD.
METHOD zif_pricing_strategy~get_strategy_name.
rv_name = |Bulk Pricing (>{ c_bulk_threshold } units: 20% off)|.
ENDMETHOD.
ENDCLASS.The Context class—the engine that utilizes the strategy:
" Context class
CLASS zcl_price_calculator DEFINITION PUBLIC.
PUBLIC SECTION.
METHODS:
constructor
IMPORTING
io_strategy TYPE REF TO zif_pricing_strategy,
set_strategy
IMPORTING
io_strategy TYPE REF TO zif_pricing_strategy,
calculate
IMPORTING
iv_base_price TYPE p DECIMALS 2
iv_quantity TYPE i
is_customer TYPE zs_customer
RETURNING
VALUE(rv_price) TYPE p DECIMALS 2.
PRIVATE SECTION.
DATA: mo_strategy TYPE REF TO zif_pricing_strategy.
ENDCLASS.
CLASS zcl_price_calculator IMPLEMENTATION.
METHOD constructor.
mo_strategy = io_strategy.
ENDMETHOD.
METHOD set_strategy.
" Strategy can be swapped at runtime!
mo_strategy = io_strategy.
ENDMETHOD.
METHOD calculate.
rv_price = mo_strategy->calculate_price(
iv_base_price = iv_base_price
iv_quantity = iv_quantity
is_customer = is_customer
).
ENDMETHOD.
ENDCLASS.Usage—selecting the strategy at runtime:
" Determine strategy based on customer type
DATA(lo_strategy) = COND #(
WHEN ls_customer-type = 'VIP' THEN NEW zcl_vip_pricing( )
WHEN ls_order-quantity >= 100 THEN NEW zcl_bulk_pricing( )
ELSE NEW zcl_standard_pricing( )
).
" Create Calculator
DATA(lo_calculator) = NEW zcl_price_calculator(
io_strategy = lo_strategy
).
" Calculate price
DATA(lv_price) = lo_calculator->calculate(
iv_base_price = ls_product-price
iv_quantity = ls_order-quantity
is_customer = ls_customer
).
WRITE: / |Final Price: { lv_price CURRENCY 'USD' }|.Using the Two Patterns Together: A Real-World Scenario
The most powerful approach is combining these two patterns. The Factory creates the correct strategy; the Strategy encapsulates the behavior.
" Pricing Strategy Factory
CLASS zcl_pricing_factory DEFINITION PUBLIC FINAL.
PUBLIC SECTION.
CLASS-METHODS:
create_strategy
IMPORTING
is_customer TYPE zs_customer
iv_quantity TYPE i
RETURNING
VALUE(ro_strategy) TYPE REF TO zif_pricing_strategy.
ENDCLASS.
CLASS zcl_pricing_factory IMPLEMENTATION.
METHOD create_strategy.
" Automatic strategy selection based on business rules
IF is_customer-type = 'VIP' AND is_customer-years_active >= 5.
" Loyal VIP customers get the best price
ro_strategy = NEW zcl_vip_pricing( ).
ELSEIF iv_quantity >= 100.
ro_strategy = NEW zcl_bulk_pricing( ).
ELSE.
ro_strategy = NEW zcl_standard_pricing( ).
ENDIF.
ENDMETHOD.
ENDCLASS.
" Now the calling code is incredibly straightforward:
DATA(lo_strategy) = zcl_pricing_factory=>create_strategy(
is_customer = ls_customer
iv_quantity = ls_order-quantity
).
DATA(lo_calc) = NEW zcl_price_calculator( io_strategy = lo_strategy ).
DATA(lv_final) = lo_calc->calculate(
iv_base_price = ls_product-price
iv_quantity = ls_order-quantity
is_customer = ls_customer
).Writing Unit Tests: The Testability of Design Patterns
One of the greatest benefits of this approach is testability. Each strategy can be tested completely independently:
" ABAP Unit Test
CLASS zcl_pricing_test DEFINITION FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT.
PRIVATE SECTION.
METHODS:
test_vip_discount FOR TESTING,
test_bulk_threshold FOR TESTING,
test_standard_pricing FOR TESTING.
ENDCLASS.
CLASS zcl_pricing_test IMPLEMENTATION.
METHOD test_vip_discount.
DATA(lo_strategy) = NEW zcl_vip_pricing( ).
DATA(ls_customer) = VALUE zs_customer( type = 'VIP' ).
DATA(lv_price) = lo_strategy->calculate_price(
iv_base_price = '100'
iv_quantity = 1
is_customer = ls_customer
).
" Should be 85 after VIP discount (15% off)
cl_abap_unit_assert=>assert_equals(
act = lv_price
exp = '85'
msg = 'VIP discount calculation failed'
).
ENDMETHOD.
METHOD test_bulk_threshold.
DATA(lo_strategy) = NEW zcl_bulk_pricing( ).
DATA(ls_customer) = VALUE zs_customer( ).
" 100 units - exactly at the bulk threshold
DATA(lv_price) = lo_strategy->calculate_price(
iv_base_price = '10'
iv_quantity = 100
is_customer = ls_customer
).
" 1000 * 0.80 = 800
cl_abap_unit_assert=>assert_equals(
act = lv_price
exp = '800'
msg = 'Bulk pricing threshold test failed'
).
ENDMETHOD.
METHOD test_standard_pricing.
DATA(lo_strategy) = NEW zcl_standard_pricing( ).
DATA(ls_customer) = VALUE zs_customer( ).
DATA(lv_price) = lo_strategy->calculate_price(
iv_base_price = '50'
iv_quantity = 3
is_customer = ls_customer
).
cl_abap_unit_assert=>assert_equals(
act = lv_price
exp = '150'
msg = 'Standard pricing calculation failed'
).
ENDMETHOD.
ENDCLASS.Evaluation from an S/4HANA Clean Core Perspective
SAP's Clean Core strategy dictates that customizations should not touch standard SAP code. These design patterns naturally support this principle:
- BAdI Integration: The Factory pattern is ideal for selecting BAdI implementations.
- SAP BTP Alignment: It becomes much easier to move abstracted business logic to the cloud with side-by-side extensibility.
- Upgrade Safety: Because you aren't modifying standard SAP objects, system upgrades remain risk-free.
Common Mistakes and How to Avoid Them
The most frequent mistakes I see when implementing these patterns:
- Over-engineering: Not everything requires a factory. If there is only one implementation and it is not expected to change, keep it simple.
- Anemic Strategy: If your Strategy class only contains a getter/setter, you've likely implemented the wrong pattern.
- Unnecessary Singletons: Avoid turning your Factory classes into Singletons—a factory that holds state is extremely difficult to test.
- Skipping Exception Management: A Factory should always throw a meaningful exception for an unknown type.
Conclusion: The Architect's Perspective
In ABAP, the Factory Method and Strategy Pattern are two of the most effective ways to make business logic manageable in complex SAP projects. With years of experience under my belt, I can confidently say: learning these patterns might take you a week, but the return on investment lasts for years.
To get started, I recommend the following steps:
- Identify the massive
CASEstatements in your current project. - Move them behind an interface.
- Write a concrete class for each branch.
- Centralize the selection logic using a Factory.
- Write your ABAP Unit tests.
Once you internalize this approach, adapting other design patterns—such as Observer, Decorator, or Command—to ABAP will feel much more natural. I will be covering those patterns in an upcoming article.
The code examples in this article have been tested on SAP NetWeaver 7.50 and above, as well as S/4HANA environments. I highly recommend validating them in your own development system prior to production use.