Skip to content

1.0.0-ALPHA

Pre-release
Pre-release

Choose a tag to compare

@MatejNedic MatejNedic released this 03 Sep 22:48
· 2 commits to main since this release

Spring Data DynamoDB 1.0.0-ALPHA Release Notes

What's New

Spring Data DynamoDB 1.0.0-ALPHA is the first official release of Spring Data support for Amazon DynamoDB. This module brings the familiar Spring Data programming model to DynamoDB with first-class support for single-table design patterns.

πŸ“— Reference Documentation

πŸš€ Why Spring Data DynamoDB?

DynamoDB's power lies in its ability to model multiple entity types within a single physical table, enabling efficient access patterns through strategic use of partition keys, sort keys, and secondary indexes. Traditional ORM approaches often obscure this design philosophy.

Spring Data DynamoDB embraces DynamoDB's core principles:

  • Not an ORM β€” DynamoDB's (partition key, sort key) model is closer to Cassandra than to traditional document stores; this library makes that modeling explicit and readable
  • Single-table design first β€” store multiple entity types in one table with typed repositories for each view
  • Access pattern driven β€” model your data around how you query it, not how you store it
  • Type-safe secondary indexes β€” read-only views over GSIs and LSIs with compile-time guarantees

✨ Key Features

Repository Support

  • Spring Data Repository abstractions β€” familiar CRUD operations and query derivation for DynamoDB
  • Derived query methods β€” generate queries from method names: findByPkAndSkStartingWith(...)
  • Explicit @Query expressions β€” write DynamoDB key condition and filter expressions when derivation isn't enough
  • PartiQL support β€” execute PartiQL statements directly from repository methods
  • Named queries β€” define reusable queries in properties files for better maintainability

Single-Table Design Features

  • @Table and @Embedded annotations β€” model multiple entity types in a single physical table with prefix-based routing using startsWith, endsWith, or regex patterns
  • Secondary Index Views β€” type-safe, read-only @SecondaryIndex views over GSIs and LSIs that automatically set IndexName
  • Multi-attribute GSI keys β€” support for DynamoDB's multi-key GSI feature (up to 4 partition attributes and 4 sort attributes) with explicit ordering
  • Sort Key Templates β€” compose sort keys from multiple properties with @SortKeyTemplate and decompose them automatically on read
  • Polymorphic containers β€” store and retrieve different entity types from the same partition key with type-safe routing
  • Item Collection Views β€” read-only @ItemCollectionView for grouping heterogeneous rows from a partition into a single typed aggregate

DynamoDB-Native Features

  • Keyset pagination β€” Window<T> pagination backed by DynamoDB's LastEvaluatedKey cursor (no offset-based pagination)
  • Optimistic locking β€” @Version-based conditional writes for safe concurrent updates
  • Batch operations β€” saveAll() uses BatchWriteItem, automatically chunked at 25 items with retry logic for unprocessed items
  • Template API β€” DynamoDbOperations and DynamoDbTemplate for operations beyond repository methods
  • Typed query builder β€” IndexQueryBuilder for validated, fluent queries with automatic attribute name aliasing

Developer Experience

  • Event callbacks β€” before/after save, convert, update, and delete events with @EventListener and functional callback interfaces
  • Custom conversions β€” register custom converters for domain-specific types or use per-property @ValueConverter
  • Flexible configuration β€” override AbstractDynamoDbConfiguration for full control
  • AWS SDK v2 β€” built on the modern AWS SDK for Java 2.x
  • Spring Cloud AWS integration β€” seamless auto-configuration when using Spring Cloud AWS starters
  • Fail-fast validation β€” invalid queries, scan methods without @AllowScan, and incorrect entity mappings fail at application startup, not at runtime

πŸ“¦ Compatibility

Component Version
Spring Boot 4.1.x
Spring Framework 7.0.x
Spring Data Commons 4.1.x
AWS Java SDK 2.x
Java 17+

🎯 Getting Started

Add the dependency:

<dependency>
    <groupId>io.awspring.cloud</groupId>
    <artifactId>spring-data-dynamodb</artifactId>
    <version>1.0.0-ALPHA</version>
</dependency>

With Spring Cloud AWS (recommended)

Add the DynamoDB starter:

<dependency>
    <groupId>io.awspring.cloud</groupId>
    <artifactId>spring-cloud-aws-starter-dynamodb</artifactId>
</dependency>

Configure the module:

@Configuration
@EnableDynamoDbRepositories(basePackageClasses = TournamentRepository.class)
public class DynamoDbConfig extends AbstractDynamoDbConfiguration {
}

Configure the client through properties:

spring.cloud.aws.region.static=eu-central-1
# Optional: LocalStack or custom endpoint
spring.cloud.aws.dynamodb.endpoint=http://localhost:4566

Standalone Configuration

@Configuration
@EnableDynamoDbRepositories(basePackageClasses = TournamentRepository.class)
public class DynamoDbConfig extends AbstractDynamoDbConfiguration {

    @Bean
    @Override
    public DynamoDbClient dynamoDbClient() {
        return DynamoDbClient.builder()
                .region(Region.EU_CENTRAL_1)
                .build();
    }
}

Define an entity:

@Table(tableName = "tournament_arena")
public class Tournament {
    @PartitionKey
    private String pk;

    @SortKey
    private String sk;

    private String name;
    private String season;
}

Create a repository:

public interface TournamentRepository extends DynamoDbRepository<Tournament, String> {
    List<Tournament> findByPk(String pk);
    List<Tournament> findByPkAndSkStartingWith(String pk, String prefix);
}

πŸ“ Example: Single-Table Design

One table, multiple entity types:

// Base table entity with composed sort key
@Table(tableName = "tournament_arena")
@SortKeyTemplate("MATCH#{matchDate}#{matchId}")
public class Match {
    @PartitionKey private String pk;        // "TOURNAMENT#winter2026"
    private LocalDate matchDate;            // 2026-02-01
    private String matchId;                 // "m1"
    // Physical sk: "MATCH#2026-02-01#m1"
    private String region;
}

// GSI view for querying by player
@SecondaryIndex(name = "GSI1", tableName = "tournament_arena")
public class PlayerMatchesView {
    @PartitionKey @Column("gsi1pk") private String collectionKey;  // "PLAYER#p1"
    @SortKey @Column("gsi1sk") private String itemKey;             // "MATCH#m1"
    private String region;
}

// Multi-attribute GSI with native composite keys
@SecondaryIndex("by_tournament_region")
public class MatchesByTournamentRegionView {
    @PartitionKey(order = 0) private String tournamentId;
    @PartitionKey(order = 1) private String region;

    @SortKey(order = 0) private String round;      // most general
    @SortKey(order = 1) private String bracket;
    @SortKey(order = 2) private String matchId;    // most specific
}

Polymorphic single-table container:

@Table(tableName = "tournament_arena")
public class ArenaItem {
    @PartitionKey @Column("pk") private String pk;
    @SortKey      @Column("sk") private String sk;

    @Column("gsi1pk") private String gsi1pk;
    @Column("gsi1sk") private String gsi1sk;

    @Embedded(startsWith = "TOURNAMENT#") private TournamentData tournament;
    @Embedded(startsWith = "PLAYER#")     private PlayerData     player;
    @Embedded(startsWith = "MATCH#")      private MatchData      match;
    @Embedded(startsWith = "RESULT#")     private ResultData     result;
}

Item collection view for grouped reads:

@ItemCollectionView(tableName = "single_table_demo", partitionKey = "pk", sortKey = "sk")
public class CustomerRow {
    @ItemCollectionMember(regex = "ORDER#[^#]+")
    private OrderData order;

    @ItemCollectionMember(regex = "ORDER#[^#]+#LINE#[^#]+")
    private List<OrderLineData> line;
}

public interface CustomerRepository extends ItemCollectionRepository<CustomerRow> {
}

Type-safe queries:

// Query base table
List<Match> matches = matchRepository.findByPk("TOURNAMENT#winter2026");

// Query GSI automatically β€” no indexName needed
List<PlayerMatchesView> playerMatches =
    playerMatchesRepository.findByCollectionKey("PLAYER#p1");

// Multi-attribute GSI query with derived method
List<MatchesByTournamentRegionView> matches =
    repository.findByTournamentIdAndRegionAndRound("winter2026", "NA-EAST", "SEMIFINALS");

// Explicit query with BETWEEN
@Query(keyConditionExpression = "#pk = :pk AND #sk BETWEEN :from AND :to",
       names = { @ExpressionName(name = "#pk", value = "gsi1pk"),
                 @ExpressionName(name = "#sk", value = "gsi1sk") })
List<MatchByDate> findInDateRange(@Param("pk") String pk,
                                  @Param("from") String from,
                                  @Param("to") String to);

Keyset pagination:

Window<PlayerMatchesView> page = repository.findWindowByCollectionKey(
    "PLAYER#p1", ScrollPosition.keyset(), Limit.of(25));

if (page.hasNext()) {
    ScrollPosition next = page.positionAt(page.size() - 1);
    Window<PlayerMatchesView> more =
        repository.findWindowByCollectionKey("PLAYER#p1", next, Limit.of(25));
}

Template API and IndexQueryBuilder:

// Fluent, validated query builder
EntityQueryResult<List<MatchesByTournamentRegionView>> result =
    operations.query(MatchesByTournamentRegionView.class, "by_tournament_region")
        .partition("tournamentId", "winter2026")
        .partition("region", "NA-EAST")
        .sortEq("round", "SEMIFINALS")
        .sortBeginsWith("bracket", "UP")
        .limit(50)
        .execute();

// Direct operations
operations.save(match);
operations.saveAll(List.of(m1, m2, m3));  // BatchWriteItem
operations.findById("TOURNAMENT#winter2026", "MATCH#m1", Match.class);

Single-item updates:

@Update(updateExpression = "SET #winner = :winner",
        conditionExpression = "attribute_exists(#pk)",
        names = { @ExpressionName(name = "#winner", value = "winner"),
                  @ExpressionName(name = "#pk", value = "pk") })
void recordWinner(@Param("pk") String pk, @Param("sk") String sk,
                  @Param("winner") String winner);

πŸ”§ What's Not Included

This module is intentionally focused on data access patterns:

  • ❌ No table creation β€” use CDK, Terraform, CloudFormation, or the AWS Console
  • ❌ No schema migration β€” DynamoDB is schema-less; indexes are infrastructure
  • ❌ No Page<T> or Slice<T> β€” DynamoDB cursors don't support offset-based pagination; use Window<T> instead
  • ❌ No automatic scanning β€” methods requiring a full-table scan must be explicitly annotated with @AllowScan
  • ❌ No transaction support β€” transactional operations are planned for a future release

πŸ›‘οΈ Fail-Fast Validation

Spring Data DynamoDB validates repository methods and entity mappings at application startup, not at runtime:

  • Invalid derived queries fail immediately if they don't match DynamoDB's key condition rules
  • Scan queries without @AllowScan are rejected to prevent accidental full-table scans
  • Entities with incorrect key mappings (multiple partition keys, incompatible annotations) fail at bootstrap
  • Invalid index names and key schema mismatches are caught before any query executes
  • Unsupported return types (Page<T>, Slice<T>) on repository methods are rejected during initialization

πŸ“‹ Core Annotations Reference

Annotation Purpose
@Table Marks a base-table entity with physical table name
@SecondaryIndex Marks a read-only GSI/LSI view with index name
@PartitionKey Partition-key component (supports order for multi-attribute keys)
@SortKey Sort-key component (supports order for multi-attribute keys)
@Column Maps property to a physical attribute name
@Embedded Flattens a nested object with startsWith/endsWith/regex routing
@ItemCollectionView Read-only view grouping partition rows into a single aggregate
@ItemCollectionMember Routes rows to item-collection fields by sort-key pattern
@SortKeyTemplate Composes sort key from multiple properties; decomposes on read
@Derived Property reconstructed from template, never written to DynamoDB
@Version Optimistic locking with conditional writes
@Query Explicit key condition, filter, or PartiQL expression
@Update Single-item update with update and condition expressions
@AllowScan Explicit opt-in for full-table scan methods
@ValueConverter Per-property custom conversion

πŸ™ Author

Matej Nedic (@MatejNedic)

πŸ”— Resources

πŸ“„ License

Apache License 2.0


Full Changelog: https://github.com/awspring/spring-data-dynamodb/commits/v1.0.0-ALPHA