AWS Developer Tools Blog

Announcing General Availability of DynamoDB Mapper for Kotlin

DynamoDB Mapper for Kotlin is now generally available, giving Kotlin developers a fully idiomatic way to read, write, and query Amazon DynamoDB using natural Kotlin data types without managing low-level API details. Since the Developer Preview launch in October 2024, we’ve added significant new capabilities based on community feedback including the updateItem operation, batch and transaction operations, atomic counters, TTL management, and more. These features make DynamoDB Mapper a complete, production-ready solution for Kotlin developers working with DynamoDB.

DynamoDB Mapper is a high-level library that provides idiomatic ways to map data between your Kotlin data classes and Amazon DynamoDB tables. It handles schema generation, type conversion, and expression building so you can focus on your business logic instead of low-level DynamoDB API details. In this post I demonstrate the features, call patterns, and API of DynamoDB Mapper.

Getting started

Start by adding the DynamoDB Mapper dependencies and schema generator plugin to your Gradle build:

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.4.20"
    id("aws.sdk.kotlin.hll.dynamodbmapper.schema.generator")
}

dependencies {
    implementation("aws.sdk.kotlin:dynamodb-mapper:$sdkVersion")
    implementation("aws.sdk.kotlin:dynamodb-mapper-annotations:$sdkVersion")
}

Annotate a data class and let the plugin generate the schema at build time:

@DynamoDbItem
data class Order(
    @DynamoDbPartitionKey  val customerId: String,
    @DynamoDbSortKey val orderId: String,
    val status: String,
    val totalCents: Long,
    val productSkus: List<String>,
)

Then use the mapper with type-safe operations:

val client = DynamoDbClient.fromEnvironment()
val mapper = DynamoDbMapper(client)
val ordersTable = mapper.getOrderTable("orders")

// Write an item
val order = Order(
    customerId = "customer-123",
    orderId = "ORDER#2026-07-08#001",
    status = "PENDING",
    totalCents = 4_999L,
    productSkus = listOf("SKU-1", "SKU-2"),
)
ordersTable.putItem(order)

// Get an item by its partition key and sort key
val fetched = ordersTable.getItem("customer-123", "ORDER#2026-07-08#001").item

DynamoDB Mapper automatically generates code for your data classes including item schemas (for example, OrderItemSchema) and extension methods (for example, Table.getOrderTable).

Features available since Developer Preview

The Developer Preview release of DynamoDB Mapper supported many fundamental features which are unchanged in the GA release including:

  • Automatic mapping between DynamoDB items and idiomatic Kotlin types. Work with your own data classes and business types rather than low-level attributes. The mapper uses item schemas to control the transformation of data from your business logic into DynamoDB’s API. This keeps your code type-safe, clean, and maintainable. You can generate schemas automatically from your existing data classes or provide completely custom schema implementations for finer control. For an introduction to the mapping features, see Get started with DynamoDB Mapper.
  • Annotation-driven schema generation. Add annotations to your existing data classes and the mapper’s schema generator plugin for Gradle generates item converters, schemas, and even extension functions for convenient access to tables. For more details, see Generate a schema from annotations.
  • Support for the deleteItem, getItem, putItem, queryPaginated, and scanPaginated operations. Key CRUD operations are made available as APIs that closely parallel DynamoDB’s low-level API—except operating on your Kotlin-level types instead of items and attributes. Like the low-level DynamoDB client for Kotlin, the mapper’s operations are fully integrated with Kotlin coroutines. One-shot operations use suspend methods and paginated operations return Flow<T> values. For more details, see Operations overview.
  • DSL syntax for filter expressions. The Kotlin language provides excellent support for domain-specific languages for more natural interaction with structured data. DynamoDB Mapper continues this tradition with an expressive syntax for defining filter criteria, forming attribute paths, and constructing complex boolean logic. For more details, see Use expressions.
  • And many more! For a full recap, see Announcing the Developer Preview of DynamoDB Mapper for Kotlin.

What’s new since Developer Preview

The GA release expands on the Developer Preview to add more functionality for key use cases. Here are some of the highlights:

Condition expressions

Filter expressions were available in the Developer Preview for queryPaginated and scanPaginated. In GA, those expressions have been extended to optional conditions in the deleteItem, putItem, and updateItem operations:

ordersTable.putItem {
    item = newOrder
    condition { attr["orderId"].notExists() }
}

ordersTable.deleteItem {
    partitionKey = Key("customer-123")
    sortKey = Key("ORDER#2026-07-08#001")
    condition { attr["status"] eq "CANCELED" }
}

The new batch and transaction operations also support per-action conditions. For more information, see Use expressions.

updateItem and update expressions DSL

With the newly-added updateItem operation, you can perform partial, in-place updates on items without fetching and re-writing the entire object. The accompanying update DSL supports all four DynamoDB update actions—SET, REMOVE, ADD, and DELETE—with an idiomatic Kotlin syntax:

ordersTable.updateItem {
    partitionKey = Key("customer-123")
    sortKey = Key("ORDER#2026-07-08#001")
    update {
        set {
            attr["status"] = "SHIPPED"
            attr["totalCents"] = attr["totalCents"] - 500L  // apply a discount
            attr["notes"] = attr["notes"] orElse "none"  // if_not_exists
            attr["productSkus"] = attr["productSkus"] appending listOf("SKU-9")
        }
        remove {
            -attr["couponCode"]  // remove an attribute
        }
        add {
            attr["tags"] += setOf("priority")  // add to a set
        }
        delete {
            attr["tags"] -= setOf("gift")  // remove from a set
        }
    }
}

For more information, see Use expressions.

Batch operations

Operate on items across multiple tables in a single call with batchWriteItem and batchGetItem:

// Batch write: put and delete items in one call in one or more tables
mapper.batchWriteItem {
    table(ordersTable) {
        putItem(Order("customer-123", "ORDER#2026-07-08#001", "SHIPPED", 100_000L, listOf("SKU-1", "SKU-2"))
        putItem(Order("customer-123", "ORDER#2026-07-08#002", "REFUNDED", 39_000L, listOf("SKU-2", "SKU-4"))
        deleteKey(Key("customer-234", "ORDER#2026-07-02#006"))
    }
}

// Batch get: retrieve items by key from one or more tables
val response = mapper.batchGetItem {
    table(ordersTable) {
        key("customer-123", "ORDER#2026-07-08#001")
        key("customer-123", "ORDER#2026-07-08#002")
    }
}
val orders = response.table(ordersTable).items

For more information, see Perform batch operations.

Transactions

Perform all-or-nothing operations across multiple tables with full ACID guarantees:

val ordersTable = mapper.getOrderTable("orders")
val productsTable = mapper.getProductTable("products")
val customersTable = mapper.getCustomerTable("customers")

mapper.transactWriteItems {
    table(ordersTable) {
        put(newOrder) {
            condition { attr["orderId"].notExists() }
        }
    }
    table(productsTable) {
        update(Key("SKU-1")) {
            condition { attr["inventory"] gte 1L }
            update {
                set { attr["inventory"] = attr["inventory"] - 1 }
            }
        }
    }
    table(customersTable) {
        update(Key("customer-123")) {
            condition { attr["balanceCents"] gte 4_999L }
            update {
                set { attr["balanceCents"] = attr["balanceCents"] - 4_999L }
            }
        }
    }
}

Transactional reads are also supported:

val response = mapper.transactGetItems {
    table(ordersTable) { key("customer-123", "ORDER#2026-06-25#0042") }
    table(customersTable) { key("customer-123") }
}
val order = response.table(ordersTable).items.firstOrNull()
val customer = response.table(customersTable).items.firstOrNull()

For more information, see Perform transactional operations.

Atomic counters

Annotate a numeric field with @DynamoDbCounter to have it automatically incremented on every putItem or updateItem call. This feature is useful for view counts, sequence numbers, or inventory tracking:

@DynamoDbItem
data class Product(
    @DynamoDbPartitionKey val sku: String,
    val name: String,
    val category: String,
    val priceCents: Long,
    @DynamoDbCounter var viewCount: Long = 0,
)

For more information, see Built-in features.

TTL management

Annotate an attribute with @DynamoDbTtlSeconds to have DynamoDB Mapper automatically set its value to the current time plus the specified lifetime (in seconds) whenever the item is written. This integrates with DynamoDB’s Time to Live feature to automatically delete expired items:

@DynamoDbItem
data class ShoppingCart(
    @DynamoDbPartitionKey val sessionId: String,
    val productSkus: List<String>,
    @DynamoDbTtlSeconds(lifetime = 86_400) var expiresAt: Long,  // 24-hour TTL
)

For more information, see Built-in features.

Custom attribute converters

Use the @DynamoDbAttributeConverter annotation to specify a custom converter for individual attributes. This is useful for types that DynamoDB Mapper doesn’t handle out of the box, like UUID:

object UuidConverter : ValueConverter<UUID> {
    override fun convertRight(from: UUID): AttributeValue = AttributeValue.S(from.toString())
    override fun convertLeft(from: AttributeValue): UUID = UUID.fromString(from.asS())
}

@DynamoDbItem
data class Order(
    @DynamoDbPartitionKey val customerId: String,
    @DynamoDbSortKey val orderId: String,
    // ...
    @DynamoDbAttributeConverter(UuidConverter::class)
    val idempotencyKey: UUID,
)

For more information, see Manually define schemas.

Secondary index annotations

Secondary index querying was available in Developer Preview, but the GA release adds annotation-driven schema generation for index projections. You can now define a dedicated data class for your index’s projected attributes and generate its schema automatically:

@DynamoDbItem
data class ProductByCategory(
    @DynamoDbPartitionKey val category: String,
    @DynamoDbSortKey val priceCents: Long,
    val sku: String,
    val name: String,
)

// Use with a secondary index
val byCategory = productsTable.getIndex("products-by-category", ProductByCategorySchema)
val cheapElectronics = byCategory
    .queryPaginated {
        keyCondition = KeyFilter("Electronics", { sortKey lt 5_000L })
    }
    .items()

For more information, see Use secondary indexes with DynamoDB Mapper.

Bug fixes since Developer Preview

The following bugs have been fixed since the initial release of Developer Preview:

  • Fixed key field conversion during paginated Scan and Query operations (#1596)
  • Fixed schema code generation for nullable List and Map elements (#1590)
  • Fixed condition expression mapping for operations that support conditions, such as putItem and deleteItem

Breaking changes from Developer Preview

If you were using the Developer Preview release, note these changes when upgrading:

  • @DynamoDbItem annotation: The converterName: String parameter has been replaced with converter: KClass<...> for type safety.
    • Example before: @DynamoDbItem("my.custom.item.converter.MyEmployeeConverter")
    • Example now: @DynamoDbItem(MyEmployeeConverter::class)
  • Converter interfaces refactored: The ItemConverter interface has been replaced with a simple type alias of Converter. Item converter implementations no longer need to identify their keys or perform subset conversions. If you have any custom item converter implementations, they only need to implement convertLeft and convertRight.
  • FilterFilterDsl and SortKeyFilterSortKeyFilterDsl: The expression builder interfaces have been renamed for clarity.
  • Multi-attribute key types: KeySpec.String / KeySpec.Number / KeySpec.ByteArray have been replaced with KeySpec.Key1 through KeySpec.Key4 supporting composite keys.
    • Example before: productsTable.getItem { partitionKey = "SKU-1" }
    • Example now: productsTable.getItem { partitionKey = Key("SKU-1") }
  • @ExperimentalApi annotations removed: All APIs are now stable and production-ready.

Next steps

In this post, I covered how to use some of the new features of DynamoDB Mapper for Kotlin. To get started with your own projects, check out:

We’d love to hear how you’re using DynamoDB Mapper. If you have questions, start a discussion on GitHub. If you’ve found a bug, file an issue on GitHub.

Ian Botsford

Ian Botsford

Ian is a developer working on the AWS SDK for Kotlin. He is passionate about making AWS easy to use through fluent, idiomatic SDKs. You can find him on GitHub @ianbotsf.