Tooling (+ SAP Build) Blog Posts
cancel
Showing results for 
Search instead for 
Did you mean: 

The landscape of application development on the SAP Business Technology Platform (SAP BTP) has evolved significantly. With SAP Build Code, developers have a unified, AI-powered environment specifically tailored for SAP Cloud Application Programming Model (CAP), SAP Fiori, mobile, and SAPUI5 development.

However, SAP Build Code is more than just a IDE or a single tool. It is a cohesive bundle of services that streamlines the entire application lifecycle. From coding and testing to deployment and management. For many developers, the challenge isn't just learning how to write code; it is understanding how these various interconnected services fit together to create enterprise-grade solutions.

I am launching this blog series to demystify the different services around SAP Build Code offering. We will visit each service individually to understand its specific role, how it integrates with the core development environment, and why it matters for your projects.

Chapter 1: Features Flag Service

In traditional application development, "deploying code" and "releasing a feature" were often the same event. If you pushed code to production, the feature was live. If something broke, you had to scramble to rollback the deployment or push a hotfix.

As we move into the world of cloud-native development on SAP BTP, we need to separate these two concepts. This is where the SAP Feature Flag Service becomes essential. It provides the ability to toggle functionality on or off without changing a single line of code or restarting your application.

In this installment of these SAP Build Code series, we explore how Feature Flags can help in your application's rollout strategy.

What is the SAP Feature Flag Service?

At its core, the SAP Feature Flag service is a control mechanism that allows you to enable or disable new features at runtime.

The critical distinction here is runtime. Unlike configuration changes that might require a restart, feature flags happen instantly. This capability allows you to:

  • Control Code Delivery: Merge code into the main branch without exposing it to users immediately.
  • Synchronize Rollouts: Coordinate the release of a feature that spans multiple microservices.
  • Fast Rollback: If a bug is discovered in a new feature, you can disable it instantly via the dashboard rather than redeploying a previous version of the application.

Why is it critical for cloud native application scenarios? Because there is a specific challenge in cloud development: Synchronization.

In a monolithic application, you deploy everything at once. In a microservice architecture (common in projects on BTP), a single business process might rely on three different services running in separate containers.

  • Scenario: Service A is updated with new logic, but Service B isn't ready yet.
  • Without Flags: You have to hold back the deployment of Service A.
  • With Flags: You deploy Service A with the feature toggled "Off." Once Service B is deployed and agreed that it is ready for end users to be used, you flip the switch, releasing the feature across the entire landscape simultaneously.

Knowing what a feature flag does is simple: it is an on/off switch. However, knowing how to use that switch to improve your software delivery lifecycle is where the real value lies.

Before we dive into the code implementation, it is crucial to understand the different delivery strategies available. These techniques allow you to move from a "Big Bang" deployment model to a more nuanced, risk-averse approach suitable for modern enterprise environments on SAP BTP.

Here are the four key delivery techniques you can leverage:

1. Latent Code Delivery

This technique involves deploying fully functional code to production but keeping it hidden from the end-user.

How it works:

  • You implement a toggle point in your application code that wraps the new functionality.
  • This toggle point is associated with an inactive feature flag.
  • The code resides in the production environment, ready to go, but remains dormant until you decide to switch the flag on at runtime.

Why use it? The primary benefit here is risk reduction. The development and delivery of new features do not put the stability of the whole product at risk.

2. Synchronized Delivery

In the world of microservices and distributed services, a single business process often spans multiple distinct services. Releasing a feature that requires updates to three different services simultaneously can be a logistical nightmare.

How it works:

  • Different teams (e.g., Development Team 1 and Team 2) work on their respective components independently.
  • Both teams wrap their new logic in toggle points that check the same feature flag.
  • Each component is deployed to production as "Latent Code" whenever it is ready.
  • Once all components are live and verified, you activate the single shared feature flag.

Why use it? This decouples deployment from release. Team 1 is not blocked by Team 2. Developers can plan their tasks better, and the feature is only "released" when the entire distributed system is ready to support it.

3. Direct Delivery

Sometimes you don't want to release a feature to everyone, but rather to a specific group of trusted users. This is the foundation of "Beta Testing" or "Internal Access."

How it works:

  • You build logic in your application to identify different sets of users.
  • In the Feature Flag service, you configure a direct shipment strategy.
  • You use identifier query parameters to map specific users or groups to the enabled feature.

Why use it? This is ideal for validating functionality in production with a controlled audience. You can grant access to your internal QA team or a friendly set of "Beta" customers to gather feedback before opening the floodgates to the general public.

4. Percentage Delivery

When you are confident in the feature but unsure about the load it might generate, or if you simply want to perform A/B testing, percentage delivery is the technique of choice.

How it works:

  • You release the feature to a specified percentage of your user base (e.g., 5% or 10%).
  • The service randomly (but consistently) assigns users to the "On" or "Off" group.
  • You can even manage Variations, where you deliver Implementation A to 10% of users and Implementation B to another 10% to see which performs better.

Why use it? This is the ultimate risk mitigation tool. If the new feature causes a performance regression, only a small fraction of your users are affected, and you can dial it back instantly. It also provides data-driven insights, allowing you to choose the implementation that receives the most positive feedback.

---

Example:

We start with our Shop Service (Service A). It is currently running in production and doing exactly what it was designed to do: it takes a user ID and calculates a checkout for the user. (Remember that we are here to talk about feature flag service, so for simplicity, the code developed in CAP is a very basic one).

The service.cds file looks like this:

service ShopService {
    // Define the structure of the response
    type CheckoutResponse {
        userId : String;
        finalPrice : Decimal;
        message : String;
    }

    // Function now returns the Object defined above
    function checkout(userId: String) returns CheckoutResponse;
}

The service.js file looks like this:

const cds = require('@sap/cds');

module.exports = async function () {

    this.on('checkout', async (req) => {
        const { userId } = req.data;
        const randomAmount = Math.floor(Math.random() * 1000) + 1;

        return {
            userId: userId,
            finalPrice: randomAmount,
            message: `Checkout successful for user ${userId} with amount ${randomAmount}`
        };
    });
}

And when we hit the checkout endpoint, the following is retrieved:

{
  "@odata.context": "$metadata#ShopService.CheckoutResponse",
  "userId": "1",
  "finalPrice": 538,
  "message": "Checkout successful for user 1 with amount 538"
}

Now, the business has introduced a new requirement: Loyalty Discounts. We want specific users to receive a percentage off their total bill. However, the logic for determining who is a VIP and how much discount they get is complex. It involves checking history, tier levels, and region.

To keep our architecture clean, we decided not to build this logic inside the Shop Service. Instead, we are creating a dedicated microservice: The Loyalty Service (Project B).

We are now facing a classic distributed system problem:

  1. Shop Service (A) needs to call Loyalty Service (B) to get the discount.
  2. The Loyalty Service is being developed by a different team (or is simply not ready yet).
  3. We cannot deploy the updated Shop Service until the Loyalty Service is live, or the Shop will crash when it tries to call a non-existent API.

This is the perfect use case for Latent Code Delivery.

We are going to update the Shop Service right now to include the orchestration logic. It will try to call the Loyalty Service. However, we will wrap that call in an SAP Feature Flag.

  • Flag OFF: The Shop Service acts exactly as it does today (Standard Price). It ignores the Loyalty Service completely.
  • Flag ON: The Shop Service calls the Loyalty Service, gets the discount, and calculates the new total.

This allows us to deploy the updated Shop Service today, even if the Loyalty Service doesn't exist yet. We are decoupled.

Now, let’s create a new feature flag:
2026-01-30_09-12-19.png

 

With the flag created and disabled, now, let’s check the code being developed. In this implementation, we are modifying the existing checkout logic to support a new "Loyalty Discount" feature. However, we are wrapping this new logic in a Toggle Point so that we can control its release without redeploying the application.

We adapt the cds and js files:

service ShopService {
    // Define the structure of the response
    type CheckoutResponse {
        userId : String;
        originalAmount : Decimal;
        discountPercentage : Integer;
        finalPrice : Decimal;
        message : String;
    }

    // Function now returns the Object defined above
    function checkout(userId: String) returns CheckoutResponse;
}
const cds = require('@sap/cds');

module.exports = async function () {

    // 1. Connect to External Services
    const featureFlagService = await cds.connect.to('FeatureFlagService');
    const loyaltyService = await cds.connect.to('LoyaltyService'); // Connect to Service B

    this.on('checkout', async (req) => {
        const { userId } = req.data;

        // --- STANDARD LOGIC ---
        // 1. Calculate the base price (simulated random amount)
        let randomAmount = Math.floor(Math.random() * 1000) + 1;
        
        // 2. Default state (Standard Pricing)
        let finalPrice = randomAmount;
        let discountPercentage = 0;
        let message = "Standard price applied.";

        // --- THE TOGGLE POINT ---
        const flagName = 'loyalty-discount';
        let isFeatureEnabled = false;

        try {
            // Check the Flag status for this user
            const response = await featureFlagService.send({
                method: 'GET',
                path: `/evaluate/${flagName}?identifier=${userId}`
            });
            
            if (response && response.variation === 'true') {
                isFeatureEnabled = true;
            }
        } catch (error) {
            console.error(`[Feature Flag] Check failed: ${error.message}`);
        }

        // --- LATENT CODE BLOCK ---
        if (isFeatureEnabled) {
            console.log(`[Orchestration] Feature ON. Calling Loyalty Service for ${userId}...`);

            try {
                // >>> REAL CALL TO SERVICE B <<<
                // We ask Service B: "What is the discount for this user?"
                const loyaltyResponse = await loyaltyService.send({
                    method: 'GET',
                    path: `/getDiscount(userId='${userId}')`
                });

                // If Service B responds, we use ITS data
                if (loyaltyResponse && loyaltyResponse.percentage) {
                    discountPercentage = loyaltyResponse.percentage;
                    
                    finalPrice = randomAmount - (randomAmount * (discountPercentage / 100));
                    message = `Loyalty Applied! Service B gave you ${discountPercentage}% off.`;
                }

            } catch (error) {
                // FALLBACK: 
                // If Service B is down (or not created yet), we log it but don't crash.
                // This is crucial for "Synchronized Delivery".
                console.error(`[Orchestration] Failed to contact Loyalty Service: ${error.message}`);
                message = "Standard price applied (Loyalty Service unavailable).";
            }
        } else {
            console.log(`[Orchestration] Feature OFF. Skipping Loyalty Service call.`);
        }

        // --- FINAL RESPONSE ---
        return {
            userId: userId,
            originalAmount: randomAmount,
            discountPercentage: discountPercentage,
            finalPrice: parseFloat(finalPrice.toFixed(2)),
            message: message
        };
    });
}

At the very beginning, we establish connections to the two external services defined in our package.json.

  • FeatureFlagService: The service instance on SAP BTP Feature Flag Service that holds our flag configuration.
  • LoyaltyService: The new remote microservice (Project B) that calculates discounts.

Before executing any new logic, we ask the Feature Flag service if the loyalty-discount feature is active for the current user.

  • We send a GET request to the /evaluate endpoint, passing the userId as the identifier. This allows for Direct Delivery (e.g., enabling the feature only for specific users).
  • Notice the try/catch block around the flag check. If the Feature Flag service is down or unreachable, we catch the error and default isFeatureEnabled to false. This ensures the Shop Service never crashes just because the flag service is having a hiccup.

This is the core of the "Latent Code" delivery technique. The code inside the if (isFeatureEnabled) block is deployed to production but remains dormant until the flag is switched on.

  • If the flag is ON, we attempt to make a real HTTP call to the Loyalty Service.
  • We send the userId to Service B and wait for a calculated discount percentage.
  • If the Loyalty Service is down (or not even deployed yet), the catch block handles the error gracefully. The user simply gets the standard price, and the error is logged. This allows us to deploy Service A before Service B is ready.

Finally, the service returns the response.

  • If the flag was OFF, the user sees the originalAmount and a standard message.
  • If the flag was ON (and succeeded), the user sees the finalPrice and a loyalty message.

Now, with the flag off, we can see the following answer from our Shop Service.

{
  "@odata.context": "$metadata#ShopService.CheckoutResponse",
  "userId": "1",
  "originalAmount": 888,
  "discountPercentage": 0,
  "finalPrice": 888,
  "message": "Standard price applied."
}

If we enable our flag, it is expected that the process will fail, because service B is not developed yet. It is important that you enable the flags at the proper moment.

{
  "@odata.context": "$metadata#ShopService.CheckoutResponse",
  "userId": "1",
  "originalAmount": 220,
  "discountPercentage": 0,
  "finalPrice": 220,
  "message": "Standard price applied (Loyalty Service unavailable)."
}

Now, let’s develop Service B. In a separate project, we define the services and the logic.

service LoyaltyService {

    type DiscountResponse {
        userId : String;
        percentage : Integer;
        tier : String;
    }

    // The endpoint Service A will call
    function getDiscount(userId: String) returns DiscountResponse;
}
const cds = require('@sap/cds');

module.exports = async function () {

    this.on('getDiscount', async (req) => {
        const { userId } = req.data;

        // Default State
        let percentage = 0;
        let tier = 'Bronze';

        // Simulation Logic
        if (userId) {
            const id = userId.toLowerCase();

            if (id.startsWith('vip')) {
                percentage = 20;
                tier = 'Platinum';
            } else if (id.startsWith('a')) {
                percentage = 10;
                tier = 'Gold';
            }
        }

        console.log(`[Loyalty Service] Calculated discount for ${userId}: ${percentage}% (${tier})`);

        return {
            userId: userId,
            percentage: percentage,
            tier: tier
        };
    });
}

Now, after deploying our application, and knowing that we are confident enough to provide this new functionality to our end users, then we enable the feature flag.

2026-01-30_11-01-02.png

After enabling it, and executing our checkout process, we can see that the new functionality is released.

{
  "@odata.context": "$metadata#ShopService.CheckoutResponse",
  "userId": "vip",
  "originalAmount": 889,
  "discountPercentage": 20,
  "finalPrice": 711.2,
  "message": "Loyalty Applied! Service B gave you 20% off."
}

Instantly—without restarting the Shop Service—the application behavior changes.

  1. Shop Service detects the flag is active.
  2. It opens the communication channel to Loyalty Service.
  3. Loyalty Service receives the request, identifies the user as a VIP, and returns a 20% discount.
  4. Shop Service calculates the final price and returns the "Loyalty Applied" message.

What we have just demonstrated is Synchronized Delivery.

In a real-world SAP BTP landscape, you might have the "Shop Team" and the "Loyalty Team" working on different sprints.

  1. The Shop Team deployed their code (with the flag OFF) on Monday.
  2. The Loyalty Team deployed their service on Wednesday.
  3. On Friday, the Product Owner clicked "Enable" in the dashboard.

Both services started working together instantly, with zero downtime and zero code rollbacks. This is the power of decoupling deployment from release.

Enhancing our rollout experience:

We have successfully orchestrated our services, but right now, the feature is either ON for everyone or OFF for everyone.

In a real-world scenario, you rarely roll out a major new feature to 100% of your users immediately. You want to test it with a trusted group first—perhaps your internal team or a set of "Beta" customers. This is where Direct Delivery comes in.

We don't need to change a single line of code to achieve this. Our code is already sending the identifier (userId) to the service.

So, let’s open our feature flag in our feature flag instance and let’s define a Direct Delivery Strategy, in which we define that only users vip01, and a01, will be able to experiment this new feature.

2026-01-30_11-19-10.png

Now, if we try to apply a discount for vip02, it will not even reach/call service B, because the toggle is OFF for that user. Even if the Service B contemplates all kind of users that start with “vip”.

{
  "@odata.context": "$metadata#ShopService.CheckoutResponse",
  "userId": "vip02",
  "originalAmount": 498,
  "discountPercentage": 0,
  "finalPrice": 498,
  "message": "Standard price applied."
}
{
  "@odata.context": "$metadata#ShopService.CheckoutResponse",
  "userId": "vip01",
  "originalAmount": 445,
  "discountPercentage": 20,
  "finalPrice": 356,
  "message": "Loyalty Applied! Service B gave you 20% off."
}

You can also perform a percentage delivery. In my case, I have defined a 50/50 percentage, so the half of my users will take advantage of my new feature, meanwhile others will not be able to use it.

2026-01-30_11-30-58.png

For example, vip06 is not being able to use this new feature, but vip33, can access to it.

{
  "@odata.context": "$metadata#ShopService.CheckoutResponse",
  "userId": "vip06",
  "originalAmount": 725,
  "discountPercentage": 0,
  "finalPrice": 725,
  "message": "Standard price applied."
}
{
  "@odata.context": "$metadata#ShopService.CheckoutResponse",
  "userId": "vip33",
  "originalAmount": 635,
  "discountPercentage": 20,
  "finalPrice": 508,
  "message": "Loyalty Applied! Service B gave you 20% off."
}

---

Personal Thoughts:

Let me share some personal thoughts from using the SAP Feature Flag Service and while redacting this blog post.

Regarding the setup and initial steps to get this service up and running. Consider that it is already available under the Build Code plan. This accelerates the process of incorporating it into your projects. The documentation is very accurate, and with the quick start guide, you can begin using the service in minutes. The only advice I can give, if you really want to take advantage of this service, is to take the time to understand its capabilities and how to adapt them to your needs. This is key to be fully aware of the benefits. The documentation is well-structured, is fast to read, and clearly explains many of the topics involved in using the service. You will find straightforward explanations about the different release methods, what you can achieve, how to perform the setup and how to maintain the feature flags.

Regarding the usage. I really think this is a very powerful tool. It helps developers and business experts agree on the release, coordinating efforts so everyone knows exactly when a feature is ready to go live. One of the most important aspects of releasing new features is the confidence of the project members prior to the launch of a new enhancement. In distributed systems, many factors must be considered during deployment. Even with unit tests, integration tests, and manual tests in place, there are always some concerns when releasing enhancements. Having flags that can enable and disable these features reduces those concerns significantly. By configuring the proper feature flags, you can test with end users before the official release. This ensures you get feedback on time and can improve the tool before everyone else sees it, increasing the confidence prior launch (and reducing possible issues in production).

Another important aspect that all project members should consider is the "next steps" after using these flags. Be careful, having too many blocks inside conditional statements can lead to complicated code (and nesting feature flags can also lead to complicated debugging sessions). Hence, the decommissioning of feature flags is an important step in the enhancement lifecycle. In my opinion, removing the flags in the code becomes a mandatory task if you want to keep your project clean and maintainable. So, do not underestimate the cleanup steps after releasing a new enhancement with this service.

---

Wrap Up:

We have journeyed from a simple, standalone "Shop Service" to a sophisticated, distributed system that orchestrates pricing logic with a "Loyalty Service."

By introducing the SAP Feature Flag Service, we didn't just add a toggle; we fundamentally changed how we deliver software.

  • Decoupling: We deployed Service A's orchestration logic before Service B was even ready.
  • Safety: We ensured that if Service B fails, Service A degrades gracefully to standard pricing.
  • Targeting: With Direct Delivery and Perncetage Delivery, we rolled out the new feature exclusively to a set of users, testing in production with zero risk to the end users.

This is the essence of modern cloud-native development on SAP Build Code. It is not just about writing code; it is about controlling when and how that code provides value to your users.

The SAP Feature Flag Service has even more depth, including complex rule sets and other capabilities that can be check in the following guide. If you find this topic interesting, let me know in the comments, and we can dedicate a future "Deep Dive" post to check more capabilities.

I hope you find this post useful.

Kind regards,

Nata.

7 Comments