ABAP Clean Code in Practice: Refactoring Legacy SAP Code to Modern Standards — A Senior Architect’s Guide
If you’ve spent any meaningful time working on real SAP systems, you already know the feeling. You open a legacy ABAP program—something written a decade ago, maybe more—and you’re immediately confronted with 3,000-line function modules, cryptic two-letter variable names, nested SELECT loops that would make a database administrator weep, and comment blocks that haven’t been updated since the original developer left the company. This is the reality of ABAP clean code refactoring in production SAP environments, and navigating it effectively is one of the most underrated skills a senior architect can possess.
This guide isn’t about abstract principles. It’s about what actually works when you need to modernize legacy ABAP code without breaking production systems—drawing on patterns I’ve applied across multiple S/4HANA migrations and refactoring projects over the years.
Related reading: If you’re just starting your clean code journey in ABAP, the foundational rules are covered in ABAP Clean Code: 10 Golden Rules for Readable and Maintainable SAP Code. This article builds directly on those fundamentals.
Why Legacy ABAP Refactoring Is Different from Other Languages
Before we dive into techniques, let’s acknowledge something important: ABAP refactoring carries unique risks that you simply don’t face in Java or Python projects. Business-critical processes—payroll runs, goods movements, financial postings—often depend on code that nobody fully understands anymore. The original authors are gone. The documentation is missing or wrong. And the tests? What tests?
This is why a structured, incremental approach matters so much. You don’t refactor a flight-critical system by rewriting it from scratch. You refactor it the way a surgeon operates—carefully, precisely, with a clear understanding of what you’re touching and why.
The good news is that modern ABAP—particularly in S/4HANA environments—gives you powerful tools to make this process safer and more systematic. OOP constructs, ABAP Unit, CDS Views, and the Clean ABAP style guide from SAP all provide a clear direction of travel.
Step 1: Understand Before You Touch — Code Archaeology
The single most dangerous thing you can do in a legacy refactoring project is start making changes before you understand what the code actually does. I’ve seen this pattern destroy projects: an eager developer starts “cleaning up” a function module, removes what looks like dead code, and two weeks later discovers it was executing a critical side effect that only triggers in a specific edge case.
Your first step is structured analysis. Before writing a single line of new code, invest time in:
- Usage analysis: Use SE80, SCI (Code Inspector), and where-used lists to understand call hierarchies
- Runtime traces: SAT (ABAP Trace) on representative business transactions to see what actually executes
- Static analysis: Run SCI with the Clean ABAP check variant to identify the worst offenders
- Business process mapping: Talk to the functional consultants. Understand the business context before touching the technical layer.
Document your findings. Even rough diagrams of the call flow are invaluable when you’re deep in a refactoring session three weeks later.
Step 2: Write Characterization Tests First
Here’s the architect’s dilemma with legacy code: you can’t write proper unit tests without refactoring the code, but you can’t safely refactor without tests. The solution is characterization tests—tests that don’t verify correct behavior, but capture existing behavior, whatever it happens to be.
In ABAP, you can use local test classes to create these safety nets:
"! Characterization test - captures CURRENT behavior of ZLegacyPriceCalc
"! Do NOT assume this behavior is CORRECT - verify with business before refactoring
CLASS ltc_price_calc_characterization DEFINITION FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT.
PRIVATE SECTION.
METHODS: test_standard_price_calculation FOR TESTING,
test_discount_threshold_behavior FOR TESTING,
test_zero_quantity_handling FOR TESTING.
ENDCLASS.
CLASS ltc_price_calc_characterization IMPLEMENTATION.
METHOD test_standard_price_calculation.
" Arrange - use known input values from production data analysis
DATA(lv_material) = CONV matnr( 'MAT-001' ).
DATA(lv_quantity) = CONV menge( 10 ).
DATA(lv_customer) = CONV kunnr( '0000001234' ).
DATA(lv_result_price) = CONV brtwr( 0 ).
DATA(lv_return_code) = CONV sy-subrc( 0 ).
" Act - call the legacy function module directly
CALL FUNCTION 'Z_LEGACY_PRICE_CALCULATE'
EXPORTING
iv_material = lv_material
iv_quantity = lv_quantity
iv_customer = lv_customer
IMPORTING
ev_price = lv_result_price
EXCEPTIONS
not_found = 1
OTHERS = 2.
lv_return_code = sy-subrc.
" Assert - capture current behavior (not necessarily correct behavior)
" Price for 10 units of MAT-001 for customer 1234 was observed as 1500.00
cl_abap_unit_assert=>assert_equals(
act = lv_result_price
exp = CONV brtwr( '1500.00' )
msg = 'Standard price calculation behavior changed - check before proceeding'
).
cl_abap_unit_assert=>assert_equals(
act = lv_return_code
exp = 0
msg = 'Expected sy-subrc 0 for valid inputs'
).
ENDMETHOD.
METHOD test_zero_quantity_handling.
" Document edge case behavior - even if the behavior seems wrong
DATA(lv_result_price) = CONV brtwr( 0 ).
CALL FUNCTION 'Z_LEGACY_PRICE_CALCULATE'
EXPORTING
iv_material = 'MAT-001'
iv_quantity = 0 " Zero quantity - what does legacy code do?
iv_customer = '0000001234'
IMPORTING
ev_price = lv_result_price
EXCEPTIONS
OTHERS = 1.
" Legacy code returns 0 price for zero quantity (not an exception)
" Business confirmed this is INCORRECT behavior to be fixed during refactor
cl_abap_unit_assert=>assert_equals(
act = sy-subrc
exp = 0
msg = 'Legacy returns sy-subrc 0 for zero qty - THIS IS A BUG to fix'
).
ENDMETHOD.
METHOD test_discount_threshold_behavior.
" Quantity > 100 triggers a 15% discount - observed in production traces
" Verify this threshold is stable before refactoring discount logic
DATA(lv_high_qty_price) = CONV brtwr( 0 ).
DATA(lv_standard_qty_price) = CONV brtwr( 0 ).
CALL FUNCTION 'Z_LEGACY_PRICE_CALCULATE'
EXPORTING iv_material = 'MAT-001' iv_quantity = 101 iv_customer = '0000001234'
IMPORTING ev_price = lv_high_qty_price
EXCEPTIONS OTHERS = 1.
CALL FUNCTION 'Z_LEGACY_PRICE_CALCULATE'
EXPORTING iv_material = 'MAT-001' iv_quantity = 10 iv_customer = '0000001234'
IMPORTING ev_price = lv_standard_qty_price
EXCEPTIONS OTHERS = 1.
" High quantity price should be approximately 85% of prorated standard price
DATA(lv_ratio) = lv_high_qty_price / ( lv_standard_qty_price * 101 / 10 ).
cl_abap_unit_assert=>assert_true(
act = xsdbool( lv_ratio >= '0.849' AND lv_ratio <= '0.851' )
msg = |Discount ratio { lv_ratio } differs from expected 0.85 - discount logic may have changed|
).
ENDMETHOD.
ENDCLASS.
These tests are your safety harness. Run them before every change. If one fails, you know you’ve altered behavior—intentionally or not.
Step 3: The Strangler Fig Pattern — Incremental Replacement
One of the most powerful architectural patterns for legacy refactoring is the Strangler Fig Pattern—named after a tree that gradually grows around and replaces another tree. In ABAP terms, this means you don’t rewrite the legacy function module; you build a new class-based implementation alongside it and gradually route traffic to the new code.
Here’s what this looks like in practice. Start by defining an interface that represents the contract the legacy code implicitly fulfills:
"! Price calculation interface - defines the contract both legacy and new implementations must fulfill
INTERFACE zif_price_calculator
PUBLIC.
TYPES:
BEGIN OF ty_price_result,
net_price TYPE brtwr,
gross_price TYPE brtwr,
discount_pct TYPE p DECIMALS 2,
currency TYPE waers,
END OF ty_price_result.
METHODS:
"! Calculate price for given material, quantity, and customer
"! @parameter iv_material | Material number
"! @parameter iv_quantity | Requested quantity
"! @parameter iv_customer | Customer number for pricing conditions
"! @parameter rs_result | Calculated price result
"! @raising zcx_price_calc_error | Raised on invalid inputs or missing master data
calculate
IMPORTING
iv_material TYPE matnr
iv_quantity TYPE menge
iv_customer TYPE kunnr
RETURNING
VALUE(rs_result) TYPE ty_price_result
RAISING
zcx_price_calc_error.
ENDINTERFACE.
Now create a Legacy Wrapper that adapts the old function module to the new interface. This is critical—it lets you immediately use the interface pattern everywhere without rewriting anything:
"! Legacy wrapper - adapts old function module to new interface
"! Use this as a drop-in during initial refactoring phase
CLASS zcl_legacy_price_calc_wrapper DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_price_calculator.
ENDCLASS.
CLASS zcl_legacy_price_calc_wrapper IMPLEMENTATION.
METHOD zif_price_calculator~calculate.
" Validate inputs that legacy code doesn't check (known bug)
IF iv_quantity <= 0.
RAISE EXCEPTION TYPE zcx_price_calc_error
EXPORTING
textid = zcx_price_calc_error=>invalid_quantity
quantity = iv_quantity.
ENDIF.
DATA(lv_raw_price) = CONV brtwr( 0 ).
CALL FUNCTION 'Z_LEGACY_PRICE_CALCULATE'
EXPORTING
iv_material = iv_material
iv_quantity = iv_quantity
iv_customer = iv_customer
IMPORTING
ev_price = lv_raw_price
EXCEPTIONS
not_found = 1
OTHERS = 2.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_price_calc_error
EXPORTING
textid = zcx_price_calc_error=>material_not_found
material = iv_material.
ENDIF.
" Map flat output to structured result
rs_result-net_price = lv_raw_price.
rs_result-currency = 'EUR'. " Legacy always returned EUR - a known limitation
ENDMETHOD.
ENDCLASS.
Now all new callers use the interface. When your clean implementation is ready, you swap the implementation without touching any calling code. This is the strangler fig in action.
Related reading: For a deeper look at how OOP design patterns enable this kind of clean architectural separation, see ABAP OOP Design Patterns Part 2: Factory, Observer and Decorator Patterns in Real SAP Systems.
Step 4: The Clean Replacement — What Modern ABAP Should Look Like
Once you have your safety harness in place, you can build the clean replacement. Here’s a before/after comparison to make the contrast concrete.
Before (Legacy Style):
" Classic procedural ABAP - real-world example of what you'll encounter
FORM calc_price USING p_matnr p_menge p_kunnr
CHANGING p_price.
DATA: lv_p TYPE p DECIMALS 2,
lv_d TYPE p DECIMALS 2,
wa TYPE mara,
t TYPE TABLE OF konv,
tw LIKE LINE OF t.
SELECT SINGLE * FROM mara INTO wa WHERE matnr = p_matnr.
IF sy-subrc = 0.
lv_p = wa-brgew * 10.
IF p_menge > 100. lv_d = lv_p * '0.15'. lv_p = lv_p - lv_d. ENDIF.
p_price = lv_p * p_menge.
ENDIF.
ENDFORM.
After (Clean ABAP with S/4HANA Constructs):
CLASS zcl_price_calculator DEFINITION PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION.
CLASS-METHODS:
create_instance
RETURNING VALUE(ro_instance) TYPE REF TO zcl_price_calculator.
INTERFACES zif_price_calculator.
PRIVATE SECTION.
CONSTANTS:
c_bulk_quantity_threshold TYPE menge VALUE 100,
c_bulk_discount_rate TYPE p DECIMALS 2 VALUE '0.15'.
METHODS:
read_material_master
IMPORTING iv_material TYPE matnr
RETURNING VALUE(rs_material) TYPE mara
RAISING zcx_price_calc_error,
apply_bulk_discount
IMPORTING iv_base_price TYPE brtwr
iv_quantity TYPE menge
RETURNING VALUE(rv_final_price) TYPE brtwr,
validate_inputs
IMPORTING iv_material TYPE matnr
iv_quantity TYPE menge
iv_customer TYPE kunnr
RAISING zcx_price_calc_error.
ENDCLASS.
CLASS zcl_price_calculator IMPLEMENTATION.
METHOD create_instance.
ro_instance = NEW #( ).
ENDMETHOD.
METHOD zif_price_calculator~calculate.
validate_inputs(
iv_material = iv_material
iv_quantity = iv_quantity
iv_customer = iv_customer
).
DATA(ls_material) = read_material_master( iv_material ).
DATA(lv_unit_price) = CONV brtwr( ls_material-brgew * 10 ).
DATA(lv_line_price) = apply_bulk_discount(
iv_base_price = lv_unit_price
iv_quantity = iv_quantity
).
rs_result = VALUE #(
net_price = lv_line_price * iv_quantity
gross_price = lv_line_price * iv_quantity * '1.19' " 19% VAT
discount_pct = COND #(
WHEN iv_quantity > c_bulk_quantity_threshold
THEN c_bulk_discount_rate * 100
ELSE 0
)
currency = 'EUR'
).
ENDMETHOD.
METHOD validate_inputs.
IF iv_material IS INITIAL.
RAISE EXCEPTION TYPE zcx_price_calc_error
EXPORTING textid = zcx_price_calc_error=>missing_material.
ENDIF.
IF iv_quantity <= 0.
RAISE EXCEPTION TYPE zcx_price_calc_error
EXPORTING textid = zcx_price_calc_error=>invalid_quantity
quantity = iv_quantity.
ENDIF.
IF iv_customer IS INITIAL.
RAISE EXCEPTION TYPE zcx_price_calc_error
EXPORTING textid = zcx_price_calc_error=>missing_customer.
ENDIF.
ENDMETHOD.
METHOD read_material_master.
SELECT SINGLE *
FROM mara
WHERE matnr = @iv_material
INTO @rs_material.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_price_calc_error
EXPORTING textid = zcx_price_calc_error=>material_not_found
material = iv_material.
ENDIF.
ENDMETHOD.
METHOD apply_bulk_discount.
rv_final_price = SWITCH #( iv_quantity
WHEN BETWEEN 1 AND c_bulk_quantity_threshold
THEN iv_base_price
ELSE iv_base_price * ( 1 - c_bulk_discount_rate )
).
ENDMETHOD.
ENDCLASS.
Notice what changed: named constants instead of magic numbers, single-responsibility methods, proper exception handling, and ABAP inline declarations. Every method is testable in isolation. Every exception carries meaningful context.
Related reading: For exception handling patterns that complement this approach, see the complete series starting with ABAP Exception Handling Deep Dive: Building a Resilient Layered Error Management Architecture in SAP S/4HANA.
Step 5: Replace Procedural Database Access with CDS Views
One of the biggest performance and maintainability wins in any ABAP refactoring project comes from eliminating nested SELECT statements and replacing them with CDS-based data access. Legacy code is full of SELECTs inside loops—the infamous “N+1 query problem” in ABAP clothing.
Instead of fetching material data in a loop within your pricing logic, define a focused CDS view that joins exactly what you need:
" In your refactored class, instead of:
" LOOP AT materials INTO DATA(material).
" SELECT SINGLE price FROM some_table WHERE matnr = material-matnr INTO lv_price.
" ENDLOOP.
" Use a single CDS-backed query with FOR ALL ENTRIES or a JOIN:
SELECT matnr, brgew, meins
FROM mara
FOR ALL ENTRIES IN @lt_material_list
WHERE matnr = @lt_material_list-matnr
INTO TABLE @DATA(lt_material_data).
Related reading: The architectural layering principles for CDS-based data access are covered in depth in ABAP CDS Views Series Part 2: Architectural Layers and the performance perspective is addressed in Part 3: Performance Optimization.
Practical Refactoring Checklist
Before signing off on any ABAP refactoring effort, use this checklist as your quality gate:
- ✅ All public methods have ABAP Doc comments (
"!syntax) - ✅ No method exceeds 30 lines of implementation
- ✅ All database access uses modern OpenSQL (no ABAP3.x syntax)
- ✅ Exception classes used throughout — no
sy-subrcchains in business logic - ✅ Constants named meaningfully — no magic numbers or strings
- ✅ ABAP Unit tests cover at least happy path + 2 edge cases per method
- ✅ No SELECT inside LOOP constructs
- ✅ All variable names are descriptive and follow Clean ABAP naming conventions
- ✅ SCI (Code Inspector) runs clean with the team’s agreed check variant
- ✅ Peer review completed by at least one other senior developer
Managing Stakeholder Expectations During Refactoring
Here’s something the technical books rarely cover: the human side of refactoring. Business stakeholders often don’t understand why you’re “rewriting code that works.” Your job as a senior architect isn’t just to write clean code—it’s to communicate the value of that work.
Frame refactoring in business terms. Don’t say “we’re improving cohesion and reducing cyclomatic complexity.” Say “this work will cut the time to implement new pricing rules from three weeks to three days, and reduce the risk of production incidents by eliminating the most common failure patterns we’ve seen in the last two years.”
Tie refactoring work to upcoming features. The best time to refactor a module is right before you need to extend it. That way the business gets immediate value, and you get the architectural breathing room to do it properly.
Related reading: For strategies on making these kinds of architectural decisions under real project pressure, see Technical Leadership in SAP Projects: How Senior Architects Make Better Decisions Under Pressure.
Key Takeaways
Refactoring legacy ABAP is less about perfection and more about discipline. Here’s what I want you to walk away with:
- Understand before you change. Use runtime traces and static analysis before touching anything.
- Write characterization tests first. They’re your safety net, not your validation suite.
- Apply the Strangler Fig Pattern. Build alongside the legacy code, not instead of it—at least initially.
- Let the interface drive the contract. Define what clean behavior looks like before implementing it.
- Communicate in business terms. Technical debt is a business problem, and you need to make that case.
- Iterate, don’t rewrite. Incremental improvement deployed safely beats a perfect system that never ships.
The legacy code in your system represents years of business logic—imperfect, undocumented, but real. Your job isn’t to judge it. Your job is to understand it, preserve its intent, and leave it better than you found it.
What’s Next?
In the next article in this series, we’ll go deeper on ABAP Unit Testing strategies specifically for refactoring scenarios—including how to use test doubles and mock objects to isolate legacy dependencies that you can’t yet replace. If you’ve struggled with testing code that makes direct RFC calls or hits the database, that one is for you.
Have you faced a particularly painful ABAP refactoring project? I’d love to hear what patterns worked—or didn’t—for you. Drop a comment below, or connect on LinkedIn and share your war stories. The best insights in this field come from practitioners, not textbooks.

