AWS Developer Blog

Fluent Client Builders

by Andrew Shore | on | in Java | Permalink | Comments |  Share

We are pleased to announce a better, more intuitive way to construct and configure service clients in the AWS SDK for Java. Previously, the only way to construct a service client was through one of the many overloaded constructors in the client class. Finding the right constructor was difficult and sometimes required duplicating the default configuration to supply a custom dependency. In addition, the constructors of the client don’t expose the full set of options that can be configured through the client. You must provide a region and custom request handlers through a setter on the client after construction. This approach isn’t intuitive, and it promotes unsafe practices when using a client in a multithreaded environment.

New Client Builders

To address the shortcomings of the current approach, we’ve introduced a new builder style to create a client in a fluent and easy-to-read way. The builder approach makes the set of available configurations more discoverable. You can use fluent setters for each option instead of relying on positional parameters in an overloaded constructor. The fluent setters allow for more readable code by using method chaining. The builder pattern also allows for overriding only the options you care about. If you just want a custom RequestMetricCollector, then that’s all you have to set and you can still benefit from the defaults of all the other dependencies. After a client is created with the builder, it is immutable to enforce thread safety when using the client in a multithreaded environment.

Let’s compare the current approach and the new builder approach.

Current Approach


/**
 * Just to provide a custom metric collector I have to supply the defaults
 * of AWSCredentialsProvider and ClientConfiguration. Also note that 
 * I've gotten the default ClientConfiguration for Dynamo wrong, 
 * it's actually got a service specific default config.
**/
AmazonDynamoDBClient client = new AmazonDynamoDBClient(
                                    new DefaultAWSCredentialsProviderChain(), 
                                    new ClientConfiguration(),
                                    new MyCustomRequestMetricsCollector());
/**
 * I have to set the region after which could cause issues in a multi 
 * threaded application if you aren't safely publishing the reference.
**/
client.configureRegion(Regions.US_EAST_1);

Builder Approach


/**
 * With the new builder I only have to supply what I want to customize
 * and all options are set before construction of the client
**/
AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard()
                          .withMetricsCollector(new MyCustomRequestMetricsCollector())
                          .withRegion(Regions.US_EAST_1)
                          .build();

Region Configuration Improvements

In the AWS SDK for Java, we strongly recommend setting a region for every client created. If you create a client through the constructor, it initializes the client with a default endpoint (typically, us-east-1 or a global endpoint). For this reason, many customers don’t bother setting a region because it just works with the default endpoint. When these customers attempt to deploy their application to another region, a lot of code has to be changed to configure the client with the correct region based on the environment. With the builder, we enforce setting a region to promote best practices to make it easier to write applications that can be deployed to multiple regions. When you create clients through the builder, you can provide a region explicitly with the fluent setters or implicitly through the new region provider chain introduced.

The region provider chain is similar to the credentials provider chain. If no region is explicitly provided, the SDK consults the region provider chain to find a region to use from the environment. The SDK will first check if the AWS_REGION environment variable is set. If not, it will look in the AWS Shared Config file (usually located at ~/.aws/config) and use the region in the default profile (unless the AWS_PROFILE environment variable is set, in which case the SDK will look at that profile). Finally, if the SDK still hasn’t found a region, it will attempt to find one from the EC2 metadata service for applications that run on AWS infrastructure. If a region is not explicitly provided or if the chain cannot determine the region, able to be determined by the chain then the builder will not allow the client to be created.


// Creates a client with an explicit region that's in the Regions enum
AmazonDynamoDBClientBuilder.standard()
            .withRegion(Regions.US_WEST_2)
            .build();
/**
 * Creates a client with an explicit region string. Useful when a new
 * region isn't present in the enum and upgrading to a newer 
 * version of the SDK isn't feasible.
**/
AmazonDynamoDBClientBuilder.standard()
            .withRegion("ap-south-1")
            .build();
/** 
 * Creates a client with the default credentials provider chain and a 
 * region found from the new region provider chain.
**/
AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();

Async Client Builder

The SDK offers async variants of most clients. Although the async client duplicates many of the constructors of the sync client, they’ve often gotten out of sync. For example, the async clients do not expose a constructor that takes a RequestMetricsCollector. The new builder remedies that. Going forward, it will allow us to keep the customizable options for both variants of the client in sync. Because the async client has additional dependencies, it is a separate builder class.


/**
 * For ExecutorService we've introduced a factory to supply new 
 * executors for each async client created as it's completely 
 * possible to use a builder instance to create multiple clients and
 * you'll rarely want to share a common executor between them. 
 * The factory interface is a functional interface so it integrates 
 * fully with Java8's lambda expressions and method references.
 */
final AmazonDynamoDBAsync client = AmazonDynamoDBAsyncClientBuilder
         .standard()
         .withExecutorFactory(() -> Executors.newFixedThreadPool(10))
         .withCredentials(new ProfileCredentialsProvider("my-profile"))
         .build();

S3 and TransferManager

S3 is a little different from most clients in the SDK. It has special configuration options (previously in S3ClientOptions) that apply only to S3. It also doesn’t have a typical async client. Instead, it provides the TransferManager utility for asynchronous uploads and downloads. Both the S3 client and TransferManager have their own builders. Note that the options in S3ClientOptions have been collapsed into the builder interface.


// Create a client with accelerate mode enabled
final AmazonS3 client = AmazonS3ClientBuilder.standard()
        .enableAccelerateMode()
        .withRegion("us-east-1").build();

The TransferManager is similar to other async client builders. Options that were in the TransferManagerConfiguration class have been collapsed into the builder interface.


/** 
 * The TransferManager requires the low level client so it must be
 * provided via AmazonS3ClientBuilder
**/
TransferManager tf = TransferManagerBuilder.standard()
        .withExecutorFactory(() -> Executors.newFixedThreadPool(100))
        .withMinimumUploadPartSize(UPLOAD_PART_SIZE)
        .withS3Client(AmazonS3ClientBuilder.defaultClient())
        .build();

// The following factory method is provided for convenience
TransferManagerBuilder.defaultTransferManager();
// The above is equivalent to
TransferManagerBuilder.standard().withS3Client(AmazonS3ClientBuilder.defaultClient()).build();

Fast Facts

  • Each service client has two dedicated builder classes for its sync and async variants
  • Builder class names are based on the interface they create (for example, the builders for the AmazonDynamoDB and AmazonDynamoDBAsync interfaces are AmazonDynamoDBClientBuilder and AmazonDynamoDBAsyncClientBuilder, respectively).
  • Builder instances can be obtained through the static factory method `standard()`.
  • A convenience factory method, defaultClient, initializes the client with the default credential and region provider chains.
  • Builders can be used to create multiple clients.
  • The async builder has all of the same options as the sync client builder plus an additional executor factory option.
  • Region must be provided either explicitly or implicitly (through the region provider chain) before creating a client.
  • Clients built through the builder are immutable.
  • S3 and TransferManager have custom builders with additional configuration options.

 

We are excited about these new additions to the SDK! Feel free to leave your feedback in the comments. To use the new builders, declare a dependency on the 1.11.18 version of the SDK.