Feature Flags in ABAP Cloud

kategorie
DevOps
Veröffentlicht
autor
Johannes

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:

AspektOhne Feature FlagsMit Feature Flags
ReleaseAlles oder nichtsSchrittweise Aktivierung
RollbackNeues Deployment nötigSofort per Flag
A/B TestingKomplexEinfach umsetzbar
Beta-FeaturesSeparater BranchProduktiv, aber versteckt
RisikoHoch bei großen ReleasesKontrolliert und minimal
FeedbackNach vollständigem ReleaseFrü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 : #RESTRICTED
define 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 : #RESTRICTED
define 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 : #RESTRICTED
define 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: true
define 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: #CUSTOMER
annotate 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 FeatureFlag
persistent table zfeature_flag
lock master
authorization 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 Service
CLASS 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

PrefixBedeutungBeispiel
FF_Feature Flag (Standard)FF_NEW_CHECKOUT
EXP_Experiment (A/B Test)EXP_BUTTON_COLOR
OPS_Operations ToggleOPS_CACHE_ENABLED
KILL_Kill SwitchKILL_EXTERNAL_API
BETA_Beta FeatureBETA_DASHBOARD_V2

Dokumentation pro Flag

" Empfohlene Dokumentation für jeden Flag
"
" Flag ID: FF_NEW_PRICING_ENGINE
" 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ügbar

Testing 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

ThemaEmpfehlung
ImplementierungCustom Tabelle oder SAP Feature Flags Service
CachingFlags cachen (5-10 Min TTL) für Performance
GranularitätUser-, Rollen- oder Prozent-basiert
Fail-SafeBei Fehlern Feature deaktivieren
TestingMocks für Unit Tests verwenden
LifecycleCleanup-Datum bei Erstellung festlegen
DokumentationOwner, Purpose, Rollout-Plan dokumentieren
MonitoringFlag-Nutzung und Auswirkungen tracken

Weiterführende Themen