AWS Developer Tools Blog

Announcing general availability of the AWS SDK for Kotlin

We are excited to announce that the AWS SDK for Kotlin is now generally available and supported for production use.

We designed the SDK from the ground up to give you an idiomatic Kotlin experience, including Domain Specific Language (DSL) builders, and support for asynchronous AWS service calls using coroutines. Today’s release enables developers to target the JVM platform or Android API Level 24+, with support for additional platforms like Kotlin/Native coming in future releases. See the roadmap for more details.

In this post we’ll take a look at the key features offered by the SDK including DSL builders, coroutines support, pagination, waiters, a pluggable HTTP layer, and more.

Why Kotlin?

You may be wondering why we wrote a new Kotlin SDK from scratch. After all, Kotlin makes it easy to have a mixed codebase and allows you to call Java code from Kotlin. Existing AWS customers developing in Kotlin already use the AWS SDK for Java this way. Nevertheless, we believe customers will benefit from a dedicated Kotlin SDK.

First, Kotlin has more to offer than Java interoperability including null-safety, coroutines, extension functions, and smart casting. See the comparison to Java in the Kotlin documentation for more details. We wanted to offer an SDK that took full advantage of the language and felt idiomatic to Kotlin developers.

Second, Android mobile development has been Kotlin-first since 2019. Android developers should have access to a modern SDK with support for all AWS services. That is why the AWS SDK for Kotlin supports Android API 24+ as a first class target. In fact, AWS Amplify for Android v2 is built on top of the AWS SDK for Kotlin.

Finally, Kotlin isn’t a JVM-only language. Kotlin multiplatform allows you to write Kotlin code that targets the JVM, native binaries (Linux, Windows, macOS, and iOS), JavaScript, and WASM. The SDK was developed as a multiplatform library from the beginning and we have plans to support additional targets in the future.

Idiomatic Kotlin

The AWS SDK for Kotlin provides DSLs following a type-safe builder pattern to create requests.

The following example shows how to create an Amazon DynamoDB table using the SDK:

DynamoDbClient.fromEnvironment().use { ddb ->
    ddb.createTable {
        tableName = "movies"

        attributeDefinitions = listOf(
            AttributeDefinition {
                attributeName = "year"
                attributeType = ScalarAttributeType.N
            },
            AttributeDefinition {
                attributeName = "title"
                attributeType = ScalarAttributeType.S
            }
        )
        
        keySchema = listOf(
            KeySchemaElement {
                attributeName = "year"
                keyType = KeyType.Hash
            },
            KeySchemaElement {
                attributeName = "title"
                keyType = KeyType.Range
            }
        )

        provisionedThroughput {
            readCapacityUnits = 10
            writeCapacityUnits = 10
        }
    }
}

First class coroutines support

The AWS SDK for Kotlin is asynchronous by design. The SDK uses suspend functions for all service operations, which must be called from a coroutine.

The following example shows how to make concurrent requests to Amazon S3 using the headObject operation to get the content size of two objects:

val bucketName = "my-bucket"   
val keys = listOf("key1", "key2")

S3Client.fromEnvironment().use { s3 ->
    val totalSizeOfKeys = keys
        .map {
            async {
                s3.headObject {
                    bucket = bucketName
                    key = it
                }.contentLength
            }
        }
        .awaitAll()
        .sum()

    println("total size of $keys: $totalSizeOfKeys bytes")
}

Streaming operations

Operations involving streaming output, like Amazon S3 getObject, are scoped to a block. Instead of returning the response directly, you supply a lambda that is given access to the response (and the underlying stream). This scoping of streaming responses simplifies lifetime management of resources for both the caller and the runtime. The output of the call is the result of the lambda expression.

val bucketName = "my-bucket"
val keyName = "key1"

S3Client.fromEnvironment().use { s3 ->
    // get an object from S3 and write it to file locally
    val request = GetObjectRequest {
        bucket = bucketName
        key = keyName
    }

    val dest = File("/tmp/$keyName")
    val objectSize = s3.getObject(request) { resp ->
        resp.body?.writeToFile(dest) ?: error("no response stream given")
        // resp is only valid until the end of this block
    } 
    println("wrote $objectSize bytes to $dest")
    println("contents: ${dest.readText()}")
}

See streaming operations in the developer guide for more information.

Pagination

To maximize availability and minimize latency, many AWS APIs break up a result across multiple “pages” of responses. The SDK can handle this automatically for you.

The following code prints all of your Amazon DynamoDB table names handling multiple pages if necessary.

DynamoDbClient.fromEnvironment().use { ddb ->
    ddb.listTablesPaginated()
        .tableNames()
        .collect(::println)
}

Waiters

When working with services that are eventually consistent, or services that asynchronously create resources, it is common to write logic that periodically polls the status of a resource until a desired state is reached or an error occurs. The SDK provides waiters that can handle this polling logic for you.

val bucketName = "my-bucket"

S3Client.fromEnvironment().use { s3 ->
    // initiate creating a bucket and potentially returns before it exists
    s3.createBucket {
        bucket = bucketName
        createBucketConfiguration {
            locationConstraint = BucketLocationConstraint.UsWest2
        }
    }

    // when this function returns, either the bucket exists or an exception is thrown
    s3.waitUntilBucketExists {
        bucket = bucketName
    }

    // bucket exists at this point
    s3.putObject {
        bucket = bucketName
        key = "helloworld.txt"
        body = ByteStream.fromString("Hello S3")
    }
}

Pluggable HTTP layer

The AWS SDK for Kotlin includes a default HTTP client (based on OkHttp) that works well for most use cases. However, there may be benefits to using a client that is more optimized for your runtime environment (for example, to optimize AWS Lambda cold start times). The SDK allows the HTTP client to be configured with another implementation that better suits your use case.

Today, the SDK offers two HTTP clients:

  • OkHttpEngine based on OkHttp
  • CrtHttpEngine based on the AWS Common Runtime (CRT)
    • Requires a dependency on aws.smithy.kotlin:http-client-engine-crt:<version>

Additionally, you can also implement your own client or open a feature request to have additional clients supported.

The following code shows how to create a service client using the CRT engine:

val s3 = S3Client.fromEnvironment {
    httpClient(CrtHttpEngine) {
        maxConnections = 64u
        connectTimeout = 5.seconds
    }
}

HTTP client sharing

A single HTTP client can be shared among multiple AWS service clients. This is useful in resource-constrained environments where you might want to share a single pool of connections among multiple AWS services.

// create an HTTP client with a maximum of 16 connections to share among 
// multiple AWS service clients
val crtHttpClient = CrtHttpEngine { maxConnections = 16u }

val s3 = S3Client.fromEnvironment { httpClient = crtHttpClient }
val ddb = DynamoDbClient.fromEnvironment { httpClient = crtHttpClient }

// use s3 & ddb clients

// close clients when done
s3.close()
ddb.close()

// because the HTTP client is no longer tied to a single AWS service client,
// you need to close the HTTP client yourself when done using it
crtHttpClient.close()

Additional features

  • Observability: Observability is an important property of any modern application to enable monitoring and diagnostics when they are needed most. The SDK comes instrumented out of the box and is capable of emitting all three common telemetry signals: metrics, traces, and logs. See the Observability section of the developer guide for more details on how to configure a TelemetryProvider to send telemetry data to an observability backend (such as AWS X-Ray or Amazon CloudWatch) and for details on available metrics.
  • Overridable client configuration: Override configuration for one or more operations.
  • Interceptors: Hook into the execution of API requests and responses. Interceptors are open-ended mechanisms in which the SDK calls code that you write to inject behavior into the request/response lifecycle.
  • Retries: Modify the retry strategy or the policy used by the SDK to determine if an error can be retried.
  • Logging: Enable trace logging or opt into wire level logging to debug your application and its use of the SDK.

Roadmap

Please refer to the roadmap for details on upcoming features planned for future releases. The following are some of the key priorities we are focused on:

We value your input, please upvote to help us prioritize or create a new feature request if something you want to see isn’t supported yet.

Getting started

Now that you’ve seen what the AWS SDK for Kotlin can do, give it a try in your own code! The developer guide is a great resource explaining how to start using the SDK. Check it out and let us know what you think by opening a discussion or issue.

Additional resources:

About the author:

Aaron Todd

Aaron Todd

Aaron is a maintainer for the AWS SDK for Kotlin and AWS SDK for Go. You can find him on GitHub @aajtodd.