Use App Lifecycle Manager feature flags with Java

This guide explains how to integrate your Java applications with App Lifecycle Manager feature flags. You'll learn how to use the OpenFeature SDK with the flagd provider to evaluate flags managed by App Lifecycle Manager, allowing you to dynamically control feature availability and behavior in your Java services.

This guide assumes you have already set up the necessary App Lifecycle Manager resources (like SaaS offerings, unit kinds, units, flags, and rollouts) by completing one of the following quickstarts:

Prerequisites

Before you begin, ensure you have the following:

  1. Java installed: JDK 17 or later.
  2. Completed an App Lifecycle Manager feature flags Quickstart:
    • Successfully completed either the integrated or standalone feature flags quickstart to provision your target unit.
    • You should have environment variables or values from that quickstart, such as PROJECT_ID, LOCATION_1 (the region of your Unit), UNIT_ID, and FLAG_KEY.
  3. Authenticated gcloud for Application Default Credentials (ADC): The Java application uses ADC to authenticate with Google Cloud services. Ensure you've authenticated in your environment:

    gcloud auth application-default login
    
  4. IAM Permissions: The identity running your Java application needs the roles/saasconfig.viewer Identity and Access Management role on your Google Cloud project to read flag configurations:

    gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
        --member="user:YOUR_EMAIL_ADDRESS" \
        --role="roles/saasconfig.viewer"
    

    Replace YOUR_PROJECT_ID and YOUR_EMAIL_ADDRESS accordingly.

Understand key environment variables

Your Java application relies on environment variables to connect to the correct flag configuration.

  • FLAGD_SOURCE_PROVIDER_ID: Specifies which feature flag configuration to fetch from the App Lifecycle Manager service. It must be the full resource name of the FeatureFlagConfig associated with your unit.
    • Format: projects/PROJECT_ID/locations/LOCATION/featureFlagsConfigs/UNIT_ID
    • Example: projects/my-gcp-project/locations/us-central1/featureFlagsConfigs/my-app-instance-01

Set up your Java project

Before you can run your application, you must initialize your project and configure the necessary dependencies.

  1. Create a project directory:

    mkdir java-featureflag-app
    cd java-featureflag-app
    
  2. Add dependencies: Configure your build system (Maven or Gradle) to include the following libraries. Ensure compatibility by using versions greater than OpenFeature 1.14.1, flagd 0.11.5, and gRPC 1.71.0.

    • dev.openfeature:javasdk:1.14.1+
    • dev.openfeature.contrib.providers:flagd:0.11.5+
    • io.grpc:grpc-netty-shaded:1.71.0+
    • io.grpc:grpc-auth:1.71.0+
    • com.google.auth:google-auth-library-oauth2-http
  3. Create the initialization code: Add a file named FeatureManagement.java with the following content. This class sets up gRPC interceptors to attach ADC and regional routing headers, and initializes the FlagdProvider using in-process resolution.

    import com.google.auth.oauth2.GoogleCredentials;
    import dev.openfeature.contrib.providers.flagd.Config;
    import dev.openfeature.contrib.providers.flagd.FlagdOptions;
    import dev.openfeature.contrib.providers.flagd.FlagdProvider;
    import dev.openfeature.sdk.OpenFeatureAPI;
    import io.grpc.*;
    import io.grpc.auth.MoreCallCredentials;
    import java.util.ArrayList;
    import java.util.List;
    
    public class FeatureManagement {
        public static void initialize() throws Exception {
            // Read the full Unit resource name
            String flagConfigId = System.getenv("FLAGD_SOURCE_PROVIDER_ID");
            if (flagConfigId == null || flagConfigId.isEmpty()) {
                throw new IllegalStateException("FLAGD_SOURCE_PROVIDER_ID environment variable is not set.");
            }
    
            // Configure gRPC Security and Routing
            GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
            CallCredentials callCredentials = MoreCallCredentials.from(credentials);
    
            List<ClientInterceptor> interceptors = new ArrayList<>();
    
            // Interceptor to inject the Unit name into headers for regional routing
            interceptors.add(new ClientInterceptor() {
                @Override
                public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
                    return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
                        @Override
                        public void start(Listener<RespT> responseListener, Metadata headers) {
                            headers.put(Metadata.Key.of("x-goog-request-params", Metadata.ASCII_STRING_MARSHALLER), "name=" + flagConfigId);
                            super.start(responseListener, headers);
                        }
                    };
                }
            });
    
            // Interceptor to attach ADC to the gRPC calls
            interceptors.add(new ClientInterceptor() {
                @Override
                public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
                    return next.newCall(method, callOptions.withCallCredentials(callCredentials));
                }
            });
    
            // Initialize the flagd provider with In-Process resolution
            FlagdProvider provider = new FlagdProvider(
                FlagdOptions.builder()
                    .resolverType(Config.Resolver.IN_PROCESS)
                    .host("saasconfig.googleapis.com").port(443)
                    .tls(true)
                    .providerId(flagConfigId)
                    .clientInterceptors(interceptors)
                    .syncMetadataDisabled(true)
                    .deadline(10000)
                    .build()
            );
    
            // Set the global provider
            OpenFeatureAPI.getInstance().setProviderAndWait(provider);
        }
    }
    
  4. Create the application logic: Add a file named Main.java with the following content to execute the flag evaluation.

    import dev.openfeature.sdk.OpenFeatureAPI;
    import dev.openfeature.sdk.MutableContext;
    
    public class Main {
        public static void main(String[] args) throws Exception {
    
        // 1. Get Client
        var client = OpenFeatureAPI.getInstance().getClient("simple-api");
    
        // 2. Create Evaluation Context 
        var context = new dev.openfeature.sdk.MutableContext();
    
        // 3. Evaluate Flag
        // Default to false if evaluation fails or service is unreachable
        boolean isEnhanced = client.getBooleanValue("enhanced-search", false, context);
    
        // 4. Execute business logic based on result
        if (isEnhanced) {
            System.out.println("Executing enhanced search algorithm...");
        } else {
            System.out.println("Standard search algorithm...");
        }
    
        }
    }
    

Run the Java application

You can execute the application either locally using your standalone configuration or deployed as a container.

Run standalone (locally)

  1. Set environment variables:

    export PROJECT_ID="your-gcp-project-id"
    export LOCATION_1="us-central1"
    export UNIT_ID="my-app-instance-01"
    export FLAGD_SOURCE_PROVIDER_ID="projects/${PROJECT_ID}/locations/${LOCATION_1}/featureFlagsConfigs/${UNIT_ID}"
    
  2. Run the application: Execute the project using your preferred build tool command (e.g., mvn compile exec:java or ./gradlew run).

Run integrated (containerized)

  1. Create a Dockerfile: Define a container build that packages the Java binary.

    FROM maven:3.9-eclipse-temurin-17 AS builder
    WORKDIR /app
    COPY pom.xml .
    COPY src ./src
    RUN mvn clean package
    
    FROM eclipse-temurin:17-jre
    WORKDIR /app
    COPY --from=builder /app/target/java-featureflag-app.jar app.jar
    ENTRYPOINT ["java", "-jar", "app.jar"]
    
  2. Build and push the image:

    export PROJECT_ID="your-gcp-project-id"
    docker build -t gcr.io/${PROJECT_ID}/my-java-app:latest .
    docker push gcr.io/${PROJECT_ID}/my-java-app:latest
    
  3. Deploy the service: Update your unit blueprint to reference gcr.io/${PROJECT_ID}/my-java-app:latest and deploy the new release using an App Lifecycle Manager rollout.

Verify flag changes

After your application is running, confirm that it evaluates flags correctly.

  1. Observe the initial value: Verify that the console output logs the evaluated value corresponding to the initial configuration state.
  2. Update the flag in App Lifecycle Manager: Modify the flag payload or targeting rules using Google Cloud console or gcloud.
  3. Verify the updated value: Re-run the local execution or observe the container logs to confirm the new evaluated state.

What's next