SOLID Principles in ABAP: Practical Code Examples
ABAP

SOLID Principles in ABAP: Practical Code Examples

If you've spent any time in legacy SAP codebases, you've seen it: a 3,000-line function module that does everything from reading master data to sending emails to writing to custom Z-tables. Nobody dares touch it. Everyone fears it. It's a monument to what happens when SOLID principles are ignored.

The good news? The SOLID principles in ABAP are not just academic theory lifted from Java textbooks. They apply directly to modern ABAP OOP, and adopting them will make your code dramatically easier to maintain, test, and extend. I've been applying these in production SAP systems, and in this post I'll show you exactly how each principle translates to real ABAP code.

What Are the SOLID Principles?

SOLID is an acronym coined by Robert C. Martin (Uncle Bob) for five design principles that, together, guide you toward loosely coupled, highly cohesive object-oriented code:

  • S — Single Responsibility Principle (SRP)
  • O — Open/Closed Principle (OCP)
  • L — Liskov Substitution Principle (LSP)
  • I — Interface Segregation Principle (ISP)
  • D — Dependency Inversion Principle (DIP)

Let's go through each one with ABAP-specific code you can actually use.

S — Single Responsibility Principle in ABAP

A class should have one — and only one — reason to change. In practice, this means each class owns exactly one slice of behavior.

Here's a violating example you'll recognize immediately:

CLASS zcl_invoice_processor DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS:
      read_invoice_data IMPORTING iv_invoice_id TYPE vbeln,
      validate_invoice,
      calculate_tax,
      post_to_fi,
      send_confirmation_email.
ENDCLASS.

This class has at least five reasons to change: data access logic, validation rules, tax calculation, FI posting, and email logic. Any business change touches this single class.

The SRP-compliant design splits responsibilities:

" Data access
CLASS zcl_invoice_reader DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS read_by_id
      IMPORTING iv_invoice_id TYPE vbeln
      RETURNING VALUE(rs_invoice) TYPE zs_invoice.
ENDCLASS.

" Validation
CLASS zcl_invoice_validator DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS validate
      IMPORTING is_invoice TYPE zs_invoice
      RETURNING VALUE(rv_valid) TYPE abap_bool.
ENDCLASS.

" Tax
CLASS zcl_tax_calculator DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS calculate
      IMPORTING is_invoice TYPE zs_invoice
      RETURNING VALUE(rv_tax_amount) TYPE wrbtr.
ENDCLASS.

Now each class has exactly one owner and one reason to change. Validation rules change? Touch only zcl_invoice_validator. Tax logic changes? Only zcl_tax_calculator needs updating.

O — Open/Closed Principle in ABAP

Classes should be open for extension but closed for modification. In ABAP, this is where interfaces and inheritance earn their keep.

A common anti-pattern is a CASE statement that grows every time a new document type is added:

METHOD process_document.
  CASE iv_doc_type.
    WHEN 'INVOICE'.
      " invoice logic
    WHEN 'CREDIT_NOTE'.
      " credit note logic
    WHEN 'DEBIT_NOTE'.
      " debit note logic — added last month
    WHEN 'PROFORMA'.
      " proforma — added this month, touching old code again
  ENDCASE.
ENDMETHOD.

Every new document type forces you to modify existing, tested code. The OCP solution uses polymorphism:

INTERFACE zif_document_processor.
  METHODS process
    IMPORTING is_document TYPE zs_document.
ENDINTERFACE.

CLASS zcl_invoice_doc_processor DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_document_processor.
ENDCLASS.

CLASS zcl_invoice_doc_processor IMPLEMENTATION.
  METHOD zif_document_processor~process.
    " invoice-specific logic only
  ENDMETHOD.
ENDCLASS.

CLASS zcl_credit_note_processor DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_document_processor.
ENDCLASS.

CLASS zcl_credit_note_processor IMPLEMENTATION.
  METHOD zif_document_processor~process.
    " credit note-specific logic only
  ENDMETHOD.
ENDCLASS.

Adding a new document type means creating a new class. You never touch the existing processors. This pairs naturally with the Factory pattern — if you want to see how factories handle object creation cleanly, the Factory and Observer patterns article on this site covers it in depth.

L — Liskov Substitution Principle in ABAP

If class B inherits from class A, you should be able to use B wherever A is expected without breaking anything. Violations typically show up as subclasses that throw unexpected exceptions or weaken preconditions.

CLASS zcl_base_tax_calculator DEFINITION PUBLIC CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS calculate_tax
      IMPORTING iv_amount TYPE wrbtr
      RETURNING VALUE(rv_tax) TYPE wrbtr.
ENDCLASS.

CLASS zcl_base_tax_calculator IMPLEMENTATION.
  METHOD calculate_tax.
    rv_tax = iv_amount * '0.20'.
  ENDMETHOD.
ENDCLASS.

" GOOD: subclass honors the contract
CLASS zcl_reduced_tax_calculator DEFINITION PUBLIC
  INHERITING FROM zcl_base_tax_calculator CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS calculate_tax REDEFINITION.
ENDCLASS.

CLASS zcl_reduced_tax_calculator IMPLEMENTATION.
  METHOD calculate_tax.
    rv_tax = iv_amount * '0.05'. " reduced rate, still returns a valid tax
  ENDMETHOD.
ENDCLASS.

An LSP violation would be a subclass that raises a cx_no_check exception when the parent never did, or that silently returns zero for negative amounts without documenting this behavior. The calling code assumes the contract of the parent — breaking that contract is a bug waiting to happen.

In practice, prefer interfaces over inheritance hierarchies when the Liskov contract is hard to enforce. Composition typically beats deep inheritance trees in ABAP.

I — Interface Segregation Principle in ABAP

Clients should not be forced to depend on interfaces they don't use. Fat interfaces with many methods create unnecessary coupling.

" BAD: fat interface — not every implementor needs all methods
INTERFACE zif_document_service.
  METHODS:
    read   IMPORTING iv_id TYPE vbeln,
    create IMPORTING is_doc TYPE zs_document,
    update IMPORTING is_doc TYPE zs_document,
    delete IMPORTING iv_id TYPE vbeln,
    archive IMPORTING iv_id TYPE vbeln,
    send_to_workflow IMPORTING iv_id TYPE vbeln.
ENDINTERFACE.

A read-only reporting class implementing this interface is forced to provide stubs for create, update, delete, archive, and send_to_workflow — none of which it uses. That's noise, and it's a maintenance trap.

" GOOD: segregated interfaces
INTERFACE zif_doc_reader.
  METHODS read IMPORTING iv_id TYPE vbeln
               RETURNING VALUE(rs_doc) TYPE zs_document.
ENDINTERFACE.

INTERFACE zif_doc_writer.
  METHODS create IMPORTING is_doc TYPE zs_document.
  METHODS update IMPORTING is_doc TYPE zs_document.
  METHODS delete IMPORTING iv_id TYPE vbeln.
ENDINTERFACE.

INTERFACE zif_doc_workflow.
  METHODS send_to_workflow IMPORTING iv_id TYPE vbeln.
ENDINTERFACE.

" A reporting class only implements what it needs
CLASS zcl_invoice_report DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_doc_reader.
ENDCLASS.

ISP pairs well with the Chain of Responsibility pattern, where each handler in the chain implements only the narrow interface it needs, keeping handler classes lean and focused.

D — Dependency Inversion Principle in ABAP

High-level modules should not depend on low-level modules. Both should depend on abstractions. This is the principle that makes unit testing in ABAP genuinely practical.

Here's the common violation — hardcoded dependency on a concrete class:

CLASS zcl_order_service DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PRIVATE SECTION.
    DATA mo_db_reader TYPE REF TO zcl_order_db_reader. " concrete!
ENDCLASS.

CLASS zcl_order_service IMPLEMENTATION.
  METHOD constructor.
    CREATE OBJECT mo_db_reader. " tightly coupled, untestable
  ENDMETHOD.
ENDCLASS.

You cannot test zcl_order_service without hitting the database. The DIP fix uses constructor injection against an interface:

INTERFACE zif_order_reader.
  METHODS read_by_id
    IMPORTING iv_order_id TYPE vbeln
    RETURNING VALUE(rs_order) TYPE zs_order.
ENDINTERFACE.

CLASS zcl_order_service DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING io_reader TYPE REF TO zif_order_reader.
  PRIVATE SECTION.
    DATA mo_reader TYPE REF TO zif_order_reader. " abstraction!
ENDCLASS.

CLASS zcl_order_service IMPLEMENTATION.
  METHOD constructor.
    mo_reader = io_reader.
  ENDMETHOD.
ENDCLASS.

Now in production you inject zcl_order_db_reader (which implements zif_order_reader), and in your unit tests you inject a test double that returns controlled data — no database required. This is exactly the technique described in the test doubles and dependency injection article we have here on the site.

SOLID Principles in ABAP: Putting It All Together

Here's a realistic mini-architecture for an invoice processing feature that applies all five principles:

" Interfaces (abstractions — DIP + ISP)
INTERFACE zif_invoice_reader.
  METHODS read IMPORTING iv_id TYPE vbeln
               RETURNING VALUE(rs_inv) TYPE zs_invoice.
ENDINTERFACE.

INTERFACE zif_invoice_validator.
  METHODS is_valid IMPORTING is_inv TYPE zs_invoice
                  RETURNING VALUE(rv_result) TYPE abap_bool.
ENDINTERFACE.

INTERFACE zif_tax_strategy.
  METHODS calculate IMPORTING iv_amount TYPE wrbtr
                   RETURNING VALUE(rv_tax) TYPE wrbtr.
ENDINTERFACE.

" Concrete implementations (OCP: extend without modifying)
CLASS zcl_standard_tax DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_tax_strategy.
ENDCLASS.
CLASS zcl_standard_tax IMPLEMENTATION.
  METHOD zif_tax_strategy~calculate.
    rv_tax = iv_amount * '0.20'.
  ENDMETHOD.
ENDCLASS.

CLASS zcl_zero_rate_tax DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_tax_strategy.
ENDCLASS.
CLASS zcl_zero_rate_tax IMPLEMENTATION.
  METHOD zif_tax_strategy~calculate.
    rv_tax = 0.
  ENDMETHOD.
ENDCLASS.

" Orchestrator (SRP: only coordinates, delegates to collaborators)
" (DIP: depends only on interfaces)
CLASS zcl_invoice_orchestrator DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING
        io_reader    TYPE REF TO zif_invoice_reader
        io_validator TYPE REF TO zif_invoice_validator
        io_tax       TYPE REF TO zif_tax_strategy.
    METHODS process IMPORTING iv_invoice_id TYPE vbeln.
  PRIVATE SECTION.
    DATA mo_reader    TYPE REF TO zif_invoice_reader.
    DATA mo_validator TYPE REF TO zif_invoice_validator.
    DATA mo_tax       TYPE REF TO zif_tax_strategy.
ENDCLASS.

CLASS zcl_invoice_orchestrator IMPLEMENTATION.
  METHOD constructor.
    mo_reader    = io_reader.
    mo_validator = io_validator.
    mo_tax       = io_tax.
  ENDMETHOD.

  METHOD process.
    DATA(ls_invoice) = mo_reader->read( iv_invoice_id ).
    CHECK mo_validator->is_valid( ls_invoice ) = abap_true.
    DATA(lv_tax) = mo_tax->calculate( ls_invoice-net_amount ).
    " ... continue processing
  ENDMETHOD.
ENDCLASS.

Notice what this buys you: you can swap the tax strategy at runtime without touching the orchestrator (OCP). You can test the orchestrator with mocks (DIP). You can change validation rules independently (SRP). Each interface is narrow (ISP). And every concrete tax class honors the zif_tax_strategy contract (LSP).

This is also a natural foundation for the Strategy pattern — if you want to see how far you can take this in a real SAP system, check out the Strategy, Command and Template Method patterns article.

Common Mistakes to Avoid

  • Over-engineering small scripts: SOLID is for classes that live in production and get touched repeatedly. A one-time data migration report doesn't need a full DIP architecture.
  • Giant interfaces on day one: Start narrow. You can always merge interfaces later if the segregation turns out to be too granular.
  • Inheritance for reuse: Prefer composition. Inheritance chains in ABAP get messy fast, and they often violate LSP in subtle ways.
  • Skipping tests when refactoring: If you're applying DIP to an existing codebase, write tests first. The ABAP unit testing guide shows exactly how to set up your test infrastructure before you start restructuring.

When to Apply SOLID Principles in ABAP

Not every line of ABAP needs to be rewritten around these principles today. Here's a pragmatic priority order:

  1. New development — always design SOLID from the start
  2. Any class you're already modifying — refactor as you go (Boy Scout Rule)
  3. Classes touched by bugs repeatedly — high ROI for SRP refactoring
  4. Code that's hard to unit test — almost always a DIP violation

Also worth reading: the ABAP clean code refactoring guide pairs perfectly with this article if you're working on a legacy codebase migration.

Final Thoughts

The SOLID principles in ABAP aren't a checklist you tick off before code review. They're habits that accumulate over time into codebases that teams actually enjoy working in. Start with SRP and DIP — they give you the biggest immediate returns, especially when you want to write proper unit tests. Once those feel natural, the remaining three principles start to fall into place almost automatically.

The goal isn't perfectly SOLID code everywhere. It's sustainable, readable, testable ABAP that your future self — and your team — will thank you for.