Skip to main content

BTP Destination Service Integration

The DestinationAccessor API is part of the cloudplatform-connectivity artifact and provides easy and convenient access to destinations that can be identified by their name. Those are, in the vast majority of use cases, destinations managed in the BTP Destination Service on Cloud Foundry.

Destination myDestination = DestinationAccessor.getDestination("my-destination");

Prerequisites

To use the DestinationAccessor to retrieve destinations from the BTP Destination Service on Cloud Foundry, the following prerequisites must be met:

Caching

Destinations retrieved from the BTP Destination Service for Cloud Foundry are cached for 5 minutes by default.

About the DestinationService

Internally, the DestinationAccessor uses the DestinationService class to retrieve destinations managed by the BTP Destination Service on Cloud Foundry.

It offers APIs to

  1. Retrieve individual destinations to connect to the target system:
var service = new DestinationService();

DestinationOptions options;
Try<Destination> destination = service.tryGetDestination("my-destination", options);
  1. Retrieve all destination properties and individual destination properties to read destination configurations:
var service = new DestinationService();

Collection<DestinationProperties> allDestinationProperties = service.getAllDestinationProperties();
DestinationProperties individualProperties = service.getDestinationProperties("my-destination");
  1. Both for either a subscriber or the provider tenant.
  2. Tweak caching behavior:
DestinationService.Cache.setSizeLimit(2_000L);
DestinationService.Cache.setExpiration(Duration.ofMinutes(10), CacheExpirationStrategy.WHEN_CREATED);

How Authentication at the Target System Works

The BTP Destination Service on Cloud Foundry offers a broad range of authentication methods. You can find further details about how to configure destinations in the BTP Cockpit here.

When retrieving a single destination, the Cloud Foundry Destination Service takes over the heavy lifting of authenticating at the target system on behalf of the calling application. Once authentication has been performed successfully, the Cloud Foundry Destination Service returns a destination configuration that contains all the information required to connect to the target system - including, for example, an Authorization header that can be used by the application.

As a consequence, the SAP Cloud SDK's DestinationService abstraction automatically supports all authentication methods offered by the Cloud Foundry Destination Service.

Authentication Flows

Please keep in mind that the authentication flows are not executed by the SAP Cloud SDK itself. Therefore, reusing existing Destination objects for longer periods of time is not recommended as the underlying authentication information may expire (e.g. a JWT). Instead, Destination objects should be retrieved via the DestinationLoader (or DestinationAccessor) API every time they are needed.

Don't worry, there is caching in place to prevent excessive HTTP calls.

OAuth Refresh Token Authentication

For retrieving destinations with authentication type OAuth Refresh Token Authentication the client has to provide the refresh token as part of the DestinationOptions:

var service = new DestinationService();

var opts = DestinationOptions.builder()
.augmentBuilder(DestinationServiceOptionsAugmenter.augmenter()
.refreshToken("<my-refresh-token>")
).build();

Try<Destination> maybeDestination = service.tryGetDestination("my-destination", options);

Please make sure that the refresh token matches the current tenant (and user, if applicable) set in the current ThreadContext.

If Authentication Is Not Required

Sometimes, applications might be interested in reading the destination configuration without actually performing any HTTP calls. In such scenarios, retrieving individual destinations from the BTP Destination Service is suboptimal as the service will perform the configured authentication flow, which can be a costly operation.

To prevent any unnecessary performance loss, the DestinationService offers an API to retrieve all destination configurations. Hereby, no authentication will be performed by the BTP Destination Service for Cloud Foundry.

var service = new DestinationService();

Collection<DestinationProperties> allDestinationProperties = service.getAllDestinationProperties();
DestinationProperties individualProperties = service.getDestinationProperties("my-destination");

Accessing Destination Properties

Both Destination and DestinationProperties object offers a number of APIs to access the destination configuration. For example, to access the URI property, you can use the following code:

Destination destination;
String uri = destination.get(DestinationProperty.URI).getOrElse("");

Destination Fragments (Experimental)

You can obtain destinations with additional data from Destination Fragments.

var service = new DestinationService();
var opts = DestinationOptions.builder()
.augmentBuilder(DestinationServiceOptionsAugmenter.augmenter()
.fragmentName("my-fragment")
).build();
Destination destination = service.tryGetDestination("my-destination", opts).get();

The obtained destination will be cached based on the provided fragment name.

Caching Strategy

In general, it is recommended to disable the change detection mode when using destinations with fragments. Leaving change detection enabled would effectively disable the cache for any fragment-based destinations.

However, if your use case benefits significantly from change detection (e.g. when using many destinations frequently), but fragments are seldomly used, it may be worthwhile keeping change detection enabled.

Since currently destination fragments can only be created via REST API calls, below is an example of how one can create a fragment using the SAP Cloud SDK.

Create a Fragment
var opts = ServiceBindingDestinationOptions.forService(ServiceIdentifier.DESTINATION).build();
var client = ApacheHttpClient5Accessor.getHttpClient(ServiceBindingDestinationLoader.defaultLoaderChain()
.getDestination(opts));

var post = new HttpPost("/subaccountDestinationFragments")
post.setEntity(new StringEntity("""
{
"FragmentName": "my-fragment",
"URL": https://my.fragment.com
}
""", ContentType.APPLICATION_JSON));
client.execute(post);

Multitenancy

The DestinationService is tenant-aware by default. This means that the DestinationService will, by default, perform requests on behalf of the current tenant, if a tenant is available. The tenant and principal information is taken from the ThreadContext as documented here.

Provider Tenant Fallback

If there is no tenant available, the DestinationService will fall back to the provider tenant.

To avoid issues with the default provider tenant fallback implementation, the destination retrieval can be configured using the DestinationOptions API. More precisely, there are two stratgies that can be configured for the destination retrieval:

Example

Using these options can be done like follows:

DestinationLoader loader;

DestinationOptions options =
DestinationOptions
.builder()
.augmentBuilder(
DestinationServiceOptionsAugmenter
.augmenter()
.retrievalStrategy(DestinationServiceRetrievalStrategy.ONLY_SUBSCRIBER))
.augmentBuilder(
DestinationServiceOptionsAugmenter
.augmenter()
.tokenExchangeStrategy(DestinationServiceTokenExchangeStrategy.FORWARD_USER_TOKEN))
.build();

Try<Destination> maybeDestination = loader.tryGetDestination("my-destination", options);

In the above code, we are configuring the DestinationService to only retrieve destinations from the subscriber tenant.
If there is no current tenant or the current tenant is the provider, the destination retrieval will fail.

Furthermore, we are also using the FORWARD_USER_TOKEN strategy.
This strategy will eagerly forward the current user token (e.g. JWT) to the destination service for the destination retrieval. That way, the service is immediately able to perform the authentication flow on behalf of the user.
In contrast to that, using the LOOKUP_THEN_EXCHANGE strategy would first retrieve the destination configuration and then perform a second HTTP request that includes the user token if the destination requires it.

Note that you need an actual instance of DestinationLoader (e.g. DestinationService) to be able to supply the DestinationOptions. The DestinationAccessor does not offer this API.

Resilience

Since the DestinationService is performing error prone operations (i.e. HTTP calls), it comes with some resilience mechanisms out of the box:

  • (request isolated) Time Limiter - Destination retrieval is limited to a maximum of 6 seconds.
  • (tenant isolated) Circuit Breaker - Requests will be blocked once more than 50% of the last 10 requests failed.
    • For fetching individual destinations, the circuit breaker will stay open for 6 seconds.
    • For fetching all destinations, the circuit breaker will stay open for 60 seconds.
  • (tenant isolated) Bulk Head - The maximum number of concurrent requests is limited to 50. Queued tasks will wait for up to 60 seconds before being rejected.

Tweaking Time Limiter

Even though the default time limiter configuration should serve as a good starting point for many applications, it may be necessary to tweak it for specific use cases. This can be achieved by building your own DestinationService instance:

import static com.sap.cloud.sdk.cloudplatform.resilience.ResilienceConfiguration.TimeLimiterConfiguration;

DestinationService service =
DestinationService
.builder()
.withTimeLimiterConfiguration(TimeLimiterConfiguration.of(Duration.ofSeconds(30)))
.build();

To apply this customized configuration for all future destination retrievals, you need to prepend your customized instance to the DestinationAccessor:

DestinationAccessor.prependDestinationLoader(service);

Default Caching Behavior

Besides the resilience measures explained above, the DestinationService also comes with a cache. This cache stores retrieved destinations based on tenant and (for some scenarios) based on user.

For example: When retrieving a destination that uses BasicAuthentication, using tenant isolation is sufficient as all users share the same credentials. However, when retrieving a destination that uses OAuth2SAMLBearerAssertion authentication, the destination is isolated based on tenant and user.

Once cached, entries will be stored for up to 5 minutes, where destinations that use JWT-based authentication may expire earlier. Additionally, the cache is configured to store only the 1000 latest entries across all tenants. This is to prevent excessive memory usage in case of a large number of tenants and/or destinations.

Tweaking the Caching Behavior

Even though the default caching behavior should serve as a good starting point for many applications, it may be necessary to tweak it for specific use cases. For that, the DestinationService.Cache offers a number of configuration APIs (JavaDoc).

Change Detection Mode

Change detection mode is a special caching mode that significantly boosts performance in most scenarios.

Traditionally, without change detection mode, all cache entries expire latest after a fixed amount of time (i.e. 5 minutes by default).

With change detection, this time-based expiration is disabled. Instead, when accessing the cache, the implementation checks whether the destination has changed since the last access. This is done by performing a single HTTP request (per tenant) to fetch all destination configurations - this request is cached itself as well and uses the expiration configured via DestinationService.Cache.setExpiration (or the default value). The received configurations are then compared to the cached ones and, if nothing has changed, the cache entry is considered valid.

Note: Destinations that use JWT-based authentication will still be refreshed before the attached JWT expires.

For a single destination (per tenant) this is not a benefit, but for two or more the benefit is up to 100% per additional destination. For example, if your application is interacting with 5 destinations per tenant, you can expect a 3 to 4 times performance increase (i.e. 3 to 4 times less costly HTTP calls).

If that behavior is not desired, you may disable the change detection mode:

DestinationService.Cache.disableChangeDetection();