Immutables

DynamoDB integration

Overview

The DynamoDB Enhanced Client, part of the AWS V2 SDK, offers a straightforward way to map client-side classes to DynamoDB tables and perform CRUD operations.

Style for Immutable Table Mapping Classes

For example, instances of a Customer class can map to a row in a customers DynamoDB table.

The enhanced client supports mappings for immutable classes, which is possible with the following annotations (starting in immutables version 2.10):

import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;

@Value.Immutable
@DynamoDbImmutable(builder = ImmutableCustomer.Builder.class)
@Value.Style(
    from = "", // Omit the from(*) methods so that they aren't interpreted as table attributes
    defaults = @Value.Immutable(copy = false), // Omit the copy(*) methods so that they aren't interpreted as table attribute
    builtinContainerAttributes = false, // Omit the add(*) methods so that they aren't interpreted as table attributes
    passAnnotations = {
        // Copy all Enhanced Client annotations to the immutable class
        DynamoDbImmutable.class,
        DynamoDbAttribute.class,
        DynamoDbConvertedBy.class,
        DynamoDbFlatten.class,
        DynamoDbIgnore.class,
        DynamoDbIgnoreNulls.class,
        DynamoDbPartitionKey.class,
        DynamoDbPreserveEmptyObject.class,
        DynamoDbSecondaryPartitionKey.class,
        DynamoDbSecondarySortKey.class,
        DynamoDbSortKey.class,
        DynamoDbUpdateBehavior.class,
        DynamoDbAtomicCounter.class,
        DynamoDbAutoGeneratedTimestampAttribute.class,
        DynamoDbVersionAttribute.class,
    }
)
public interface Customer {

  // Partition key attribute named "customerId"
  @DynamoDbPartitionKey
  String getCustomerId();

  //Sort key attribute named "email"
  @DynamoDbSortKey
  String getEmail();

  //"name" attribute
  String getName();

  //An optional "occupation" attribute. The @Nullable annotation is used because the Enhanced Client does not support java.util.Optional
  @Nullable
  String getOccupation();
}

Then to persist the item:

DynamoDbEnhancedClient client = ...

//Initialize the schema and table mapping
TableSchema<ImmutableCustomer> customerSchema = TableSchema.fromImmutableClass(ImmutableCustomer.class);
DynamoDbTable<ImmutableCustomer> customerTable = client.table("customers", customerSchema);

// Build and persist the customer
ImmutableCustomer customer = ImmutableCustomer.builder()
    .customerId("customer123")
    .email("example@email.com")
    .name("John")
    .build();
customerTable.putItem(customer);

// Retrieve the customer
Key lookupKey = Key.builder().partitionValue("customer123").sortValue("example@email.com").build();
ImmutableCustomer retrievedCustomer = customerTable.getItem(lookupKey);

Immutable Partial Updates

When updating an item, the Enhanced Client’s interface takes an instance of the table mapping class as input, but allows you to set attribute values to null to indicate they shouldn’t be modified. For example, you may want to atomically increment a counter attribute without overwriting other attributes with potentially stale values.

Ideally partial updates are possible without making all attributes optional in the table mapping class. This is possible by extending the mapping class and disabling immutables validation of required fields:

@Value.Immutable
@SuppressWarnings("immutables:subtype")
@DynamoDbImmutable(builder = ImmutablePartialCustomer.Builder.class)
@Value.Style(
    // Disable required field validation
    validationMethod = ValidationMethod.NONE,

    from = "",
    builtinContainerAttributes = false,
    defaults = @Value.Immutable(copy = false),
    passAnnotations = {
      // Same annotations as in Customer
    }
)
public interface PartialCustomer extends Customer { 
  
}

And then to make a partial update:

// Initialize the schema and table mappings for the partial class items. The same "customers" table is used as the non-partial mapping.
TableSchema<ImmutablePartialCustomer> partialCustomerSchema = TableSchema.fromImmutableClass(ImmutablePartialCustomer.class);
DynamoDbTable<ImmutablePartialCustomer> partialCustomerTable = client.table("customers", partialCustomerSchema);

// Update just the occupation attribute, not the required "name" attribute
ImmutablePartialCustomer partialUpdate = ImmutablePartialCustomer.builder()
  .customerId("customer123")
  .email("example@email.com")
  .occupation("software developer")
  .build();

// ignoreNulls(true) only updates non-null attributes
partialCustomerTable.updateItem(request -> request.ignoreNulls(true).item(partialUpdate));

Code re-use

Custom annotations can be created to re-use the above styles across tables:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@Value.Style(
    from = "",
    builtinContainerAttributes = false,
    defaults = @Value.Immutable(copy = false),
    passAnnotations = {
        DynamoDbImmutable.class,
        DynamoDbAttribute.class,
        DynamoDbConvertedBy.class,
        DynamoDbFlatten.class,
        DynamoDbIgnore.class,
        DynamoDbIgnoreNulls.class,
        DynamoDbPartitionKey.class,
        DynamoDbPreserveEmptyObject.class,
        DynamoDbSecondaryPartitionKey.class,
        DynamoDbSecondarySortKey.class,
        DynamoDbSortKey.class,
        DynamoDbUpdateBehavior.class,
        DynamoDbAtomicCounter.class,
        DynamoDbAutoGeneratedTimestampAttribute.class,
        DynamoDbVersionAttribute.class,
    }
)
public @interface ImmutablesDynamoDBStyle {}

@Value.Immutable
@ImmutablesDynamoDBStyle
@DynamoDbImmutable(builder = ImmutableCustomer.Builder.class)
public interface Customer { ... }

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@Value.Style(
    validationMethod = ValidationMethod.NONE,
    from = "",
    builtinContainerAttributes = false,
    defaults = @Value.Immutable(copy = false),
    passAnnotations = {
        DynamoDbImmutable.class,
        DynamoDbAttribute.class,
        DynamoDbConvertedBy.class,
        DynamoDbFlatten.class,
        DynamoDbIgnore.class,
        DynamoDbIgnoreNulls.class,
        DynamoDbPartitionKey.class,
        DynamoDbPreserveEmptyObject.class,
        DynamoDbSecondaryPartitionKey.class,
        DynamoDbSecondarySortKey.class,
        DynamoDbSortKey.class,
        DynamoDbUpdateBehavior.class,
        DynamoDbAtomicCounter.class,
        DynamoDbAutoGeneratedTimestampAttribute.class,
        DynamoDbVersionAttribute.class,
    }
)
public @interface ImmutablesDynamoDBPartialStyle {}

@Value.Immutable
@ImmutablesDynamoDBPartialStyle
@SuppressWarnings("immutables:subtype")
@DynamoDbImmutable(builder = ImmutablePartialCustomer.Builder.class)
public interface PartialCustomer extends Customer { }

Legacy AWS V1 SDK Guide

A guide for V1 of the SDK using DynamoDbMapper can be found here