Feature Flags (auch Feature Toggles genannt) ermöglichen das schrittweise Ausrollen neuer Funktionen, ohne neuen Code zu deployen. In ABAP Cloud kannst du Feature Flags selbst implementieren oder den SAP Feature Flags Service nutzen.
Was sind Feature Flags?
Feature Flags sind Konfigurationsschalter, die zur Laufzeit bestimmen, ob ein Feature aktiv ist oder nicht:
| Aspekt | Ohne Feature Flags | Mit Feature Flags |
|---|---|---|
| Release | Alles oder nichts | Schrittweise Aktivierung |
| Rollback | Neues Deployment nötig | Sofort per Flag |
| A/B Testing | Komplex | Einfach umsetzbar |
| Beta-Features | Separater Branch | Produktiv, aber versteckt |
| Risiko | Hoch bei großen Releases | Kontrolliert und minimal |
| Feedback | Nach vollständigem Release | Früh von ausgewählten Nutzern |
Typische Einsatzszenarien
┌────────────────────────────────────────────────────────────────┐│ Feature Flag Anwendungsfälle │├────────────────────────────────────────────────────────────────┤│ ││ 1. Release Toggles ││ └─ Neues Feature für alle aktivieren, wenn bereit ││ ││ 2. Experiment Toggles ││ └─ A/B Tests mit verschiedenen Varianten ││ ││ 3. Ops Toggles ││ └─ Features bei Lastproblemen deaktivieren ││ ││ 4. Permission Toggles ││ └─ Features nur für bestimmte Benutzer/Rollen ││ ││ 5. Kill Switches ││ └─ Sofortiges Deaktivieren bei Problemen ││ │└────────────────────────────────────────────────────────────────┘Custom Feature Flag Implementierung
Datenmodell: Feature Flag Tabelle
Erstelle zunächst eine Tabelle für die Feature Flags:
@EndUserText.label : 'Feature Flags'@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE@AbapCatalog.tableCategory : #TRANSPARENT@AbapCatalog.deliveryClass : #C@AbapCatalog.dataMaintenance : #RESTRICTEDdefine table zfeature_flag { key client : abap.clnt not null; key flag_id : abap.char(40) not null; flag_name : abap.char(100); description : abap.char(255); is_enabled : abap_boolean; rollout_percent : abap.int2; valid_from : abap.dats; valid_to : abap.dats; created_by : abap.uname; created_at : timestampl; changed_by : abap.uname; changed_at : timestampl;}User-spezifische Flags
Für Benutzer- oder Rollenbasierte Aktivierung:
@EndUserText.label : 'Feature Flag User Assignments'@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE@AbapCatalog.tableCategory : #TRANSPARENT@AbapCatalog.deliveryClass : #C@AbapCatalog.dataMaintenance : #RESTRICTEDdefine table zfeature_flag_usr { key client : abap.clnt not null; key flag_id : abap.char(40) not null; key user_id : abap.uname not null; is_enabled : abap_boolean; valid_from : abap.dats; valid_to : abap.dats;}Rollenbasierte Flags
@EndUserText.label : 'Feature Flag Role Assignments'@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE@AbapCatalog.tableCategory : #TRANSPARENT@AbapCatalog.deliveryClass : #C@AbapCatalog.dataMaintenance : #RESTRICTEDdefine table zfeature_flag_rol { key client : abap.clnt not null; key flag_id : abap.char(40) not null; key role_id : abap.char(40) not null; is_enabled : abap_boolean; valid_from : abap.dats; valid_to : abap.dats;}Feature Flag Service Klasse
Interface Definition
INTERFACE zif_feature_flag_service PUBLIC.
TYPES: BEGIN OF ty_flag_info, flag_id TYPE zfeature_flag-flag_id, flag_name TYPE zfeature_flag-flag_name, is_enabled TYPE abap_bool, rollout_percent TYPE zfeature_flag-rollout_percent, source TYPE string, END OF ty_flag_info.
METHODS: "! Prüft ob ein Feature für den aktuellen Benutzer aktiv ist is_enabled IMPORTING iv_flag_id TYPE zfeature_flag-flag_id iv_user_id TYPE sy-uname DEFAULT sy-uname RETURNING VALUE(rv_enabled) TYPE abap_bool,
"! Gibt alle Feature Flags zurück get_all_flags RETURNING VALUE(rt_flags) TYPE STANDARD TABLE OF ty_flag_info WITH EMPTY KEY,
"! Aktiviert ein Feature Flag enable_flag IMPORTING iv_flag_id TYPE zfeature_flag-flag_id RAISING zcx_feature_flag,
"! Deaktiviert ein Feature Flag disable_flag IMPORTING iv_flag_id TYPE zfeature_flag-flag_id RAISING zcx_feature_flag,
"! Setzt Rollout-Prozentsatz set_rollout_percentage IMPORTING iv_flag_id TYPE zfeature_flag-flag_id iv_percent TYPE zfeature_flag-rollout_percent RAISING zcx_feature_flag.
ENDINTERFACE.Service Implementation
CLASS zcl_feature_flag_service DEFINITION PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION. INTERFACES zif_feature_flag_service.
CLASS-METHODS: get_instance RETURNING VALUE(ro_instance) TYPE REF TO zcl_feature_flag_service.
PRIVATE SECTION. CLASS-DATA: go_instance TYPE REF TO zcl_feature_flag_service.
DATA: mt_flag_cache TYPE HASHED TABLE OF zfeature_flag WITH UNIQUE KEY flag_id, mv_cache_timestamp TYPE timestampl.
CONSTANTS: c_cache_ttl_seconds TYPE i VALUE 300. " 5 Minuten Cache
METHODS: refresh_cache_if_needed, check_user_override IMPORTING iv_flag_id TYPE zfeature_flag-flag_id iv_user_id TYPE sy-uname EXPORTING ev_found TYPE abap_bool ev_enabled TYPE abap_bool, check_role_override IMPORTING iv_flag_id TYPE zfeature_flag-flag_id iv_user_id TYPE sy-uname EXPORTING ev_found TYPE abap_bool ev_enabled TYPE abap_bool, is_in_rollout_group IMPORTING iv_flag_id TYPE zfeature_flag-flag_id iv_user_id TYPE sy-uname iv_rollout_percent TYPE zfeature_flag-rollout_percent RETURNING VALUE(rv_in_group) TYPE abap_bool, is_within_validity IMPORTING iv_valid_from TYPE dats iv_valid_to TYPE dats RETURNING VALUE(rv_valid) TYPE abap_bool.
ENDCLASS.
CLASS zcl_feature_flag_service IMPLEMENTATION.
METHOD get_instance. IF go_instance IS NOT BOUND. go_instance = NEW #( ). ENDIF. ro_instance = go_instance. ENDMETHOD.
METHOD zif_feature_flag_service~is_enabled. DATA: lv_found TYPE abap_bool, lv_enabled TYPE abap_bool.
" Cache aktualisieren falls nötig refresh_cache_if_needed( ).
" 1. Prüfen ob Flag existiert READ TABLE mt_flag_cache INTO DATA(ls_flag) WITH TABLE KEY flag_id = iv_flag_id.
IF sy-subrc <> 0. " Flag nicht gefunden = deaktiviert rv_enabled = abap_false. RETURN. ENDIF.
" 2. Zeitliche Gültigkeit prüfen IF NOT is_within_validity( iv_valid_from = ls_flag-valid_from iv_valid_to = ls_flag-valid_to ). rv_enabled = abap_false. RETURN. ENDIF.
" 3. User-spezifische Überschreibung prüfen check_user_override( EXPORTING iv_flag_id = iv_flag_id iv_user_id = iv_user_id IMPORTING ev_found = lv_found ev_enabled = lv_enabled ).
IF lv_found = abap_true. rv_enabled = lv_enabled. RETURN. ENDIF.
" 4. Rollen-basierte Überschreibung prüfen check_role_override( EXPORTING iv_flag_id = iv_flag_id iv_user_id = iv_user_id IMPORTING ev_found = lv_found ev_enabled = lv_enabled ).
IF lv_found = abap_true. rv_enabled = lv_enabled. RETURN. ENDIF.
" 5. Globaler Flag-Status IF ls_flag-is_enabled = abap_false. rv_enabled = abap_false. RETURN. ENDIF.
" 6. Rollout-Prozentsatz prüfen IF ls_flag-rollout_percent > 0 AND ls_flag-rollout_percent < 100. rv_enabled = is_in_rollout_group( iv_flag_id = iv_flag_id iv_user_id = iv_user_id iv_rollout_percent = ls_flag-rollout_percent ). ELSE. rv_enabled = xsdbool( ls_flag-rollout_percent = 100 OR ls_flag-is_enabled = abap_true ). ENDIF. ENDMETHOD.
METHOD refresh_cache_if_needed. DATA: lv_current_ts TYPE timestampl, lv_diff TYPE i.
GET TIME STAMP FIELD lv_current_ts.
IF mv_cache_timestamp IS INITIAL. lv_diff = c_cache_ttl_seconds + 1. ELSE. lv_diff = cl_abap_tstmp=>subtract( tstmp1 = lv_current_ts tstmp2 = mv_cache_timestamp ). ENDIF.
IF lv_diff > c_cache_ttl_seconds. " Cache neu laden SELECT * FROM zfeature_flag INTO TABLE @mt_flag_cache.
mv_cache_timestamp = lv_current_ts. ENDIF. ENDMETHOD.
METHOD check_user_override. DATA: lv_today TYPE dats.
ev_found = abap_false. lv_today = cl_abap_context_info=>get_system_date( ).
SELECT SINGLE is_enabled FROM zfeature_flag_usr WHERE flag_id = @iv_flag_id AND user_id = @iv_user_id AND ( valid_from IS INITIAL OR valid_from <= @lv_today ) AND ( valid_to IS INITIAL OR valid_to >= @lv_today ) INTO @ev_enabled.
IF sy-subrc = 0. ev_found = abap_true. ENDIF. ENDMETHOD.
METHOD check_role_override. DATA: lv_today TYPE dats, lt_roles TYPE STANDARD TABLE OF agr_users-agr_name.
ev_found = abap_false. lv_today = cl_abap_context_info=>get_system_date( ).
" Rollen des Benutzers ermitteln (vereinfachte Darstellung) " In der Praxis via Authorization APIs SELECT agr_name FROM agr_users WHERE uname = @iv_user_id AND from_dat <= @lv_today AND to_dat >= @lv_today INTO TABLE @lt_roles.
IF lt_roles IS INITIAL. RETURN. ENDIF.
" Prüfen ob eine Rolle den Flag überschreibt SELECT SINGLE is_enabled FROM zfeature_flag_rol WHERE flag_id = @iv_flag_id AND role_id IN @( SELECT agr_name FROM @lt_roles AS roles ) AND ( valid_from IS INITIAL OR valid_from <= @lv_today ) AND ( valid_to IS INITIAL OR valid_to >= @lv_today ) INTO @ev_enabled.
IF sy-subrc = 0. ev_found = abap_true. ENDIF. ENDMETHOD.
METHOD is_in_rollout_group. DATA: lv_hash TYPE i, lv_bucket TYPE i.
" Deterministischer Hash basierend auf Flag-ID und User-ID " Gleicher User bekommt für gleichen Flag immer das gleiche Ergebnis DATA(lv_combined) = iv_flag_id && iv_user_id. lv_hash = cl_abap_message_digest=>calculate_hash_for_char( if_algorithm = 'MD5' if_data = lv_combined ).
" Hash in Bucket 0-99 umwandeln lv_bucket = lv_hash MOD 100.
" Prüfen ob Bucket im Rollout-Bereich liegt rv_in_group = xsdbool( lv_bucket < iv_rollout_percent ). ENDMETHOD.
METHOD is_within_validity. DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
rv_valid = abap_true.
IF iv_valid_from IS NOT INITIAL AND iv_valid_from > lv_today. rv_valid = abap_false. RETURN. ENDIF.
IF iv_valid_to IS NOT INITIAL AND iv_valid_to < lv_today. rv_valid = abap_false. ENDIF. ENDMETHOD.
METHOD zif_feature_flag_service~get_all_flags. refresh_cache_if_needed( ).
LOOP AT mt_flag_cache INTO DATA(ls_flag). APPEND VALUE #( flag_id = ls_flag-flag_id flag_name = ls_flag-flag_name is_enabled = ls_flag-is_enabled rollout_percent = ls_flag-rollout_percent source = 'DATABASE' ) TO rt_flags. ENDLOOP. ENDMETHOD.
METHOD zif_feature_flag_service~enable_flag. UPDATE zfeature_flag SET is_enabled = @abap_true, changed_by = @sy-uname, changed_at = @( cl_abap_context_info=>get_system_time( ) ) WHERE flag_id = @iv_flag_id.
IF sy-subrc <> 0. RAISE EXCEPTION TYPE zcx_feature_flag MESSAGE e001(zfeature_flag) WITH iv_flag_id. ENDIF.
" Cache invalidieren CLEAR: mt_flag_cache, mv_cache_timestamp. ENDMETHOD.
METHOD zif_feature_flag_service~disable_flag. UPDATE zfeature_flag SET is_enabled = @abap_false, changed_by = @sy-uname, changed_at = @( cl_abap_context_info=>get_system_time( ) ) WHERE flag_id = @iv_flag_id.
IF sy-subrc <> 0. RAISE EXCEPTION TYPE zcx_feature_flag MESSAGE e001(zfeature_flag) WITH iv_flag_id. ENDIF.
" Cache invalidieren CLEAR: mt_flag_cache, mv_cache_timestamp. ENDMETHOD.
METHOD zif_feature_flag_service~set_rollout_percentage. IF iv_percent < 0 OR iv_percent > 100. RAISE EXCEPTION TYPE zcx_feature_flag MESSAGE e002(zfeature_flag) WITH iv_percent. ENDIF.
UPDATE zfeature_flag SET rollout_percent = @iv_percent, changed_by = @sy-uname, changed_at = @( cl_abap_context_info=>get_system_time( ) ) WHERE flag_id = @iv_flag_id.
IF sy-subrc <> 0. RAISE EXCEPTION TYPE zcx_feature_flag MESSAGE e001(zfeature_flag) WITH iv_flag_id. ENDIF.
" Cache invalidieren CLEAR: mt_flag_cache, mv_cache_timestamp. ENDMETHOD.
ENDCLASS.Feature Flag Check im Code
Einfache Verwendung
CLASS zcl_order_service DEFINITION PUBLIC FINAL.
PUBLIC SECTION. METHODS: create_order IMPORTING is_order TYPE zs_order_create RETURNING VALUE(rs_result) TYPE zs_order_result RAISING zcx_order_error.
ENDCLASS.
CLASS zcl_order_service IMPLEMENTATION.
METHOD create_order. DATA(lo_feature_flags) = zcl_feature_flag_service=>get_instance( ).
" Neues Validierungs-Feature prüfen IF lo_feature_flags->zif_feature_flag_service~is_enabled( 'ORDER_EXTENDED_VALIDATION' ). " Neue erweiterte Validierung validate_order_extended( is_order ). ELSE. " Bisherige Validierung validate_order_basic( is_order ). ENDIF.
" Neues Rabatt-Feature prüfen IF lo_feature_flags->zif_feature_flag_service~is_enabled( 'ORDER_AUTO_DISCOUNT' ). rs_result-discount = calculate_auto_discount( is_order ). ENDIF.
" Bestellung erstellen rs_result = create_order_internal( is_order ). ENDMETHOD.
ENDCLASS.Feature Wrapper Pattern
Für saubereren Code empfiehlt sich ein Wrapper-Pattern:
CLASS zcl_features DEFINITION PUBLIC FINAL.
PUBLIC SECTION. CLASS-METHODS: is_extended_validation_enabled RETURNING VALUE(rv_enabled) TYPE abap_bool,
is_auto_discount_enabled RETURNING VALUE(rv_enabled) TYPE abap_bool,
is_new_pricing_engine_enabled RETURNING VALUE(rv_enabled) TYPE abap_bool,
is_beta_dashboard_enabled RETURNING VALUE(rv_enabled) TYPE abap_bool.
PRIVATE SECTION. CLASS-DATA: go_service TYPE REF TO zif_feature_flag_service.
CLASS-METHODS: get_service RETURNING VALUE(ro_service) TYPE REF TO zif_feature_flag_service.
ENDCLASS.
CLASS zcl_features IMPLEMENTATION.
METHOD get_service. IF go_service IS NOT BOUND. go_service = zcl_feature_flag_service=>get_instance( ). ENDIF. ro_service = go_service. ENDMETHOD.
METHOD is_extended_validation_enabled. rv_enabled = get_service( )->is_enabled( 'ORDER_EXTENDED_VALIDATION' ). ENDMETHOD.
METHOD is_auto_discount_enabled. rv_enabled = get_service( )->is_enabled( 'ORDER_AUTO_DISCOUNT' ). ENDMETHOD.
METHOD is_new_pricing_engine_enabled. rv_enabled = get_service( )->is_enabled( 'NEW_PRICING_ENGINE' ). ENDMETHOD.
METHOD is_beta_dashboard_enabled. rv_enabled = get_service( )->is_enabled( 'BETA_DASHBOARD' ). ENDMETHOD.
ENDCLASS.Verwendung im Code:
METHOD calculate_price. IF zcl_features=>is_new_pricing_engine_enabled( ). rv_price = calculate_price_new( is_item ). ELSE. rv_price = calculate_price_legacy( is_item ). ENDIF.ENDMETHOD.Admin-UI für Flag-Verwaltung
RAP Business Object für Feature Flags
@AccessControl.authorizationCheck: #NOT_REQUIRED@EndUserText.label: 'Feature Flags'define root view entity ZI_FeatureFlag as select from zfeature_flag{ key flag_id as FlagId, flag_name as FlagName, description as Description, is_enabled as IsEnabled, rollout_percent as RolloutPercent, valid_from as ValidFrom, valid_to as ValidTo, created_by as CreatedBy, created_at as CreatedAt, changed_by as ChangedBy, changed_at as ChangedAt}Projection View mit UI Annotations
@AccessControl.authorizationCheck: #NOT_REQUIRED@EndUserText.label: 'Feature Flags Admin'@Metadata.allowExtensions: true@Search.searchable: truedefine root view entity ZC_FeatureFlag provider contract transactional_query as projection on ZI_FeatureFlag{ @Search.defaultSearchElement: true key FlagId, @Search.defaultSearchElement: true FlagName, Description, @UI.hidden: false IsEnabled, RolloutPercent, ValidFrom, ValidTo, @UI.hidden: true CreatedBy, @UI.hidden: true CreatedAt, @UI.hidden: true ChangedBy, @UI.hidden: true ChangedAt}Metadata Extension
@Metadata.layer: #CUSTOMERannotate view ZC_FeatureFlag with{ @UI.facet: [ { id: 'GeneralInfo', type: #IDENTIFICATION_REFERENCE, label: 'Feature Flag', position: 10 }, { id: 'Validity', type: #FIELDGROUP_REFERENCE, targetQualifier: 'Validity', label: 'Gültigkeit', position: 20 } ]
@UI.lineItem: [{ position: 10 }] @UI.identification: [{ position: 10 }] FlagId;
@UI.lineItem: [{ position: 20 }] @UI.identification: [{ position: 20 }] FlagName;
@UI.lineItem: [{ position: 30 }] @UI.identification: [{ position: 30 }] Description;
@UI.lineItem: [{ position: 40, criticality: 'EnabledCriticality' }] @UI.identification: [{ position: 40 }] IsEnabled;
@UI.lineItem: [{ position: 50 }] @UI.identification: [{ position: 50 }] @EndUserText.label: 'Rollout %' RolloutPercent;
@UI.fieldGroup: [{ qualifier: 'Validity', position: 10 }] ValidFrom;
@UI.fieldGroup: [{ qualifier: 'Validity', position: 20 }] ValidTo;}RAP Behavior mit Actions
managed implementation in class zbp_i_featureflag unique;strict ( 2 );
define behavior for ZI_FeatureFlag alias FeatureFlagpersistent table zfeature_flaglock masterauthorization master ( global ){ field ( readonly ) CreatedBy, CreatedAt, ChangedBy, ChangedAt; field ( mandatory ) FlagId, FlagName;
create; update; delete;
action EnableFlag result [1] $self; action DisableFlag result [1] $self; action SetRollout parameter zs_rollout_param result [1] $self;
determination SetDefaults on save { create; } determination SetTimestamps on save { create; update; }
mapping for zfeature_flag { FlagId = flag_id; FlagName = flag_name; Description = description; IsEnabled = is_enabled; RolloutPercent = rollout_percent; ValidFrom = valid_from; ValidTo = valid_to; CreatedBy = created_by; CreatedAt = created_at; ChangedBy = changed_by; ChangedAt = changed_at; }}Behavior Implementation
CLASS lhc_featureflag DEFINITION INHERITING FROM cl_abap_behavior_handler. PRIVATE SECTION. METHODS: get_global_authorizations FOR GLOBAL AUTHORIZATION IMPORTING REQUEST requested_authorizations FOR FeatureFlag RESULT result,
enableflag FOR MODIFY IMPORTING keys FOR ACTION FeatureFlag~EnableFlag RESULT result,
disableflag FOR MODIFY IMPORTING keys FOR ACTION FeatureFlag~DisableFlag RESULT result,
setrollout FOR MODIFY IMPORTING keys FOR ACTION FeatureFlag~SetRollout RESULT result,
setdefaults FOR DETERMINE ON SAVE IMPORTING keys FOR FeatureFlag~SetDefaults,
settimestamps FOR DETERMINE ON SAVE IMPORTING keys FOR FeatureFlag~SetTimestamps.ENDCLASS.
CLASS lhc_featureflag IMPLEMENTATION.
METHOD get_global_authorizations. " Berechtigung für Feature Flag Verwaltung prüfen AUTHORITY-CHECK OBJECT 'Z_FF_ADM' ID 'ACTVT' FIELD '02'.
IF sy-subrc = 0. result = VALUE #( ( %create = if_abap_behv=>auth-allowed %update = if_abap_behv=>auth-allowed %delete = if_abap_behv=>auth-allowed %action-EnableFlag = if_abap_behv=>auth-allowed %action-DisableFlag = if_abap_behv=>auth-allowed %action-SetRollout = if_abap_behv=>auth-allowed ) ). ELSE. result = VALUE #( ( %create = if_abap_behv=>auth-unauthorized %update = if_abap_behv=>auth-unauthorized %delete = if_abap_behv=>auth-unauthorized %action-EnableFlag = if_abap_behv=>auth-unauthorized %action-DisableFlag = if_abap_behv=>auth-unauthorized %action-SetRollout = if_abap_behv=>auth-unauthorized ) ). ENDIF. ENDMETHOD.
METHOD enableflag. " Alle ausgewählten Flags aktivieren MODIFY ENTITIES OF zi_featureflag IN LOCAL MODE ENTITY FeatureFlag UPDATE FIELDS ( IsEnabled ) WITH VALUE #( FOR key IN keys ( %tky = key-%tky IsEnabled = abap_true ) ) FAILED failed REPORTED reported.
" Ergebnis zurückgeben READ ENTITIES OF zi_featureflag IN LOCAL MODE ENTITY FeatureFlag ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_flags).
result = VALUE #( FOR flag IN lt_flags ( %tky = flag-%tky %param = flag ) ). ENDMETHOD.
METHOD disableflag. MODIFY ENTITIES OF zi_featureflag IN LOCAL MODE ENTITY FeatureFlag UPDATE FIELDS ( IsEnabled ) WITH VALUE #( FOR key IN keys ( %tky = key-%tky IsEnabled = abap_false ) ) FAILED failed REPORTED reported.
READ ENTITIES OF zi_featureflag IN LOCAL MODE ENTITY FeatureFlag ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_flags).
result = VALUE #( FOR flag IN lt_flags ( %tky = flag-%tky %param = flag ) ). ENDMETHOD.
METHOD setrollout. " Rollout-Prozentsatz setzen MODIFY ENTITIES OF zi_featureflag IN LOCAL MODE ENTITY FeatureFlag UPDATE FIELDS ( RolloutPercent ) WITH VALUE #( FOR key IN keys ( %tky = key-%tky RolloutPercent = key-%param-rollout_percent ) ) FAILED failed REPORTED reported.
READ ENTITIES OF zi_featureflag IN LOCAL MODE ENTITY FeatureFlag ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_flags).
result = VALUE #( FOR flag IN lt_flags ( %tky = flag-%tky %param = flag ) ). ENDMETHOD.
METHOD setdefaults. READ ENTITIES OF zi_featureflag IN LOCAL MODE ENTITY FeatureFlag FIELDS ( IsEnabled RolloutPercent ) WITH CORRESPONDING #( keys ) RESULT DATA(lt_flags).
MODIFY ENTITIES OF zi_featureflag IN LOCAL MODE ENTITY FeatureFlag UPDATE FIELDS ( IsEnabled RolloutPercent ) WITH VALUE #( FOR flag IN lt_flags ( %tky = flag-%tky IsEnabled = COND #( WHEN flag-IsEnabled IS INITIAL THEN abap_false ELSE flag-IsEnabled ) RolloutPercent = COND #( WHEN flag-RolloutPercent IS INITIAL THEN 0 ELSE flag-RolloutPercent ) ) ) REPORTED reported. ENDMETHOD.
METHOD settimestamps. DATA(lv_timestamp) = cl_abap_context_info=>get_system_time( ). DATA(lv_user) = cl_abap_context_info=>get_user_technical_name( ).
READ ENTITIES OF zi_featureflag IN LOCAL MODE ENTITY FeatureFlag FIELDS ( CreatedAt ) WITH CORRESPONDING #( keys ) RESULT DATA(lt_flags).
MODIFY ENTITIES OF zi_featureflag IN LOCAL MODE ENTITY FeatureFlag UPDATE FIELDS ( CreatedBy CreatedAt ChangedBy ChangedAt ) WITH VALUE #( FOR flag IN lt_flags ( %tky = flag-%tky CreatedBy = COND #( WHEN flag-CreatedAt IS INITIAL THEN lv_user ELSE flag-CreatedBy ) CreatedAt = COND #( WHEN flag-CreatedAt IS INITIAL THEN lv_timestamp ELSE flag-CreatedAt ) ChangedBy = lv_user ChangedAt = lv_timestamp ) ) REPORTED reported. ENDMETHOD.
ENDCLASS.SAP Feature Flags Service Integration
Der SAP Feature Flags Service ist ein BTP-Service für Enterprise Feature Management.
Service Subscription
┌────────────────────────────────────────────────────────────────┐│ SAP BTP Cockpit > Services > Instances and Subscriptions │├────────────────────────────────────────────────────────────────┤│ ││ Service: Feature Flags Service ││ Plan: lite (free) / standard ││ ││ Instance Name: feature-flags-instance ││ ││ Binding: ││ - Name: ff-binding ││ - Type: Service Key ││ │└────────────────────────────────────────────────────────────────┘Communication Arrangement
" Destination für Feature Flags ServiceCLASS zcl_feature_flags_btp DEFINITION PUBLIC FINAL.
PUBLIC SECTION. INTERFACES zif_feature_flag_service.
METHODS: constructor IMPORTING iv_destination TYPE string DEFAULT 'FEATURE_FLAGS_SERVICE'.
PRIVATE SECTION. DATA: mv_destination TYPE string, mo_http_client TYPE REF TO if_web_http_client.
METHODS: get_http_client RETURNING VALUE(ro_client) TYPE REF TO if_web_http_client RAISING cx_web_http_client_error.
ENDCLASS.
CLASS zcl_feature_flags_btp IMPLEMENTATION.
METHOD constructor. mv_destination = iv_destination. ENDMETHOD.
METHOD get_http_client. IF mo_http_client IS NOT BOUND. DATA(lo_destination) = cl_http_destination_provider=>create_by_cloud_destination( i_name = mv_destination i_authn_mode = if_a4c_cp_service=>service_specific ).
mo_http_client = cl_web_http_client_manager=>create_by_http_destination( lo_destination ). ENDIF.
ro_client = mo_http_client. ENDMETHOD.
METHOD zif_feature_flag_service~is_enabled. TRY. DATA(lo_client) = get_http_client( ).
DATA(lo_request) = lo_client->get_http_request( ). lo_request->set_uri_path( |/api/v2/evaluate/{ iv_flag_id }| ). lo_request->set_header_field( i_name = 'X-User-Id' i_value = CONV #( iv_user_id ) ).
DATA(lo_response) = lo_client->execute( if_web_http_client=>get ).
IF lo_response->get_status( )-code = 200. DATA(lv_json) = lo_response->get_text( ). " JSON parsen /ui2/cl_json=>deserialize( EXPORTING json = lv_json CHANGING data = rv_enabled ). ELSE. rv_enabled = abap_false. ENDIF.
CATCH cx_web_http_client_error cx_root. " Bei Fehlern: Feature deaktiviert (Fail-Safe) rv_enabled = abap_false. ENDTRY. ENDMETHOD.
METHOD zif_feature_flag_service~get_all_flags. TRY. DATA(lo_client) = get_http_client( ).
DATA(lo_request) = lo_client->get_http_request( ). lo_request->set_uri_path( '/api/v2/flags' ).
DATA(lo_response) = lo_client->execute( if_web_http_client=>get ).
IF lo_response->get_status( )-code = 200. DATA(lv_json) = lo_response->get_text( ). /ui2/cl_json=>deserialize( EXPORTING json = lv_json CHANGING data = rt_flags ). ENDIF.
CATCH cx_web_http_client_error cx_root. " Leere Liste bei Fehler ENDTRY. ENDMETHOD.
METHOD zif_feature_flag_service~enable_flag. " Über BTP API nicht unterstützt - nur über Cockpit/Dashboard RAISE EXCEPTION TYPE zcx_feature_flag MESSAGE e003(zfeature_flag). ENDMETHOD.
METHOD zif_feature_flag_service~disable_flag. RAISE EXCEPTION TYPE zcx_feature_flag MESSAGE e003(zfeature_flag). ENDMETHOD.
METHOD zif_feature_flag_service~set_rollout_percentage. RAISE EXCEPTION TYPE zcx_feature_flag MESSAGE e003(zfeature_flag). ENDMETHOD.
ENDCLASS.Factory für Flexible Backend-Wahl
CLASS zcl_feature_flag_factory DEFINITION PUBLIC FINAL.
PUBLIC SECTION. CLASS-METHODS: get_service IMPORTING iv_use_btp_service TYPE abap_bool DEFAULT abap_false RETURNING VALUE(ro_service) TYPE REF TO zif_feature_flag_service.
ENDCLASS.
CLASS zcl_feature_flag_factory IMPLEMENTATION.
METHOD get_service. IF iv_use_btp_service = abap_true. ro_service = NEW zcl_feature_flags_btp( ). ELSE. ro_service = zcl_feature_flag_service=>get_instance( ). ENDIF. ENDMETHOD.
ENDCLASS.Best Practices für Feature Flags
Flag Lifecycle Management
┌────────────────────────────────────────────────────────────────┐│ Feature Flag Lifecycle │├────────────────────────────────────────────────────────────────┤│ ││ 1. CREATED ││ └─ Flag angelegt, deaktiviert ││ └─ Code implementiert mit Flag-Check ││ ││ 2. TESTING ││ └─ Für Test-User aktiviert ││ └─ QA-Validierung ││ ││ 3. ROLLOUT ││ └─ Schrittweise: 5% → 25% → 50% → 100% ││ └─ Monitoring der Metriken ││ ││ 4. RELEASED ││ └─ 100% aktiviert, Feature ist Standard ││ └─ Flag-Check im Code bleibt vorerst ││ ││ 5. CLEANUP ││ └─ Flag-Check aus Code entfernen ││ └─ Flag aus Datenbank löschen ││ └─ Alten Code-Pfad entfernen ││ ││ ⚠️ WICHTIG: Cleanup nicht vergessen! ││ Tech Debt durch alte Flags vermeiden ││ │└────────────────────────────────────────────────────────────────┘Namenskonventionen
| Prefix | Bedeutung | Beispiel |
|---|---|---|
FF_ | Feature Flag (Standard) | FF_NEW_CHECKOUT |
EXP_ | Experiment (A/B Test) | EXP_BUTTON_COLOR |
OPS_ | Operations Toggle | OPS_CACHE_ENABLED |
KILL_ | Kill Switch | KILL_EXTERNAL_API |
BETA_ | Beta Feature | BETA_DASHBOARD_V2 |
Dokumentation pro Flag
" Empfohlene Dokumentation für jeden Flag"" Flag ID: FF_NEW_PRICING_ENGINE" Owner: [email protected]" Created: 2026-01-15" Purpose: Neuer Preisberechnungsalgorithmus mit ML-Support" Rollout Plan:" - 2026-02-01: 5% (Pilotgruppe)" - 2026-02-15: 25%" - 2026-03-01: 50%" - 2026-03-15: 100%" Cleanup Date: 2026-04-30" Dependencies: None" Rollback: Safe - alter Algorithmus bleibt verfügbarTesting von Feature Flags
CLASS zcl_order_service_test DEFINITION FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.
PRIVATE SECTION. DATA: mo_cut TYPE REF TO zcl_order_service, mo_ff_mock TYPE REF TO ltd_feature_flag_mock.
METHODS: setup, test_with_feature_enabled FOR TESTING, test_with_feature_disabled FOR TESTING.
CLASS-DATA: go_ff_injector TYPE REF TO zif_feature_flag_service.ENDCLASS.
CLASS ltd_feature_flag_mock DEFINITION FOR TESTING. PUBLIC SECTION. INTERFACES zif_feature_flag_service.
DATA: mt_enabled_flags TYPE STANDARD TABLE OF zfeature_flag-flag_id WITH EMPTY KEY.ENDCLASS.
CLASS ltd_feature_flag_mock IMPLEMENTATION. METHOD zif_feature_flag_service~is_enabled. rv_enabled = xsdbool( line_exists( mt_enabled_flags[ table_line = iv_flag_id ] ) ). ENDMETHOD.
METHOD zif_feature_flag_service~get_all_flags. ENDMETHOD.
METHOD zif_feature_flag_service~enable_flag. ENDMETHOD.
METHOD zif_feature_flag_service~disable_flag. ENDMETHOD.
METHOD zif_feature_flag_service~set_rollout_percentage. ENDMETHOD.ENDCLASS.
CLASS zcl_order_service_test IMPLEMENTATION.
METHOD setup. mo_ff_mock = NEW ltd_feature_flag_mock( ). mo_cut = NEW zcl_order_service( io_feature_flags = mo_ff_mock ). ENDMETHOD.
METHOD test_with_feature_enabled. " Given: Feature aktiviert mo_ff_mock->mt_enabled_flags = VALUE #( ( 'FF_NEW_PRICING' ) ).
" When DATA(ls_result) = mo_cut->calculate_price( VALUE #( product_id = 'PROD01' ) ).
" Then: Neuer Preis cl_abap_unit_assert=>assert_equals( act = ls_result-calculation_method exp = 'NEW' ). ENDMETHOD.
METHOD test_with_feature_disabled. " Given: Feature deaktiviert CLEAR mo_ff_mock->mt_enabled_flags.
" When DATA(ls_result) = mo_cut->calculate_price( VALUE #( product_id = 'PROD01' ) ).
" Then: Legacy Preis cl_abap_unit_assert=>assert_equals( act = ls_result-calculation_method exp = 'LEGACY' ). ENDMETHOD.
ENDCLASS.Zusammenfassung
| Thema | Empfehlung |
|---|---|
| Implementierung | Custom Tabelle oder SAP Feature Flags Service |
| Caching | Flags cachen (5-10 Min TTL) für Performance |
| Granularität | User-, Rollen- oder Prozent-basiert |
| Fail-Safe | Bei Fehlern Feature deaktivieren |
| Testing | Mocks für Unit Tests verwenden |
| Lifecycle | Cleanup-Datum bei Erstellung festlegen |
| Dokumentation | Owner, Purpose, Rollout-Plan dokumentieren |
| Monitoring | Flag-Nutzung und Auswirkungen tracken |
Weiterführende Themen
- CI/CD mit ABAP Cloud - Pipeline-Integration
- RAP Basics - RAP-Entwicklung
- SAP Destination Service - Externe Services anbinden