Let me be direct with you: most ABAP systems I've seen in the wild don't use a dependency injection framework, and they probably never will. That's not an excuse to skip dependency injection entirely — it's an invitation to implement it pragmatically, using patterns that fit how ABAP actually works.
In this article I'll walk you through three ABAP dependency injection patterns that work without any framework overhead. No external libraries, no complex bootstrapping — just clean object-oriented ABAP that makes your code testable and maintainable.
Before we dive in, if you haven't read the article on SOLID principles with practical ABAP code examples, I'd suggest starting there. Dependency inversion — the D in SOLID — is the theoretical foundation for everything we'll discuss here.
Why ABAP Dependency Injection Matters (And Why People Skip It)
The typical ABAP class creates its own dependencies internally. A document processor instantiates its own database reader. A pricing engine hard-codes which tax calculator it calls. A notification service directly invokes the mail API.
This feels natural when you're writing code. It feels painful when you're writing tests — or inheriting someone else's code six months later.
Hard-coded dependencies mean:
- You can't unit test without hitting the database
- You can't swap implementations without editing the class
- You can't trace what a class actually needs just by reading its signature
The fix is dependency injection: instead of a class creating its dependencies, those dependencies are provided from the outside. This is not complicated theory. It's a naming convention for a very practical idea.
And you don't need a framework to do it.
Pattern 1: Constructor Injection (The One You Should Default To)
Constructor injection is the cleanest form. The class declares all its dependencies as constructor parameters. Whoever creates the object is responsible for supplying the implementations.
Here's a concrete example. Say you have an invoice processor that needs to read data and send notifications:
INTERFACE zif_invoice_reader.
METHODS read_invoice
IMPORTING iv_invoice_id TYPE vbeln
RETURNING VALUE(rs_data) TYPE zs_invoice_data.
ENDINTERFACE.
INTERFACE zif_notifier.
METHODS send
IMPORTING iv_message TYPE string.
ENDINTERFACE.
CLASS zcl_invoice_processor DEFINITION PUBLIC FINAL.
PUBLIC SECTION.
METHODS constructor
IMPORTING
io_reader TYPE REF TO zif_invoice_reader
io_notifier TYPE REF TO zif_notifier.
METHODS process
IMPORTING iv_invoice_id TYPE vbeln.
PRIVATE SECTION.
DATA mo_reader TYPE REF TO zif_invoice_reader.
DATA mo_notifier TYPE REF TO zif_notifier.
ENDCLASS.
CLASS zcl_invoice_processor IMPLEMENTATION.
METHOD constructor.
mo_reader = io_reader.
mo_notifier = io_notifier.
ENDMETHOD.
METHOD process.
DATA(ls_invoice) = mo_reader->read_invoice( iv_invoice_id ).
" ... business logic ...
mo_notifier->send( |Invoice { iv_invoice_id } processed| ).
ENDMETHOD.
ENDCLASS.Notice that zcl_invoice_processor knows nothing about where data comes from or how notifications are sent. In production you inject the real implementations. In tests you inject test doubles.
The production wiring might look like this in a factory or program:
DATA(lo_processor) = NEW zcl_invoice_processor(
io_reader = NEW zcl_db_invoice_reader( )
io_notifier = NEW zcl_email_notifier( )
).And the test wiring:
DATA(lo_processor) = NEW zcl_invoice_processor(
io_reader = NEW zcl_mock_invoice_reader( )
io_notifier = NEW zcl_spy_notifier( )
).Same class, different behaviour, no modifications. That's the payoff.
When to use it: Constructor injection is your default. If a class can't function without a dependency, that dependency belongs in the constructor. It makes the requirement explicit and forces callers to think about what they're providing.
Pattern 2: Setter Injection (Optional Dependencies and Late Binding)
Sometimes a dependency is optional, or you need to swap it after construction — during testing or in specific execution paths. Setter injection handles this case.
CLASS zcl_report_builder DEFINITION PUBLIC.
PUBLIC SECTION.
METHODS constructor.
METHODS set_formatter
IMPORTING io_formatter TYPE REF TO zif_output_formatter.
METHODS build
IMPORTING it_data TYPE ztt_report_data.
PRIVATE SECTION.
DATA mo_formatter TYPE REF TO zif_output_formatter.
ENDCLASS.
CLASS zcl_report_builder IMPLEMENTATION.
METHOD constructor.
" Default implementation — works out of the box
mo_formatter = NEW zcl_default_formatter( ).
ENDMETHOD.
METHOD set_formatter.
mo_formatter = io_formatter.
ENDMETHOD.
METHOD build.
" Uses whatever formatter is currently set
mo_formatter->format( it_data ).
ENDMETHOD.
ENDCLASS.The class works standalone with its default formatter. But you can override it — either in production when you need a CSV formatter instead of HTML, or in tests when you need a mock.
" Production: override with specific formatter
DATA(lo_builder) = NEW zcl_report_builder( ).
lo_builder->set_formatter( NEW zcl_csv_formatter( ) ).
" Test: inject spy
DATA(lo_builder) = NEW zcl_report_builder( ).
lo_builder->set_formatter( lo_spy_formatter ).When to use it: Setter injection suits optional dependencies or cases where a sensible default exists but you want to allow overriding. Be careful not to overuse it — if a class genuinely can't work without something, make it a constructor parameter instead.
Pattern 3: Factory Method Injection (Injecting Factories, Not Instances)
Here's a pattern that many ABAP developers overlook: sometimes you don't want to inject an object instance — you want to inject the ability to create objects on demand. This is especially relevant when you need multiple instances created at different points during execution, or when the factory itself carries configuration logic.
Define a factory interface:
INTERFACE zif_connection_factory.
METHODS create_connection
RETURNING VALUE(ro_connection) TYPE REF TO zif_db_connection.
ENDINTERFACE.Inject the factory into the consumer:
CLASS zcl_batch_processor DEFINITION PUBLIC FINAL.
PUBLIC SECTION.
METHODS constructor
IMPORTING io_conn_factory TYPE REF TO zif_connection_factory.
METHODS run
IMPORTING it_items TYPE ztt_items.
PRIVATE SECTION.
DATA mo_conn_factory TYPE REF TO zif_connection_factory.
ENDCLASS.
CLASS zcl_batch_processor IMPLEMENTATION.
METHOD constructor.
mo_conn_factory = io_conn_factory.
ENDMETHOD.
METHOD run.
LOOP AT it_items INTO DATA(ls_item).
" Create a fresh connection per item if needed
DATA(lo_conn) = mo_conn_factory->create_connection( ).
lo_conn->execute( ls_item ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.In tests, you inject a factory that always returns the same mock connection — giving you full control over what happens inside the loop without any real database calls.
CLASS zcl_mock_conn_factory DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES zif_connection_factory.
DATA mo_mock_connection TYPE REF TO zcl_mock_db_connection.
METHODS constructor.
ENDCLASS.
CLASS zcl_mock_conn_factory IMPLEMENTATION.
METHOD constructor.
mo_mock_connection = NEW zcl_mock_db_connection( ).
ENDMETHOD.
METHOD zif_connection_factory~create_connection.
ro_connection = mo_mock_connection.
ENDMETHOD.
ENDCLASS.When to use it: Factory injection is your go-to when a class needs to create multiple instances of a dependency during its lifecycle, or when the creation logic itself needs to be configurable. It's also extremely useful when you want to test how many times a dependency was created, or verify creation parameters.
Composing the Three Patterns Together
Real applications don't pick just one pattern — they use all three in context. Here's a rough decision guide:
- Constructor injection: The dependency is required. The class cannot function without it. Use this by default.
- Setter injection: The dependency is optional, or a sensible default exists but you need the flexibility to override it.
- Factory injection: You need multiple instances created at runtime, or the creation logic itself must be swappable.
In practice, a well-structured class might combine constructor injection for required dependencies with setter injection for optional collaborators:
CLASS zcl_order_service DEFINITION PUBLIC.
PUBLIC SECTION.
METHODS constructor
IMPORTING
io_repo TYPE REF TO zif_order_repository " required
io_factory TYPE REF TO zif_event_factory. " required
METHODS set_logger
IMPORTING io_logger TYPE REF TO zif_logger. " optional
PRIVATE SECTION.
DATA mo_repo TYPE REF TO zif_order_repository.
DATA mo_factory TYPE REF TO zif_event_factory.
DATA mo_logger TYPE REF TO zif_logger.
ENDCLASS.This approach keeps the API honest: required dependencies are impossible to forget, optional ones can be layered on when needed.
The Composition Root: Where Everything Gets Wired
One detail that trips people up: where do you actually do the wiring? The answer is the composition root — a single place in your application where all objects are constructed and connected.
In ABAP, this is typically your program's start-of-selection block, a main method, or a dedicated setup class. The key discipline is: wiring happens in one place, not scattered throughout the codebase.
" Composition root in a report
START-OF-SELECTION.
" Build the dependency graph
DATA(lo_repo) = NEW zcl_order_db_repository( ).
DATA(lo_factory) = NEW zcl_domain_event_factory( ).
DATA(lo_logger) = NEW zcl_application_logger( ).
DATA(lo_service) = NEW zcl_order_service(
io_repo = lo_repo
io_factory = lo_factory
).
lo_service->set_logger( lo_logger ).
" Now run the application
lo_service->process_pending_orders( ).This is the poor man's IoC container — explicit, readable, debuggable. You know exactly what depends on what, and there's no magic.
Connecting This to Testing
The reason dependency injection matters so much in ABAP is unit testing. If you're building test doubles and mocks, you need injection points to plug them in. The patterns above give you exactly those injection points.
For a deeper look at how this works in practice with ABAP Unit, check out the article on test doubles, mocking frameworks and dependency injection — it covers the test-side mechanics in detail.
And if you're working with design patterns that naturally benefit from these injection techniques, the article on Strategy, Command, and Template Method patterns shows how swappable strategies pair perfectly with constructor injection.
Common Mistakes to Avoid
Injecting concrete classes instead of interfaces. If you inject zcl_email_notifier instead of zif_notifier, you've solved nothing — the caller is still coupled to the implementation. Always inject against interfaces.
Creating dependencies inside methods. Injecting in the constructor and then calling NEW inside a method defeats the purpose. If a method creates its own dependencies, those are hidden dependencies that you can't substitute in tests.
Scattering the composition root. If every class is responsible for wiring its own subgraph, you end up with messy, overlapping construction logic. Keep wiring centralised.
Over-injecting. Not everything needs to be injected. Simple value objects, utility classes with no external side effects — these don't need injection. Apply DI where it reduces coupling to external systems, I/O, or swappable business logic.
What About Performance?
A question I hear regularly: does all this object creation have a performance impact? The honest answer is: negligibly, in almost all real cases. ABAP object instantiation is fast. The cost of a few extra NEW calls at program startup is orders of magnitude smaller than any database read. Don't let performance anxiety push you back toward untestable code.
Final Thoughts
You don't need a framework to do dependency injection in ABAP. Constructor injection, setter injection, and factory injection — used with discipline — give you everything you need to write modular, testable, maintainable code.
The investment pays off quickly. Classes that receive their dependencies from outside are easier to test, easier to reuse, and much easier to modify without cascading side effects. Start with constructor injection on your next class and see how naturally it shapes the design.
For a broader view of how these patterns fit into clean ABAP architecture, the article on refactoring legacy SAP code to modern standards is worth reading alongside this one. Dependency injection is one of the most powerful tools in that refactoring toolkit.