Reactive REST API with AWS DynamoDB and Spring WebFlux

Eric Anicet
5 min readMay 29


In this story, we’re going to implement a reactive REST API using Spring WebFlux and AWS DynamoDB.

· Prerequisites
· Setting Up DynamoDB in AWS Console
· Spring WebFlux API
Creating the Configuration
Creating the Mapping Class
Creating the Repository Classes
Controller class
· Test the REST API:
· Conclusion
· References


This is the list of all the prerequisites for following this story:

  • Java 17
  • Starter WebFlux 3.1.0
  • Maven 3.6.3
  • An active AWS account.
  • Optionally, LocalStack to run Dynamodb locally
  • Postman or Insomnia

Setting Up DynamoDB in AWS Console

  1. Log in to the AWS Management Console and open the DynamoDB service.
  2. Create Table. Add the table name and the primary key. (For this story we are using all other default settings)

3. Create an IAM user to access the DynamoDB tables structure. We need to access DynamoDB programmatically using an access key and a secret access key.

We can also use Localstack to set up DynamoDb locally. It provides an easy-to-use test/mocking framework for developing Cloud applications.
It allows us to Test and debug AWS Cloud Resource Locally.

Spring WebFlux API

Let’s start by creating a simple Spring Reactive project from, with the following dependencies: Spring Reactive Web and Lombok.

Creating the Configuration

First, let us include the DynamoDB Enhanced Client dependency in the pom.xml file.

The DynamoDB enhanced client is a high-level library that is part of the AWS SDK for Java 2.x. It offers a straightforward way to map client-side classes to DynamoDB tables.


The second important step is to add the aws credentials to connect to AWS DynamoDB in the properties file.

# AWS properties
access-key: <YOUR ACCESS KEY>
secret-key: <SECRET ACCESS KEY>
region: eu-south-2

Create a DynamoDbConfig configuration class to initialize the DynamoDbEnhancedAsyncClient bean.

DynamoDbEnhancedAsyncClient is an asynchronous interface for running commands against a DynamoDb database.

public class DynamoDbConfig {

private final AwsConfig config;

public DynamoDbAsyncClient dynamoDbAsyncClient(){
return DynamoDbAsyncClient.builder().credentialsProvider(StaticCredentialsProvider
.create(AwsBasicCredentials.create(config.getAccessKey(), config.getSecretKey())))

public DynamoDbEnhancedAsyncClient dynamoDbEnhancedAsyncClient() {
return DynamoDbEnhancedAsyncClient.builder()


Creating the Mapping Class

Let us now create the Book and Author classes to represent book and author tables in DynamoDB. At a minimum we must annotate the class so that it can be used as a DynamoDb bean, and also the property that represents the primary partition key of the table.

The following Book class shows these annotations that will link the class definition to the DynamoDB table.

public class Book {

@Getter(onMethod=@__({@DynamoDbPartitionKey, @DynamoDbAttribute("id")}))
private String id;

private String title;

private Integer page;

private String isbn;

private String description;

private String language;

private Double price;

The attribute primary partition key must map to a DynamoDb scalar type (string, number, or binary) to be valid. Every mapped table schema must have exactly one of these.

Creating the Repository Classes

Let’s create the Repository class which will interact with the book table to perform CRUD operations.

public class BookRepository {

public static final String TABLE_NAME = "book";

private final DynamoDbAsyncTable<Book> bookTable;

public BookRepository(DynamoDbEnhancedAsyncClient dynamoDbClient) {
bookTable = dynamoDbClient
.table(TABLE_NAME, TableSchema.fromBean(Book.class));

public Flux<Book> findAll() {
return Flux.from(bookTable.scan().items());

public Mono<Book> findById(String id) {
return Mono.fromFuture(bookTable.getItem(getKeyBuild(id)));

public Mono<Book> delete(String id) {
return Mono.fromCompletionStage(bookTable.deleteItem(getKeyBuild(id)));

public Mono<Integer> count() {
ScanEnhancedRequest scanEnhancedRequest = ScanEnhancedRequest.builder().addAttributeToProject("id").build();
AtomicInteger counter = new AtomicInteger(0);
return Flux.from(bookTable.scan(scanEnhancedRequest))
.doOnNext(page -> counter.getAndAdd(page.items().size()))
.then(Mono.defer(() -> Mono.just(counter.get())));

public Mono<Book> update(Book entity) {
var updateRequest = UpdateItemEnhancedRequest.builder(Book.class).item(entity).build();
return Mono.fromCompletionStage(bookTable.updateItem(updateRequest));

public Mono<Book> save(Book entity){

var putRequest = PutItemEnhancedRequest.builder(Book.class).item(entity).build();
return Mono.fromCompletionStage(bookTable.putItem(putRequest).thenApply(x -> entity));

private Key getKeyBuild(String id) {
return Key.builder().partitionValue(id).build();

We used the DynamoDbEnhancedAsyncClient bean in the repository class to perform async database operations.

Controller class

public class AuthorController {

private final AuthorRepository repository;

public AuthorController(AuthorRepository repository) {
this.repository = repository;

public Mono<ApiResponse> getAllAuthors() {
return repository.findAll()
.map(authors -> new ApiResponse(authors, MessageFormat.format("{0} result found", authors.size())));

public Mono<ApiResponse> authorCount() {
return repository.count()
.map(count -> new ApiResponse(count, MessageFormat.format("Count authors: {0}", count)));

public Mono<ApiResponse> getByAuthorId(@PathVariable String id) {
return repository.findById(id)
.map(book -> new ApiResponse(book, MessageFormat.format("Result found", book)))
.defaultIfEmpty(new ApiResponse(null, "Author not found"));

public Mono<ApiResponse> create(@RequestBody Mono<Author> author) {
return author
.map(author1 -> new ApiResponse(author1, "Author successfully created"));
public Mono<ApiResponse> update(@PathVariable String id, @RequestBody Mono<Author> author) {
return author
.map(author1 -> {
return author1;
.map(authorUpdated -> new ApiResponse(authorUpdated, "Author successfully updated"));
public Mono<ApiResponse> update(@PathVariable String id) {
return repository.delete(id)
.map(authorDeleted -> new ApiResponse(authorDeleted, "Author successfully deleted"));

Test the REST API:

Run the Application.

  • POST /book
POST http://localhost:8081/book
  • GET all
GET http://localhost:8081/book
  • Count item
GET http://localhost:8081/book/count


Well done !!.

The complete source code is available on GitHub.

If you enjoyed this story, please give it a few claps for support.

Happy coding!




Eric Anicet