Skip to content

Example application to integrate Java Spring with AWS DynamoDB

License

Notifications You must be signed in to change notification settings

daschaa/spring-dynamodb-example

Repository files navigation

Spring Boot + DynamoDB

This repository is an example on how to integrate DynamoDB with an Spring application. First thing is to setup a local DynamoDB instance to test things out.

Setup Local DynamoDB

To setup DynamoDB locally, you can create a Docker compose file with the amazon/dynamodb-local image.

version: '3.8'
services:
  dynamodb-local:
    command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
    image: "amazon/dynamodb-local:latest"
    container_name: dynamodb-local
    ports:
      - "8000:8000"
    volumes:
      - "./docker/dynamodb:/home/dynamodblocal/data"
    working_dir: /home/dynamodblocal

You can then spin it up with the command docker-compose up -d.

Create Spring App

The Spring Boot application can be created with the Spring Initializr https://start.spring.io/

Add DynamoDB support

To add DynamoDB support you first need to add some dependencies to the Gradle build file.

implementation 'com.amazonaws:aws-java-sdk-dynamodb:1.12.696'
implementation 'com.github.derjust:spring-data-dynamodb:5.1.0'

After you have added and loaded the dependencies, you can create a DynamoDBConfig configuration in your project.

@Configuration
@EnableDynamoDBRepositories(basePackages = "de.joshuaw.mmatechniques")
public class DynamoDBConfig {

  @Value("${amazon.dynamodb.endpoint}")
  private String amazonDynamoDBEndpoint;

  @Value("${amazon.aws.accesskey}")
  private String amazonAWSAccessKey;

  @Value("${amazon.aws.secretkey}")
  private String amazonAWSSecretKey;

  @Bean
  public AmazonDynamoDB amazonDynamoDB() {
    AmazonDynamoDBClientBuilder builder = AmazonDynamoDBClientBuilder.standard();
    builder.setCredentials(new AWSStaticCredentialsProvider(amazonAWSCredentials()));
    if (StringUtils.hasText(amazonDynamoDBEndpoint)) {
      builder.setEndpointConfiguration(
          new EndpointConfiguration(
              amazonDynamoDBEndpoint,
              Regions.EU_WEST_1.getName()
          )
      );
    }
    return builder.build();
  }

  @Bean
  public AWSCredentials amazonAWSCredentials() {
    return new BasicAWSCredentials(amazonAWSAccessKey, amazonAWSSecretKey);
  }

  @Bean(name = "mvcHandlerMappingIntrospectorCustom")
  public HandlerMappingIntrospector mvcHandlerMappingIntrospectorCustom() {
    return new HandlerMappingIntrospector();
  }
}

As you can see, there are some application properties that need to be set for the connection to the DynamoDB instance. In the src/main/resources/application.properties you need to set:

amazon.dynamodb.endpoint=http://localhost:8000/
amazon.aws.accesskey=key
amazon.aws.secretkey=key2

For production use, you would need to change this to point to the real DynamoDB endpoint and you would need to change the access to the Credentials from static to load it from environment variables.

With the configuration set, we can then create an entity.

Create a DynamoDB entity

When we want to create an entity, we need to configure it to be compatible with DynamoDB. We have to set the @DynamoDBTable annotation and we need to set which attribute is the @DynamoDBHashKey.

In this repository I have the following example

@NoArgsConstructor
@Setter
@DynamoDBTable(tableName = "Techniques")
public class Technique {
  private String title;
  private String id;
  private String description;
  private List<String> videoLinks;

  @DynamoDBHashKey
  @DynamoDBAutoGeneratedKey
  public String getId() {
    return id;
  }

  @DynamoDBAttribute
  public String getTitle() {
    return title;
  }

  @DynamoDBAttribute
  public String getDescription() {
    return description;
  }

  @DynamoDBAttribute
  public List<String> getVideoLinks() {
    return videoLinks;
  }

  public Technique(final String title) {
    this.title = title;
  }
}

Now that we have the entity, we can setup the repository.

Create the DynamoDB repository

Creating the repository is really simple. See the example from this repository.

package de.joshuaw.mmatechniques;

import java.util.Optional;
import org.socialsignin.spring.data.dynamodb.repository.EnableScan;
import org.springframework.data.repository.CrudRepository;

@EnableScan
public interface TechniqueRepository extends
    CrudRepository<Technique, String> {

  Optional<Technique> findById(String id);
}

Note that we use the @EnableScan annotation. That's all for setting it up!

Testing the DynamoDB repository

Now that we have set everything up, we want to test if the repository integration works.

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = MmaTechniquesApplication.class)
@WebAppConfiguration
@ActiveProfiles("local")
@TestPropertySource(properties = {
    "amazon.dynamodb.endpoint=http://localhost:8000/",
    "amazon.aws.accesskey=test1",
    "amazon.aws.secretkey=test231" })
public class TechniqueRepositoryIntegrationTest {
  private DynamoDBMapper dynamoDBMapper;

  @Autowired
  private AmazonDynamoDB amazonDynamoDB;

  @Autowired
  TechniqueRepository repository;

  @Before
  public void setup() throws Exception {
    dynamoDBMapper = new DynamoDBMapper(amazonDynamoDB);

    try {
      CreateTableRequest tableRequest = dynamoDBMapper
          .generateCreateTableRequest(Technique.class);
      tableRequest.setProvisionedThroughput(
          new ProvisionedThroughput(1L, 1L));
      amazonDynamoDB.createTable(tableRequest);
    } catch(final ResourceInUseException e) {
      // Do nothing because table exists
    }

    dynamoDBMapper.batchDelete(
        repository.findAll());
  }

  @Test
  public void givenItemWithTitle_whenRunFindAll_thenItemIsFound() {
    Technique technique = new Technique("Kimura");
    repository.save(technique);
    List<Technique> result = (List<Technique>) repository.findAll();

    assertThat(result.size(), is(greaterThan(0)));
  }
}

Additional stuff

I also included a small frontend and a controller in this example, to make it a "real world" example. You can check out the frontend in the frontend folder. It is a small React + Vite frontend.

The controller for the application looks like this:

@RestController
@RequiredArgsConstructor
public class TechniqueController {

  private final TechniqueRepository repository;

  @GetMapping("/techniques")
  public ResponseEntity<List<Technique>> getTechniques() {
    final List<Technique> list = new ArrayList<>();
    repository.findAll().forEach(list::add);
    return ResponseEntity.ok(list);
  }

  @GetMapping("/techniques/{id}")
  public ResponseEntity<Technique> getTechnique(@PathVariable String id) {
    final Optional<Technique> technique = repository.findById(id);
    return technique.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.noContent().build());
  }

  @PostMapping("/techniques")
  public ResponseEntity<String> addTechnique(@RequestBody Technique technique) {
    final String id = repository.save(technique).getId();
    return ResponseEntity.ok(id);
  }

  @PatchMapping("/techniques/{id}")
  public ResponseEntity<Technique> updateTechnique(@PathVariable String id, @RequestBody Technique techniqueUpdate) {
    final Optional<Technique> techniqueOptional = repository.findById(id);
    if(techniqueOptional.isPresent()) {
      final Technique technique = techniqueOptional.get();
      technique.setTitle(techniqueUpdate.getTitle());
      technique.setDescription(techniqueUpdate.getDescription());
      technique.setVideoLinks(techniqueUpdate.getVideoLinks());
      return ResponseEntity.ok(repository.save(technique));
    }
    return ResponseEntity.noContent().build();
  }

  @DeleteMapping("/techniques/{id}")
  public ResponseEntity<Technique> removeTechnique(@PathVariable String id) {
    final Optional<Technique> technique = repository.findById(id);
    if(technique.isPresent()) {
      repository.delete(technique.get());
      return ResponseEntity.ok().build();
    }
    return ResponseEntity.noContent().build();
  }
}

Credits

The original article that I used for this example was: https://www.baeldung.com/spring-data-dynamodb

I had to adjust some things to make it up to date and I added the controller and the frontend.