Getting Started with DynamoDB and Spring

Introduction

DynamoDB is a NoSQL database provided by AWS, and in the same way as MongoDB or Cassandra, it is very suitable to boost horizontal scalability and increase development speed.

Main Features

DynamoDB Components

Guidelines

The official DynamoDB documentation for best practices provides more information.

However, DynamoDB is not for everyone. In NoSQL, you are tuning the data to the access particular patterns. Therefore, if you do not know what queries are going to be performed, or your system is very normalized, NoSQL databases might not be a good candidate.

Spring Integration

One of the languages supported by DynamoDB is Java, and you can develop you own API to query DynamoDB. However, you will realize soon that there is a lot of boilerplate for very simple queries.

Spring provides a community module to integrate DynamoDB on Spring, which is built on top of Spring Data. This module supports:

Getting Started

To start using this module, you just need to add these two dependencies:

spring-data-dynamodb aws-java-sdk-dynamodb

<profiles>
    <profile>
        <id>allow-snapshots</id>
        <activation><activeByDefault>true</activeByDefault></activation>
        <repositories>
        <repository>
            <id>snapshots-repo</id>
            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
            <releases><enabled>false</enabled></releases>
            <snapshots><enabled>true</enabled></snapshots>
        </repository>
        </repositories>
    </profile>
</profiles>

DynamoDB Set Up

First of all, you need a DynamoDB web service instance on AWS or run locally a downloadable version of DynamoDB. For testing, you can simply use a dockerized DynamoDB version:

docker run -p 8000:8000 amazon/dynamodb-local

Spring Configuration

Firstly, you need to add @EnableDynamoDBRepositories annotation on our configuration class. And you can have two Spring configuration classes, one for testing and another for production.

@Profile("dev")
@Configuration
@EnableDynamoDBRepositories(basePackages = "org.smartinrubio.springbootdynamodb.repository")
public class DynamoDBConfigDev {
    @Value("${amazon.dynamodb.endpoint}")
    private String amazonDynamoDBEndpoint;
    @Value("${amazon.dynamodb.region}")
    private String amazonDynamoDBRegion;
    @Bean
    public DynamoDBMapperConfig dynamoDBMapperConfig() {
        return DynamoDBMapperConfig.DEFAULT;
    }
    @Bean
    public DynamoDBMapper dynamoDBMapper(AmazonDynamoDB amazonDynamoDB, DynamoDBMapperConfig config) {
        return new DynamoDBMapper(amazonDynamoDB, config);
    }
    @Bean
    public AmazonDynamoDB amazonDynamoDB() {
        return AmazonDynamoDBClientBuilder
                .standard()
                .withEndpointConfiguration(
                        new AwsClientBuilder
                                .EndpointConfiguration(amazonDynamoDBEndpoint, amazonDynamoDBRegion))
                .build();
    }
    @Bean
    public DynamoDB dynamoDB() {
        return new DynamoDB(amazonDynamoDB());
    }
}

The first bean (DynamoDBMapperConfig) is created to override default behaviors so you can set things like types of loading data (LAZY_LOADING, EAGER_LOADING, ITERATION_ONLY), read configuration (EVENTUAL (default), CONSISTENT), or how the mapper should deal with attributes during save operations (UPDATE, CLOBBER). More info.

AmazonDynamoDB bean configures the connection with your DynamoDB host to perform query operations.

Lastly, DynamoDB bean was created to provide another instance with more refined query operations.

Additionally, in case you want to point to the web version, you will need to set up:

@Profile("prod")
@Configuration
@EnableDynamoDBRepositories(basePackages = "org.smartinrubio.springbootdynamodb.repository")
public class DynamoDBConfigProd {
    @Value("${amazon.dynamodb.accesskey}")
    private String amazonDynamoDBAccessKey;
    @Value("${amazon.dynamodb.secretkey}")
    private String amazonDynamoDBSecretKey;
    @Bean
    public AWSCredentials amazonAWSCredentials() {
        return new BasicAWSCredentials(amazonDynamoDBAccessKey, amazonDynamoDBSecretKey);
    }
    @Bean
    public DynamoDBMapperConfig dynamoDBMapperConfig() {
        return DynamoDBMapperConfig.DEFAULT;
    }
    @Bean
    public DynamoDBMapper dynamoDBMapper(AmazonDynamoDB amazonDynamoDB, DynamoDBMapperConfig config) {
        return new DynamoDBMapper(amazonDynamoDB, config);
    }
    @Bean
    public AmazonDynamoDB amazonDynamoDB() {
        return AmazonDynamoDBClientBuilder
                .standard()
                .withCredentials(amazonAWSCredentialsProvider())
                .withRegion(Regions.US_WEST_2)
                .build();
    }
    @Bean
    public DynamoDB dynamoDB() {
        return new DynamoDB(amazonDynamoDB());
    }
    public AWSCredentialsProvider amazonAWSCredentialsProvider() {
        return new AWSStaticCredentialsProvider(amazonAWSCredentials());
    }
}

Repositories

Repositories need to be annotated with @EnableScan to use method name as queries.

@EnableScan
public interface HotelRepository extends CrudRepository<Hotel, String> , CustomHotelRepository{
    List<Hotel> findAllByName(String name);
}
And as you can see on the previous code snippet, method query operations like findAllByName are allowed.

For custom queries we can create an interface to support the additional queries.

public interface CustomHotelRepository {
    void createTable();
    void loadData() throws IOException;
}

Entities

DynamoDBMapper allows you to convert DynamoDB items to POJOs, and generate table definitions.

@Data
@DynamoDBTable(tableName = "Hotels")
public class Hotel {
    @DynamoDBHashKey
    @DynamoDBGeneratedUuid(DynamoDBAutoGenerateStrategy.CREATE)
    private String id;
    @DynamoDBAttribute
    private String name;
    @DynamoDBAttribute
    @DynamoDBTypeConverted(converter = GeoTypeConverter.class)
    private Geo geo;
}

For this particular example, the Partition Key is the field annotated with @DynamoDBHashKey, which will be generated randomly.

In case you have inner objects, you will need to use @DynamoDBTypeConverted with a converter since this Spring module is not able to serialize objects by itself and only allows strings as values.

There are also other useful annotations such as @DynamoDBIndexRangeKey for secondary indexes on a DynamoDB table.

Source Code

Conclusion