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, andscanPaginatedoperations. 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 usesuspendmethods and paginated operations returnFlow<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
ScanandQueryoperations (#1596) - Fixed schema code generation for nullable
ListandMapelements (#1590) - Fixed condition expression mapping for operations that support conditions, such as
putItemanddeleteItem
Breaking changes from Developer Preview
If you were using the Developer Preview release, note these changes when upgrading:
@DynamoDbItemannotation: TheconverterName: Stringparameter has been replaced withconverter: KClass<...>for type safety.- Example before:
@DynamoDbItem("my.custom.item.converter.MyEmployeeConverter") - Example now:
@DynamoDbItem(MyEmployeeConverter::class)
- Example before:
- Converter interfaces refactored: The
ItemConverterinterface has been replaced with a simple type alias ofConverter. 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 implementconvertLeftandconvertRight. Filter→FilterDslandSortKeyFilter→SortKeyFilterDsl: The expression builder interfaces have been renamed for clarity.- Multi-attribute key types:
KeySpec.String/KeySpec.Number/KeySpec.ByteArrayhave been replaced withKeySpec.Key1throughKeySpec.Key4supporting composite keys.- Example before:
productsTable.getItem { partitionKey = "SKU-1" } - Example now:
productsTable.getItem { partitionKey = Key("SKU-1") }
- Example before:
@ExperimentalApiannotations 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.