The General Data Protection Regulation (GDPR) places specific requirements on software development. As an ABAP Cloud developer, you need to implement these requirements technically. This article shows how to develop GDPR-compliant applications in ABAP Cloud.
GDPR Requirements for Developers
The GDPR defines rights for data subjects that directly affect development:
| Right | Article | Technical Implementation |
|---|---|---|
| Right of Access | Art. 15 | Data export function |
| Right to Rectification | Art. 16 | Edit functions |
| Right to Erasure | Art. 17 | Deletion concept |
| Right to Restriction | Art. 18 | Blocking function |
| Data Portability | Art. 20 | Export in standard format |
| Right to Object | Art. 21 | Consent management |
Technical Principles
GDPR requires Privacy by Design and Privacy by Default:
Privacy by Design:┌─────────────────────────────────────────────────────┐│ Integrate data protection in all development phases │├─────────────────────────────────────────────────────┤│ • Minimal data collection ││ • Check purpose limitation ││ • Define retention periods ││ • Implement encryption ││ • Activate access logging │└─────────────────────────────────────────────────────┘Implementing Deletion Concepts (Data Retention)
A deletion concept defines when and how personal data is deleted. In ABAP Cloud, you implement this through Retention Rules and corresponding deletion jobs.
Define Retention Table
@EndUserText.label: 'Data Category Retention Rules'@AbapCatalog.enhancement.category: #NOT_EXTENSIBLEdefine table zretention_rules { key client : abap.clnt not null; key data_category : abap.char(30) not null; retention_period_days : abap.int4; deletion_method : abap.char(10); -- HARD, SOFT, ANON legal_basis : abap.char(100); last_review_date : abap.dats; responsible_role : abap.char(30);}Implement Deletion Service
CLASS zcl_gdpr_deletion_service DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_oo_adt_classrun.
TYPES: BEGIN OF ty_deletion_result, data_category TYPE string, records_deleted TYPE i, records_failed TYPE i, deletion_date TYPE timestampl, END OF ty_deletion_result, tt_deletion_results TYPE STANDARD TABLE OF ty_deletion_result WITH EMPTY KEY.
METHODS: execute_retention_deletion RETURNING VALUE(rt_results) TYPE tt_deletion_results,
delete_customer_data IMPORTING iv_customer_id TYPE zcustomer_id iv_deletion_type TYPE string DEFAULT 'HARD' RETURNING VALUE(rv_success) TYPE abap_bool,
get_retention_period IMPORTING iv_data_category TYPE string RETURNING VALUE(rv_days) TYPE i.
PRIVATE SECTION. METHODS: get_deletion_candidates IMPORTING iv_data_category TYPE string iv_cutoff_date TYPE d RETURNING VALUE(rt_keys) TYPE ztt_entity_keys,
perform_hard_delete IMPORTING it_keys TYPE ztt_entity_keys RETURNING VALUE(rv_count) TYPE i,
perform_soft_delete IMPORTING it_keys TYPE ztt_entity_keys RETURNING VALUE(rv_count) TYPE i,
log_deletion IMPORTING iv_category TYPE string iv_count TYPE i iv_method TYPE string.ENDCLASS.
CLASS zcl_gdpr_deletion_service IMPLEMENTATION.
METHOD if_oo_adt_classrun~main. DATA(lt_results) = execute_retention_deletion( ).
LOOP AT lt_results INTO DATA(ls_result). out->write( |Category: { ls_result-data_category }| ). out->write( |Deleted: { ls_result-records_deleted }| ). out->write( |Failed: { ls_result-records_failed }| ). out->write( |---| ). ENDLOOP. ENDMETHOD.
METHOD execute_retention_deletion. " Get all active retention rules SELECT * FROM zretention_rules INTO TABLE @DATA(lt_rules).
DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
LOOP AT lt_rules INTO DATA(ls_rule). " Calculate cutoff date DATA(lv_cutoff_date) = lv_today - ls_rule-retention_period_days.
" Determine deletion candidates DATA(lt_candidates) = get_deletion_candidates( iv_data_category = ls_rule-data_category iv_cutoff_date = lv_cutoff_date ).
" Perform deletion DATA(lv_deleted) = COND #( WHEN ls_rule-deletion_method = 'HARD' THEN perform_hard_delete( lt_candidates ) WHEN ls_rule-deletion_method = 'SOFT' THEN perform_soft_delete( lt_candidates ) WHEN ls_rule-deletion_method = 'ANON' THEN anonymize_records( lt_candidates ) ELSE 0 ).
" Log result log_deletion( iv_category = ls_rule-data_category iv_count = lv_deleted iv_method = ls_rule-deletion_method ).
APPEND VALUE #( data_category = ls_rule-data_category records_deleted = lv_deleted records_failed = lines( lt_candidates ) - lv_deleted deletion_date = utclong_current( ) ) TO rt_results. ENDLOOP. ENDMETHOD.
METHOD delete_customer_data. " Individual deletion for data subject request TRY. CASE iv_deletion_type. WHEN 'HARD'. " Irreversible deletion DELETE FROM zcustomer WHERE customer_id = @iv_customer_id. DELETE FROM zcustomer_contact WHERE customer_id = @iv_customer_id. DELETE FROM zcustomer_address WHERE customer_id = @iv_customer_id.
WHEN 'SOFT'. " Mark as deleted UPDATE zcustomer SET is_deleted = @abap_true, deletion_date = @( cl_abap_context_info=>get_system_date( ) ) WHERE customer_id = @iv_customer_id.
WHEN 'ANONYMIZE'. " Anonymization instead of deletion UPDATE zcustomer SET first_name = 'ANONYMIZED', last_name = 'ANONYMIZED', phone = '000000000', is_anonymized = @abap_true WHERE customer_id = @iv_customer_id. ENDCASE.
rv_success = abap_true.
CATCH cx_sy_open_sql_db. rv_success = abap_false. ENDTRY. ENDMETHOD.
METHOD get_retention_period. SELECT SINGLE retention_period_days FROM zretention_rules WHERE data_category = @iv_data_category INTO @rv_days.
IF sy-subrc <> 0. rv_days = 365 * 10. " Default: 10 years ENDIF. ENDMETHOD.
" ... additional methods
ENDCLASS.RAP Action for Deletion
" Behavior Definitiondefine behavior for ZI_Customer alias Customerimplementation in class zbp_i_customer unique{ // GDPR deletion as Action action deletePersonalData result [1] $self;
// Soft Delete instead of physical deletion delete ( features : instance );}" Behavior ImplementationMETHOD deletePersonalData. DATA(lo_deletion_service) = NEW zcl_gdpr_deletion_service( ).
LOOP AT keys INTO DATA(ls_key). DATA(lv_success) = lo_deletion_service->delete_customer_data( iv_customer_id = ls_key-CustomerId iv_deletion_type = 'ANONYMIZE' ).
IF lv_success = abap_true. " Return result READ ENTITIES OF zi_customer IN LOCAL MODE ENTITY Customer ALL FIELDS WITH VALUE #( ( %key = ls_key-%key ) ) RESULT DATA(lt_customers).
result = VALUE #( FOR customer IN lt_customers ( %tky = customer-%tky %param = customer ) ). ELSE. APPEND VALUE #( %tky = ls_key-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = 'Deletion failed' ) ) TO reported-customer. ENDIF. ENDLOOP.ENDMETHOD.Data Anonymization
Anonymization makes data irreversibly non-attributable to a person. It’s an alternative to deletion when data is needed for statistics.
Anonymization Service
CLASS zcl_gdpr_anonymization DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. TYPES: BEGIN OF ty_field_rule, field_name TYPE string, anon_method TYPE string, -- MASK, HASH, RANDOM, CONSTANT, NULLIFY constant_value TYPE string, END OF ty_field_rule, tt_field_rules TYPE STANDARD TABLE OF ty_field_rule WITH EMPTY KEY.
METHODS: anonymize_customer IMPORTING iv_customer_id TYPE zcustomer_id RETURNING VALUE(rv_success) TYPE abap_bool,
anonymize_field IMPORTING iv_value TYPE any iv_method TYPE string iv_constant TYPE string OPTIONAL RETURNING VALUE(rv_result) TYPE string,
generate_pseudonym IMPORTING iv_original TYPE string RETURNING VALUE(rv_pseudonym) TYPE string.
ENDCLASS.
CLASS zcl_gdpr_anonymization IMPLEMENTATION.
METHOD anonymize_customer. " Define anonymization rules DATA(lt_rules) = VALUE tt_field_rules( ( field_name = 'FIRST_NAME' anon_method = 'CONSTANT' constant_value = 'ANON' ) ( field_name = 'LAST_NAME' anon_method = 'CONSTANT' constant_value = 'ANON' ) ( field_name = 'EMAIL' anon_method = 'HASH' ) ( field_name = 'PHONE' anon_method = 'MASK' ) ( field_name = 'BIRTH_DATE' anon_method = 'NULLIFY' ) ( field_name = 'ADDRESS' anon_method = 'CONSTANT' constant_value = 'Anonymized' ) ).
" Load customer data SELECT SINGLE * FROM zcustomer WHERE customer_id = @iv_customer_id INTO @DATA(ls_customer).
IF sy-subrc <> 0. rv_success = abap_false. RETURN. ENDIF.
" Anonymize fields ls_customer-first_name = anonymize_field( iv_value = ls_customer-first_name iv_method = 'CONSTANT' iv_constant = 'ANON' ).
ls_customer-last_name = anonymize_field( iv_value = ls_customer-last_name iv_method = 'CONSTANT' iv_constant = 'ANON' ).
ls_customer-email = anonymize_field( iv_value = ls_customer-email iv_method = 'HASH' ).
ls_customer-phone = anonymize_field( iv_value = ls_customer-phone iv_method = 'MASK' ).
ls_customer-is_anonymized = abap_true. ls_customer-anonymization_date = cl_abap_context_info=>get_system_date( ).
" Save anonymized data UPDATE zcustomer FROM @ls_customer.
rv_success = xsdbool( sy-subrc = 0 ). ENDMETHOD.
METHOD anonymize_field. CASE iv_method. WHEN 'MASK'. " Partially mask (e.g., ***1234) DATA(lv_length) = strlen( iv_value ). IF lv_length > 4. rv_result = repeat( val = '*' occ = lv_length - 4 ) && substring( val = iv_value off = lv_length - 4 len = 4 ). ELSE. rv_result = repeat( val = '*' occ = lv_length ). ENDIF.
WHEN 'HASH'. " One-way hash (not reversible) TRY. cl_abap_message_digest=>create_md5( EXPORTING if_data = cl_abap_codepage=>convert_to( iv_value ) IMPORTING ef_hashstring = rv_result ). CATCH cx_abap_message_digest. rv_result = 'HASH_ERROR'. ENDTRY.
WHEN 'RANDOM'. " Generate random value rv_result = cl_system_uuid=>create_uuid_x16_static( ).
WHEN 'CONSTANT'. " Fixed replacement value rv_result = iv_constant.
WHEN 'NULLIFY'. " Empty value rv_result = ''.
WHEN OTHERS. rv_result = iv_value. ENDCASE. ENDMETHOD.
METHOD generate_pseudonym. " Deterministic pseudonym (same original = same pseudonym) DATA(lv_salt) = 'GDPR_PSEUDONYM_SALT_2024'. DATA(lv_input) = lv_salt && iv_original.
TRY. cl_abap_message_digest=>create_sha256( EXPORTING if_data = cl_abap_codepage=>convert_to( lv_input ) IMPORTING ef_hashstring = DATA(lv_hash) ).
" Create shorter, readable pseudonym rv_pseudonym = 'PSN_' && substring( val = lv_hash len = 12 ). CATCH cx_abap_message_digest. rv_pseudonym = 'PSN_ERROR'. ENDTRY. ENDMETHOD.
ENDCLASS.Pseudonymization vs. Anonymization
| Aspect | Pseudonymization | Anonymization |
|---|---|---|
| Reversible | Yes, with key | No |
| GDPR Status | Personal data | Not personal data |
| Application | Processing | Statistics, archive |
| Example | PSN_A3F82B | ANON |
Implementing Right of Access (Data Export)
Data subjects have the right to receive all stored data about them in a machine-readable format.
Data Export Service
CLASS zcl_gdpr_data_export DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. TYPES: BEGIN OF ty_export_section, category TYPE string, data TYPE REF TO data, END OF ty_export_section, tt_export_sections TYPE STANDARD TABLE OF ty_export_section WITH EMPTY KEY.
METHODS: export_customer_data IMPORTING iv_customer_id TYPE zcustomer_id RETURNING VALUE(rv_json) TYPE string,
export_as_json IMPORTING it_sections TYPE tt_export_sections RETURNING VALUE(rv_json) TYPE string,
create_export_request IMPORTING iv_customer_id TYPE zcustomer_id iv_request_type TYPE string RETURNING VALUE(rv_request_id) TYPE sysuuid_x16.
ENDCLASS.
CLASS zcl_gdpr_data_export IMPLEMENTATION.
METHOD export_customer_data. DATA: lt_sections TYPE tt_export_sections.
" 1. Master data SELECT SINGLE customer_id, first_name, last_name, email, phone, birth_date, created_at FROM zcustomer WHERE customer_id = @iv_customer_id INTO @DATA(ls_customer).
IF sy-subrc = 0. GET REFERENCE OF ls_customer INTO DATA(lr_customer). APPEND VALUE #( category = 'Master Data' data = lr_customer ) TO lt_sections. ENDIF.
" 2. Addresses SELECT address_type, street, city, postal_code, country FROM zcustomer_address WHERE customer_id = @iv_customer_id INTO TABLE @DATA(lt_addresses).
IF sy-subrc = 0. GET REFERENCE OF lt_addresses INTO DATA(lr_addresses). APPEND VALUE #( category = 'Addresses' data = lr_addresses ) TO lt_sections. ENDIF.
" 3. Orders SELECT order_id, order_date, status, total_amount, currency FROM zorder WHERE customer_id = @iv_customer_id INTO TABLE @DATA(lt_orders).
IF sy-subrc = 0. GET REFERENCE OF lt_orders INTO DATA(lr_orders). APPEND VALUE #( category = 'Orders' data = lr_orders ) TO lt_sections. ENDIF.
" 4. Communications SELECT communication_date, channel, subject FROM zcommunication_log WHERE customer_id = @iv_customer_id INTO TABLE @DATA(lt_communications).
IF sy-subrc = 0. GET REFERENCE OF lt_communications INTO DATA(lr_comms). APPEND VALUE #( category = 'Communications' data = lr_comms ) TO lt_sections. ENDIF.
" 5. Consent history SELECT consent_type, consent_date, consent_status, ip_address FROM zconsent_log WHERE customer_id = @iv_customer_id INTO TABLE @DATA(lt_consents).
IF sy-subrc = 0. GET REFERENCE OF lt_consents INTO DATA(lr_consents). APPEND VALUE #( category = 'Consents' data = lr_consents ) TO lt_sections. ENDIF.
" Export as JSON rv_json = export_as_json( lt_sections ). ENDMETHOD.
METHOD create_export_request. " Log export request rv_request_id = cl_system_uuid=>create_uuid_x16_static( ).
INSERT INTO zgdpr_requests VALUES @( VALUE #( request_id = rv_request_id customer_id = iv_customer_id request_type = iv_request_type " EXPORT, DELETE, RECTIFY request_date = cl_abap_context_info=>get_system_date( ) request_time = cl_abap_context_info=>get_system_time( ) status = 'NEW' requestor = cl_abap_context_info=>get_user_technical_name( ) ) ). ENDMETHOD.
ENDCLASS.Consent Management
Consents must be documented, revocable, and verifiable.
Consent Data Model
@EndUserText.label: 'Consent Types'define table zconsent_types { key client : abap.clnt not null; key consent_type : abap.char(30) not null; description : abap.char(200); legal_basis : abap.char(100); is_mandatory : abap_boolean; default_duration : abap.int4; -- Days}
@EndUserText.label: 'Consent Log'define table zconsent_log { key client : abap.clnt not null; key consent_id : sysuuid_x16 not null; customer_id : zcustomer_id; consent_type : abap.char(30); consent_status : abap.char(10); -- GRANTED, WITHDRAWN, EXPIRED consent_date : abap.dats; consent_time : abap.tims; expiry_date : abap.dats; ip_address : abap.char(45); user_agent : abap.char(500); consent_text : abap.string; withdrawal_date : abap.dats; withdrawal_reason : abap.char(200);}Consent Service
CLASS zcl_consent_manager DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. CONSTANTS: BEGIN OF gc_consent_status, granted TYPE string VALUE 'GRANTED', withdrawn TYPE string VALUE 'WITHDRAWN', expired TYPE string VALUE 'EXPIRED', END OF gc_consent_status.
METHODS: grant_consent IMPORTING iv_customer_id TYPE zcustomer_id iv_consent_type TYPE string iv_ip_address TYPE string OPTIONAL iv_user_agent TYPE string OPTIONAL RETURNING VALUE(rv_consent_id) TYPE sysuuid_x16,
withdraw_consent IMPORTING iv_customer_id TYPE zcustomer_id iv_consent_type TYPE string iv_reason TYPE string OPTIONAL RETURNING VALUE(rv_success) TYPE abap_bool,
check_consent IMPORTING iv_customer_id TYPE zcustomer_id iv_consent_type TYPE string RETURNING VALUE(rv_granted) TYPE abap_bool,
expire_old_consents.
ENDCLASS.
CLASS zcl_consent_manager IMPLEMENTATION.
METHOD grant_consent. " Load consent type details SELECT SINGLE * FROM zconsent_types WHERE consent_type = @iv_consent_type INTO @DATA(ls_type).
IF sy-subrc <> 0. RETURN. ENDIF.
" Calculate expiry date DATA(lv_today) = cl_abap_context_info=>get_system_date( ). DATA(lv_expiry) = COND #( WHEN ls_type-default_duration > 0 THEN lv_today + ls_type-default_duration ELSE '99991231' ).
" Create new consent rv_consent_id = cl_system_uuid=>create_uuid_x16_static( ).
INSERT INTO zconsent_log VALUES @( VALUE #( consent_id = rv_consent_id customer_id = iv_customer_id consent_type = iv_consent_type consent_status = gc_consent_status-granted consent_date = lv_today consent_time = cl_abap_context_info=>get_system_time( ) expiry_date = lv_expiry ip_address = iv_ip_address user_agent = iv_user_agent consent_text = ls_type-description ) ).
" Invalidate old consent UPDATE zconsent_log SET consent_status = @gc_consent_status-withdrawn WHERE customer_id = @iv_customer_id AND consent_type = @iv_consent_type AND consent_id <> @rv_consent_id AND consent_status = @gc_consent_status-granted. ENDMETHOD.
METHOD withdraw_consent. DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
UPDATE zconsent_log SET consent_status = @gc_consent_status-withdrawn, withdrawal_date = @lv_today, withdrawal_reason = @iv_reason WHERE customer_id = @iv_customer_id AND consent_type = @iv_consent_type AND consent_status = @gc_consent_status-granted.
rv_success = xsdbool( sy-subrc = 0 ). ENDMETHOD.
METHOD check_consent. DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
SELECT SINGLE @abap_true FROM zconsent_log WHERE customer_id = @iv_customer_id AND consent_type = @iv_consent_type AND consent_status = @gc_consent_status-granted AND expiry_date >= @lv_today INTO @rv_granted. ENDMETHOD.
METHOD expire_old_consents. DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
UPDATE zconsent_log SET consent_status = @gc_consent_status-expired WHERE expiry_date < @lv_today AND consent_status = @gc_consent_status-granted. ENDMETHOD.
ENDCLASS.Consent Check in Business Logic
METHOD send_marketing_email. DATA(lo_consent) = NEW zcl_consent_manager( ).
" Check if marketing consent exists IF lo_consent->check_consent( iv_customer_id = iv_customer_id iv_consent_type = 'MARKETING_EMAIL' ) = abap_false. " No consent - do not send email RAISE EXCEPTION TYPE zcx_no_consent EXPORTING textid = zcx_no_consent=>no_marketing_consent. ENDIF.
" Send marketing email " ...ENDMETHOD.GDPR Compliance Checklist
Development Checklist
| Checkpoint | Implemented | Notes |
|---|---|---|
| Data Minimization | ||
| Only collect necessary fields | ☐ | |
| Required fields minimized | ☐ | |
| No “just in case” data collection | ☐ | |
| Purpose Limitation | ||
| Usage purpose documented | ☐ | |
| Consent obtained for each purpose | ☐ | |
| No misuse of data | ☐ | |
| Storage Limitation | ||
| Retention periods defined | ☐ | |
| Deletion jobs implemented | ☐ | |
| Archiving configured | ☐ | |
| Data Subject Rights | ||
| Access function available | ☐ | |
| Rectification function available | ☐ | |
| Deletion function available | ☐ | |
| Blocking function available | ☐ | |
| Export function available | ☐ | |
| Security | ||
| Access control implemented | ☐ | |
| Encryption during transfer | ☐ | |
| Logging activated | ☐ | |
| Documentation | ||
| Processing records up to date | ☐ | |
| Technical documentation available | ☐ | |
| Data protection impact assessment conducted | ☐ |
Code Review Checklist for GDPR
" ❌ NOT GDPR compliantDATA: gv_customer_ssn TYPE string. " Sensitive data global
" ✅ GDPR compliantDATA(lo_customer) = zcl_customer_handler=>get_instance( iv_customer_id ).DATA(lv_masked_ssn) = lo_customer->get_masked_ssn( ).
" ❌ NOT GDPR compliant - Logging with personal dataMESSAGE |Customer { ls_customer-name } placed order { lv_order_id }| TYPE 'I'.
" ✅ GDPR compliant - No personal data in logMESSAGE |Order { lv_order_id } created| TYPE 'I'.
" ❌ NOT GDPR compliant - Unrestricted exportSELECT * FROM zcustomer INTO TABLE @DATA(lt_all_customers).
" ✅ GDPR compliant - Authorization check and loggingIF lo_auth->check_export_authorization( ) = abap_true. lo_audit->log_data_export( iv_reason = 'CUSTOMER_REQUEST' ). SELECT * FROM zcustomer WHERE customer_id = @iv_customer_id INTO TABLE @DATA(lt_customer_data).ENDIF.Best Practices
1. Privacy by Design
" Data minimization already in data modeldefine table zcustomer_minimal { key customer_id : sysuuid_x16; -- Pseudonym instead of real name display_name : abap.char(100); -- Optional email_hash : abap.char(64); -- Hashed email created_at : timestampl; " NO: Birth date, address, phone - if not needed}2. Privacy by Default
" Default: Minimal data processingMETHOD constructor. " Consent is NOT automatically assumed me->consent_status = gc_consent_status-not_given.
" Marketing is NOT enabled by default me->marketing_enabled = abap_false.
" Data is NOT stored indefinitely me->retention_date = cl_abap_context_info=>get_system_date( ) + 365.ENDMETHOD.Summary
| GDPR Requirement | ABAP Cloud Solution |
|---|---|
| Right to Erasure | RAP Action + Deletion Service |
| Anonymization | Anonymization Service |
| Right of Access | Data Export as JSON |
| Consent Management | Consent Service + CDS Views |
| Retention | ILM Integration |
| Logging | Application Logging |
GDPR compliance requires consistent consideration of data protection requirements throughout the entire application - from data model through business logic to user interface.
Related Topics
- Authorization Checks - Implementing authorization checks
- Change Documents - Logging data changes
- RAP Actions & Functions - Actions for GDPR operations