ABAP OOP Design Patterns — Part 2: Factory, Observer, and Decorator Patterns in Real SAP Systems
If you’ve been writing ABAP long enough, you’ve probably inherited a codebase where every business rule is buried inside a 3,000-line function module, and adding a new requirement means copy-pasting logic you’re not even sure is correct. That’s not a skills problem — it’s an architecture problem. In Part 1 of this series, we explored how the Strategy Pattern helps you swap business logic cleanly without touching the calling code. Now it’s time to go further. In this second installment, we’re tackling three more battle-tested ABAP OOP design patterns: Factory, Observer, and Decorator. Each one solves a very specific pain point I’ve encountered repeatedly across large SAP S/4HANA implementations — and I’ll show you exactly how to apply them.
Before we dive in, a quick note: these patterns aren’t academic exercises. Every example below is inspired by real implementation challenges on production SAP systems. If you’re also working on improving code quality across the board, it’s worth reading alongside our guides on Clean ABAP best practices and robust exception handling in ABAP.
Why These Three Patterns Matter in SAP Environments
SAP systems are notorious for their complexity — multiple integration touchpoints, constantly changing business rules, and the eternal challenge of extending standard functionality without breaking things. Design patterns give you a shared vocabulary and proven blueprints for solving these recurring structural problems.
Here’s a quick orientation before we get into code:
- Factory Pattern: Controls how objects are created, keeping instantiation logic away from business logic.
- Observer Pattern: Decouples event producers from event consumers — critical in event-driven SAP architectures.
- Decorator Pattern: Adds behavior to objects dynamically without changing the original class — your best friend when extending standard SAP logic.
Let’s get into each one.
Pattern 1: The Factory Pattern — Stop Hardcoding Object Creation
The Problem It Solves
Imagine you have a pricing engine that needs to instantiate different pricing strategies based on customer type: standard, VIP, or wholesale. Without a factory, you’ll see code like this scattered everywhere:
" The anti-pattern: instantiation logic bleeding into business logic
IF lv_customer_type = 'VIP'.
CREATE OBJECT lo_pricing TYPE zcl_vip_pricing.
ELSEIF lv_customer_type = 'WHOLESALE'.
CREATE OBJECT lo_pricing TYPE zcl_wholesale_pricing.
ELSE.
CREATE OBJECT lo_pricing TYPE zcl_standard_pricing.
ENDIF.
Now imagine this block duplicated across 12 different programs. The day you add a new customer type, you’re hunting through the entire codebase. That’s exactly the problem the Factory Pattern eliminates.
Implementing a Simple Factory in ABAP
First, define your interface:
INTERFACE zif_pricing_strategy.
METHODS:
calculate_price
IMPORTING iv_base_price TYPE p DECIMALS 2
iv_quantity TYPE i
RETURNING VALUE(rv_final_price) TYPE p DECIMALS 2.
ENDINTERFACE.
Then implement your concrete classes:
" Standard pricing: no discount
CLASS zcl_standard_pricing DEFINITION PUBLIC FINAL.
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.
ENDCLASS.
" VIP pricing: 15% discount
CLASS zcl_vip_pricing DEFINITION PUBLIC FINAL.
PUBLIC SECTION.
INTERFACES zif_pricing_strategy.
ENDCLASS.
CLASS zcl_vip_pricing IMPLEMENTATION.
METHOD zif_pricing_strategy~calculate_price.
rv_final_price = iv_base_price * iv_quantity * '0.85'.
ENDMETHOD.
ENDCLASS.
Now, the factory class itself — this is where the magic lives:
CLASS zcl_pricing_factory DEFINITION PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION.
CLASS-METHODS:
get_instance
RETURNING VALUE(ro_factory) TYPE REF TO zcl_pricing_factory,
create_strategy
IMPORTING iv_customer_type TYPE char10
RETURNING VALUE(ro_strategy) TYPE REF TO zif_pricing_strategy
RAISING zcx_unknown_customer_type.
PRIVATE SECTION.
CLASS-DATA: go_instance TYPE REF TO zcl_pricing_factory.
ENDCLASS.
CLASS zcl_pricing_factory IMPLEMENTATION.
METHOD get_instance.
" Singleton: only one factory instance needed
IF go_instance IS NOT BOUND.
CREATE OBJECT go_instance.
ENDIF.
ro_factory = go_instance.
ENDMETHOD.
METHOD create_strategy.
CASE iv_customer_type.
WHEN 'STANDARD'.
CREATE OBJECT ro_strategy TYPE zcl_standard_pricing.
WHEN 'VIP'.
CREATE OBJECT ro_strategy TYPE zcl_vip_pricing.
WHEN 'WHOLESALE'.
CREATE OBJECT ro_strategy TYPE zcl_wholesale_pricing.
WHEN OTHERS.
" Always raise a typed exception — see our exception handling guide
RAISE EXCEPTION TYPE zcx_unknown_customer_type
EXPORTING iv_customer_type = iv_customer_type.
ENDCASE.
ENDMETHOD.
ENDCLASS.
Now your business code becomes clean and future-proof:
DATA: lo_factory TYPE REF TO zcl_pricing_factory,
lo_strategy TYPE REF TO zif_pricing_strategy.
lo_factory = zcl_pricing_factory=>get_instance( ).
TRY.
lo_strategy = lo_factory->create_strategy( iv_customer_type = lv_cust_type ).
DATA(lv_price) = lo_strategy->calculate_price(
iv_base_price = '100.00'
iv_quantity = 5
).
CATCH zcx_unknown_customer_type INTO DATA(lx_exc).
" Log and handle gracefully
ENDTRY.
Adding a new customer type now means adding one class and one WHEN clause in the factory. Nothing else changes. That’s the power of encapsulated object creation.
Pattern 2: The Observer Pattern — Event-Driven Logic Without Tight Coupling
The Problem It Solves
Consider a sales order creation process. When an order is created, you might need to: send a notification email, update a reporting table, and trigger a downstream MES workflow. Without the Observer pattern, your order creation class ends up calling all of these directly — a violation of the Single Responsibility Principle and a maintenance nightmare.
The Observer pattern lets you define a subject (the order) and observers (notification, reporting, MES integration) that register themselves and react independently.
ABAP Implementation
" Observer interface — all listeners must implement this
INTERFACE zif_order_observer.
METHODS:
on_order_created
IMPORTING is_order_data TYPE zs_sales_order.
ENDINTERFACE.
" Subject interface — the observable entity
INTERFACE zif_order_subject.
METHODS:
attach IMPORTING io_observer TYPE REF TO zif_order_observer,
detach IMPORTING io_observer TYPE REF TO zif_order_observer,
notify IMPORTING is_order_data TYPE zs_sales_order.
ENDINTERFACE.
" Concrete subject: Sales Order processor
CLASS zcl_sales_order_processor DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES zif_order_subject.
METHODS create_order
IMPORTING is_order_data TYPE zs_sales_order.
PRIVATE SECTION.
DATA: gt_observers TYPE TABLE OF REF TO zif_order_observer.
ENDCLASS.
CLASS zcl_sales_order_processor IMPLEMENTATION.
METHOD zif_order_subject~attach.
APPEND io_observer TO gt_observers.
ENDMETHOD.
METHOD zif_order_subject~detach.
DELETE gt_observers WHERE table_line = io_observer.
ENDMETHOD.
METHOD zif_order_subject~notify.
LOOP AT gt_observers INTO DATA(lo_observer).
lo_observer->on_order_created( is_order_data = is_order_data ).
ENDLOOP.
ENDMETHOD.
METHOD create_order.
" Core order creation logic here...
" (database insert, number range, etc.)
" Notify all registered observers
zif_order_subject~notify( is_order_data = is_order_data ).
ENDMETHOD.
ENDCLASS.
" Concrete Observer 1: Email notification
CLASS zcl_order_email_notifier DEFINITION PUBLIC FINAL.
PUBLIC SECTION.
INTERFACES zif_order_observer.
ENDCLASS.
CLASS zcl_order_email_notifier IMPLEMENTATION.
METHOD zif_order_observer~on_order_created.
" Send confirmation email logic
" cl_bcs or custom email class here
WRITE: / 'Email sent for order:', is_order_data-order_id.
ENDMETHOD.
ENDCLASS.
" Concrete Observer 2: MES trigger
CLASS zcl_order_mes_trigger DEFINITION PUBLIC FINAL.
PUBLIC SECTION.
INTERFACES zif_order_observer.
ENDCLASS.
CLASS zcl_order_mes_trigger IMPLEMENTATION.
METHOD zif_order_observer~on_order_created.
" Trigger MES workflow via REST call or IDoc
" See: SAP MES Integration architecture article
WRITE: / 'MES workflow triggered for order:', is_order_data-order_id.
ENDMETHOD.
ENDCLASS.
Wiring it all together:
DATA: lo_processor TYPE REF TO zcl_sales_order_processor,
lo_email TYPE REF TO zcl_order_email_notifier,
lo_mes TYPE REF TO zcl_order_mes_trigger.
CREATE OBJECT lo_processor.
CREATE OBJECT lo_email.
CREATE OBJECT lo_mes.
" Register observers
lo_processor->attach( lo_email ).
lo_processor->attach( lo_mes ).
" Create order — observers fire automatically
lo_processor->create_order( is_order_data = ls_order ).
Want to add a reporting observer next month? Create the class, register it. The processor never changes. This is exactly the kind of extensibility you need in a living SAP system — and it pairs beautifully with the event-driven approach we discussed in the context of SAP BTP Event Mesh architectures.
Pattern 3: The Decorator Pattern — Extending Behavior Without Inheritance Hell
The Problem It Solves
Here’s a scenario I see regularly: you have a report output class that formats data. Now stakeholders want optional features — logging, caching, and access control — applied in different combinations. Using inheritance, you’d need a class for every combination: LoggingCachingOutputFormatter, CachingOutputFormatter, etc. That explodes fast.
The Decorator pattern wraps objects to add behavior dynamically, without subclassing. It’s composable, testable, and elegant.
ABAP Implementation
" Core interface
INTERFACE zif_data_formatter.
METHODS:
format_data
IMPORTING it_raw_data TYPE ztt_report_data
RETURNING VALUE(rv_output) TYPE string.
ENDINTERFACE.
" Base concrete implementation
CLASS zcl_base_formatter DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES zif_data_formatter.
ENDCLASS.
CLASS zcl_base_formatter IMPLEMENTATION.
METHOD zif_data_formatter~format_data.
" Core formatting logic — converts internal table to string output
LOOP AT it_raw_data INTO DATA(ls_row).
CONCATENATE rv_output ls_row-field1 '|' ls_row-field2 CL_ABAP_CHAR_UTILITIES=>NEWLINE
INTO rv_output.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
" Abstract decorator base — holds a reference to the wrapped component
CLASS zcl_formatter_decorator DEFINITION PUBLIC ABSTRACT.
PUBLIC SECTION.
INTERFACES zif_data_formatter.
METHODS constructor
IMPORTING io_wrapped TYPE REF TO zif_data_formatter.
PROTECTED SECTION.
DATA: mo_wrapped TYPE REF TO zif_data_formatter.
ENDCLASS.
CLASS zcl_formatter_decorator IMPLEMENTATION.
METHOD constructor.
mo_wrapped = io_wrapped.
ENDMETHOD.
METHOD zif_data_formatter~format_data.
" Default: delegate to the wrapped component
rv_output = mo_wrapped->format_data( it_raw_data = it_raw_data ).
ENDMETHOD.
ENDCLASS.
" Logging decorator: wraps any formatter and adds execution logging
CLASS zcl_logging_formatter DEFINITION PUBLIC INHERITING FROM zcl_formatter_decorator FINAL.
PUBLIC SECTION.
METHODS zif_data_formatter~format_data REDEFINITION.
ENDCLASS.
CLASS zcl_logging_formatter IMPLEMENTATION.
METHOD zif_data_formatter~format_data.
DATA(lv_start) = cl_abap_systime=>get_current_time( ).
" Delegate to wrapped formatter
rv_output = mo_wrapped->format_data( it_raw_data = it_raw_data ).
DATA(lv_end) = cl_abap_systime=>get_current_time( ).
" Log execution time to application log
MESSAGE |Formatter executed in { lv_end - lv_start } ms| TYPE 'I'.
ENDMETHOD.
ENDCLASS.
" Caching decorator: returns cached output if data hasn't changed
CLASS zcl_caching_formatter DEFINITION PUBLIC INHERITING FROM zcl_formatter_decorator FINAL.
PUBLIC SECTION.
METHODS zif_data_formatter~format_data REDEFINITION.
PRIVATE SECTION.
DATA: mv_cache TYPE string,
mv_cache_key TYPE string.
ENDCLASS.
CLASS zcl_caching_formatter IMPLEMENTATION.
METHOD zif_data_formatter~format_data.
" Simple hash-based cache key from row count + first key field
DATA(lv_key) = |{ lines( it_raw_data ) }|.
IF lv_key = mv_cache_key AND mv_cache IS NOT INITIAL.
rv_output = mv_cache. " Return cached result
RETURN.
ENDIF.
rv_output = mo_wrapped->format_data( it_raw_data = it_raw_data ).
mv_cache = rv_output.
mv_cache_key = lv_key.
ENDMETHOD.
ENDCLASS.
Now compose them however you need, at runtime:
" Build a formatter stack: Base → Cache → Log
DATA: lo_base TYPE REF TO zif_data_formatter,
lo_cached TYPE REF TO zif_data_formatter,
lo_logged TYPE REF TO zif_data_formatter.
CREATE OBJECT lo_base TYPE zcl_base_formatter.
CREATE OBJECT lo_cached TYPE zcl_caching_formatter EXPORTING io_wrapped = lo_base.
CREATE OBJECT lo_logged TYPE zcl_logging_formatter EXPORTING io_wrapped = lo_cached.
" The caller uses the interface — unaware of what's underneath
DATA(lv_output) = lo_logged->format_data( it_raw_data = lt_data ).
Swap decorators in and out based on configuration. Add an access-control decorator for certain users. None of the underlying classes change. This is composition over inheritance in action — one of the most important principles in the Clean ABAP guidelines.
When to Use Which Pattern — A Decision Guide
| Pattern | Use When | Avoid When |
|---|---|---|
| Factory | Object creation logic is complex or type-dependent | You only ever create one type of object |
| Observer | One event triggers multiple independent reactions | Observers are tightly ordered and dependent on each other |
| Decorator | You need to combine behaviors at runtime without subclassing | Behavior combinations are fixed and few |
Making Patterns Testable with ABAP Unit
One underrated benefit of these patterns is testability. Because each component depends on interfaces, not concrete classes, you can inject test doubles easily. If you’re not yet writing unit tests for your ABAP classes, now is the time — our dedicated guide on ABAP unit testing in SAP S/4HANA walks through exactly how to structure tests for OOP-based code.
For example, testing the Observer pattern is trivial with a mock observer:
CLASS zcl_mock_observer DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES zif_order_observer.
DATA: mv_was_called TYPE abap_bool,
ms_received TYPE zs_sales_order.
ENDCLASS.
CLASS zcl_mock_observer IMPLEMENTATION.
METHOD zif_order_observer~on_order_created.
mv_was_called = abap_true.
ms_received = is_order_data.
ENDMETHOD.
ENDCLASS.
Inject the mock, trigger the action, assert mv_was_called = abap_true. Clean, isolated, fast.
Key Takeaways
- The Factory Pattern centralizes object creation and makes your codebase extensible without scattering
CREATE OBJECTlogic everywhere. - The Observer Pattern decouples event producers from consumers, making it easy to add new reactions to system events without modifying existing code.
- The Decorator Pattern lets you compose behaviors dynamically — far more flexible than deep inheritance hierarchies.
- All three patterns make your code significantly easier to unit test, a non-negotiable requirement in modern SAP development.
- These aren’t theoretical niceties — they solve concrete problems you face in every mid-to-large SAP project.
If you’re just getting started with OOP in ABAP and found this article dense, I’d recommend revisiting the Strategy Pattern from Part 1 of this series first — it lays the foundation that makes these patterns click.
In Part 3, we’ll explore the Command and Template Method patterns — both essential for building undo/redo mechanisms, batch processing pipelines, and workflow engines in SAP. Stay tuned.
What’s your experience with design patterns in ABAP? Have you applied any of these on a real project? I’d love to hear what worked, what didn’t, and what challenges you ran into. Drop a comment below or connect with me — let’s keep the conversation going.


