Implementing a Reactive App using Spring WebFlux and MongoDB
Step-by-step guide on how to implement Book API Reactive Spring Boot App
In this article, we will implement a Spring Boot Reactive application, with Spring WebFlux and Netty, called Book API. We will use Spring Data Reactive MongoDB to communicate with the MongoDB database. Book API will expose the following endpoints:
GET /api/books
GET /api/books/{id}
POST /api/books {"title": "...", "author": "...", "year": ...}
PATCH /api/books/{id} {"title": "...", "author": "...", "year": ...}
DELETE /api/books/{id}A Spring Boot Reactive app with MongoDB connection provides:
- Improved scalability through asynchronous, non-blocking operations;
- Lower latency and resource-efficient processing;
- Enhanced fault tolerance and higher throughput for concurrent requests;
- Optimized database interactions with a reactive MongoDB driver;
- Consistent programming model, ensuring future-proofing and alignment with event-driven architecture trends.
We will use the Book API as the base application in future articles where we will discuss how to implement unit or integration tests for the Book API, how to introduce caching, and more.
Without any further ado, let’s get started.
Prerequisites
If you would like to follow along, you must have Java 17+ and Docker installed on your machine.
Creating Book API Spring-Boot app
Let’s create a Spring-Boot application using Spring Initializr.
The application name will be book-api and the dependencies needed are: Spring Reactive Web, Spring Data Reactive MongoDB, Lombok and Validation.
We will use the Spring Boot version 3.2.0 and Java 17. Here is the link that contains all the setup mentioned previously.
Click the GENERATE button to download a zip file. Unzip the file to a preferred folder and then open the book-api project in your IDE.
Create some packages
In order to keep our code organized, let’s create the following packages, inside com.example.bookapi root package: controller, exception, mapper, model, repository and service.
Create the Model class
In model package, let’s create the Book class with the content below:
package com.example.bookapi.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "books")
public class Book {
@Id
private String id;
private String title;
private String author;
private Integer year;
public Book(String title, String author, Integer year) {
this.title = title;
this.author = author;
this.year = year;
}
}The Book class defines the data model for representing books in a MongoDB collection.
Create the Repository interface
In repository package, let’s create the BookRepository interface with the content below:
package com.example.bookapi.repository;
import com.example.bookapi.model.Book;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
public interface BookRepository extends ReactiveMongoRepository<Book, String> {
}The BookRepository interface serves as a MongoDB repository for the Book entity, leveraging the Spring Data MongoDB framework. It allows performing various database operations (such as saving, querying, updating, and deleting) on Book entities in a reactive way.
Create the Exception class
In exception package, let’s create the BookNotFoundException class with the content below:
package com.example.bookapi.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(String id) {
super("Book with id %s not found.".formatted(id));
}
}The BookNotFoundException class is thrown when a request is made for a book that doesn’t exist.
Create the Service interface and class
In service package, let’s create the BookService interface with the content below:
package com.example.bookapi.service;
import com.example.bookapi.model.Book;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface BookService {
Flux<Book> getBooks();
Mono<Book> validateAndGetBookById(String id);
Mono<Book> saveBook(Book book);
Mono<Void> deleteBook(Book book);
}Also, in service package, let’s create the implementation of the BookService interface:
package com.example.bookapi.service;
import com.example.bookapi.exception.BookNotFoundException;
import com.example.bookapi.model.Book;
import com.example.bookapi.repository.BookRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RequiredArgsConstructor
@Service
public class BookServiceImpl implements BookService {
private final BookRepository bookRepository;
@Override
public Flux<Book> getBooks() {
return bookRepository.findAll();
}
@Override
public Mono<Book> validateAndGetBookById(String id) {
return bookRepository.findById(id)
.switchIfEmpty(Mono.error(new BookNotFoundException(id)));
}
@Override
public Mono<Book> saveBook(Book book) {
return bookRepository.save(book);
}
@Override
public Mono<Void> deleteBook(Book book) {
return bookRepository.delete(book);
}
}The BookService interface and BookServiceImpl class are part of the service layer. They provide methods for managing books, including retrieving all books, validating and getting a book by ID, saving a book, and deleting a book.
The methods in the BookService and BookServiceImpl are designed to work reactively, utilizing Mono for handling single values and errors, and Flux for handling streams of values.
Create the DTO records
In controller package, let’s create a new package called dto. Inside it, we will create three DTO (Data Transfer Object) records, CreateBookRequest, UpdateBookRequest and BookResponse.
Starting CreateBookRequest record, create it with the content below:
package com.example.bookapi.controller.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
public record CreateBookRequest(
@NotBlank String title,
@NotBlank String author,
@Positive Integer year) {
}Then, let’s create the UpdateBookRequest record with the following content:
package com.example.bookapi.controller.dto;
public record UpdateBookRequest(String title, String author, Integer year) {
}Finally, let’s create the BookResponse record with the content below:
package com.example.bookapi.controller.dto;
import java.io.Serializable;
public record BookResponse(String id, String title, String author, Integer year) implements Serializable {
}These records are used in the controller class (that will be implemented in the next steps). The CreateBookRequest and the UpdateBookRequest define the expected structure of data in the request bodies for creating and updating book, respectively. The BookResponse defines the expected structure of data in the response.
Create the Mapper interface and class
In mapper package, let’s create the BookMapper interface with the content below:
package com.example.bookapi.mapper;
import com.example.bookapi.controller.dto.BookResponse;
import com.example.bookapi.controller.dto.CreateBookRequest;
import com.example.bookapi.controller.dto.UpdateBookRequest;
import com.example.bookapi.model.Book;
public interface BookMapper {
Book toBook(CreateBookRequest createBookRequest);
void updateBookFromUpdateBookRequest(UpdateBookRequest updateBookRequest, Book book);
BookResponse toBookResponse(Book book);
}Also, in mapper package, let’s create the implementation of the BookMapper interface:
package com.example.bookapi.mapper;
import com.example.bookapi.controller.dto.BookResponse;
import com.example.bookapi.controller.dto.CreateBookRequest;
import com.example.bookapi.controller.dto.UpdateBookRequest;
import com.example.bookapi.model.Book;
import org.springframework.stereotype.Service;
@Service
public class BookMapperImpl implements BookMapper {
@Override
public Book toBook(CreateBookRequest createBookRequest) {
if (createBookRequest == null) {
return null;
}
Book book = new Book();
book.setTitle(createBookRequest.title());
book.setAuthor(createBookRequest.author());
book.setYear(createBookRequest.year());
return book;
}
@Override
public void updateBookFromUpdateBookRequest(UpdateBookRequest updateBookRequest, Book book) {
if (updateBookRequest == null) {
return;
}
if (updateBookRequest.title() != null) {
book.setTitle(updateBookRequest.title());
}
if (updateBookRequest.author() != null) {
book.setAuthor(updateBookRequest.author());
}
if (updateBookRequest.year() != null) {
book.setYear(updateBookRequest.year());
}
}
@Override
public BookResponse toBookResponse(Book book) {
if (book == null) {
return null;
}
return new BookResponse(book.getId(), book.getTitle(), book.getAuthor(), book.getYear());
}
}The BookMapper interface and BookMapperImpl class are responsible to perform some mapping from the DTO classes CreateBookRequest and UpdateBookRequest to the Book MongoDB document class. Also, to map from Book to BookResponse.
Create the Controller class
In controller package, let’s create the BookController class with the content below:
package com.example.bookapi.controller;
import com.example.bookapi.controller.dto.BookResponse;
import com.example.bookapi.controller.dto.CreateBookRequest;
import com.example.bookapi.controller.dto.UpdateBookRequest;
import com.example.bookapi.mapper.BookMapper;
import com.example.bookapi.model.Book;
import com.example.bookapi.service.BookService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
private final BookMapper bookMapper;
@GetMapping(produces = MediaType.APPLICATION_NDJSON_VALUE)
public Flux<BookResponse> getBook() {
return bookService.getBooks().map(bookMapper::toBookResponse);
}
@GetMapping("/{id}")
public Mono<BookResponse> getBook(@PathVariable String id) {
return bookService.validateAndGetBookById(id).map(bookMapper::toBookResponse);
}
@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public Mono<BookResponse> createBook(@Valid @RequestBody CreateBookRequest createBookRequest) {
Book book = bookMapper.toBook(createBookRequest);
return bookService.saveBook(book).map(bookMapper::toBookResponse);
}
@PatchMapping("/{id}")
public Mono<BookResponse> updateBook(@PathVariable String id,
@RequestBody UpdateBookRequest updateBookRequest) {
return bookService.validateAndGetBookById(id)
.doOnSuccess(book -> {
bookMapper.updateBookFromUpdateBookRequest(updateBookRequest, book);
bookService.saveBook(book).subscribe();
})
.map(bookMapper::toBookResponse);
}
@DeleteMapping("/{id}")
public Mono<BookResponse> deleteBook(@PathVariable String id) {
return bookService.validateAndGetBookById(id)
.doOnSuccess(book -> bookService.deleteBook(book).subscribe())
.map(bookMapper::toBookResponse);
}
}The BookControllerclass is a Spring WebFlux controller that handles HTTP requests related to books in a reactive style using Mono and Flux from the Reactor library. It provides endpoints to get, create, update, and delete books using bookService and bookMapper.
Update the application.properties
Let’s update the application.properties file with the content below:
spring.application.name=book-api
spring.data.mongodb.uri=mongodb://${MONGODB_HOST:localhost}:${MONGODB_PORT:27017}/bookdb
logging.level.org.springframework.data.mongodb.core=DEBUGA shorter explanation of the properties:
spring.application.name: Specifies the name of the application;spring.data.mongodb.uri: Specifies the connection URI for MongoDB;logging.level.org.springframework.data.mongodb.core: Sets the logging level for the Spring Data MongoDB core component.
Starting Environment
Start MongoDB
In a terminal, run the following command to start the MongoDB Docker container:
docker run -d --name mongodb -p 27017:27017 mongo:7.0.4
Start Book API
Next, make sure you and inside the book-api root folder. Then, run the command below to start Book API:
./mvnw clean spring-boot:run
Testing Endpoints
In a terminal, let’s run the following cURL commands to interact with Book API endpoints.
Let’s retrieve all books:
curl -i localhost:8080/api/books
As there is no book registered, we should get:
HTTP/1.1 200 OK transfer-encoding: chunked Content-Type: application/x-ndjson
Let’s create a book:
curl -i -X POST localhost:8080/api/books \
-H 'Content-Type: application/json' \
-d '{"title": "Spring in Action", "author": "Craig Walls and Ryan Breidenbach", "year": 1005}'It should return:
HTTP/1.1 201 Created
Content-Type: application/json
...
{"id":"657622259ba4516799bfc290","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":1005}Note: In my case, the book ID is
657622259ba4516799bfc290. For you, it will be different. In order to facilitate the writing of this article, let’s export to an environment variable the book ID. For it, run the following command in the terminal where you are performing the calls to Book API endpoints:
BOOK_ID=<the-created-book-id>
Again, let’s retrieve all books:
curl -i localhost:8080/api/books
This time, it should return:
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/x-ndjson
{"id":"657622259ba4516799bfc290","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":1005}Let’s update the book year, as it should be 2005 and not 1005:
curl -i -X PATCH localhost:8080/api/books/$BOOK_ID \
-H 'Content-Type: application/json' \
-d '{"year": 2005}'We should get:
HTTP/1.1 200 OK
Content-Type: application/json
...
{"id":"657622259ba4516799bfc290","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":2005}Let’s retrieve the book:
curl -i localhost:8080/api/books/$BOOK_IDWe should get as response:
HTTP/1.1 200 OK
Content-Type: application/json
...
{"id":"657622259ba4516799bfc290","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":2005}Finally, let’s delete the book:
curl -i -X DELETE localhost:8080/api/books/$BOOK_IDThe response should be:
HTTP/1.1 200 OK
Content-Type: application/json
...
{"id":"657622259ba4516799bfc290","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":2005}In case, we try to retrieve a book that does not exist:
curl -i localhost:8080/api/books/$BOOK_IDThe following response should be returned:
HTTP/1.1 404 Not Found
Content-Type: application/json
...
{"timestamp":"2023-12-10T20:42:54.044+00:00","path":"/api/books/657622259ba4516799bfc290","status":404,"error":"Not Found","requestId":"f41a3c79-8"}Shutdown
In the terminal where the Book API is running, press Ctrl+C to stop the application.
To stop the MongoDB Docker container, run the following command in a terminal:
docker rm -fv mongodbConclusion
In this article, we created a Spring Boot Reactive application named Book API, employing Spring WebFlux and Netty, and establishing a connection to MongoDB via Spring Data Reactive MongoDB.
This reactive Spring Boot app coupled with MongoDB brings several advantages: enhanced scalability achieved through asynchronous, non-blocking operations; reduced latency and resource-efficient processing; optimized database interactions facilitated by a reactive MongoDB driver; and more.
To ensure the application performs as anticipated, we conclude by testing its endpoints for a seamless and expected execution.
Additional Readings
Support and Engagement
If you enjoyed this article and would like to show your support, please consider taking the following actions:
- 👏 Engage by clapping, highlighting, and replying to my story. I’ll be happy to answer any of your questions;
- 🌐 Share my story on Social Media;
- 🔔 Follow me on: Medium | LinkedIn | Twitter | GitHub;
- ✉️ Subscribe to my newsletter, so you don’t miss out on my latest posts.


