DEV Community

Oktay Ates
Oktay Ates

Posted on • Originally published at aixsap.com

ABAP OOP Design Patterns — Part 2: Factory, Observer, and Decorator Patterns in Real SAP Systems

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.

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

" 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.

Enter fullscreen mode Exit fullscreen mode

" 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.

Enter fullscreen mode Exit fullscreen mode

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 ).

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

" 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.

Enter fullscreen mode Exit fullscreen mode

" 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.

Enter fullscreen mode Exit fullscreen mode

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 ).

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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 OBJECT logic 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.

Top comments (0)