Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
Java
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Languageschevron-right
Javachevron-right

Reactive Java Spring Boot with MongoDB

Teo Wen Jie5 min read • Published Apr 02, 2024 • Updated Apr 02, 2024
SpringMongoDBJava
FULL APPLICATION
Facebook Icontwitter iconlinkedin icon
Rate this code example
star-empty
star-empty
star-empty
star-empty
star-empty
social-githubView Code

Introduction

Spring Boot + Reactive + Spring Data + MongoDB. Putting these four technologies together can be a challenge, especially if you are just starting out. Without getting into details of each of these technologies, this tutorial aims to help you get a jump start on a working code base based on this technology stack. This tutorial features:
  • Interacting with MongoDB using ReactiveMongoRepositories.
  • Interacting with MongoDB using ReactiveMongoTemplate.
  • Wrapping queries in a multi-document ACID transaction.
This simplified cash balance application allows you to make REST API calls to:
  • Create or fetch an account.
  • Perform transactions on one account or between two accounts.

GitHub repository

Access the repository README for more details on the functional specifications. The README also contains setup, API usage, and testing instructions. To clone the repository:
1git clone git@github.com:mongodb-developer/mdb-spring-boot-reactive.git

Code walkthrough

Let's do a logical walkthrough of how the code works. I would include code snippets, but to reduce verbosity, I will exclude lines of code that are not key to our understanding of how the code works.

Creating or fetching an account

This section showcases how you can perform Create and Read operations with ReactiveMongoRepository.
The API endpoints to create or fetch an account can be found in AccountController.java:
1@RestController
2public class AccountController {
3 //...
4 @PostMapping("/account")
5 public Mono<Account> createAccount(@RequestBody Account account) {
6 return accountRepository.save(account);
7 }
8
9 @GetMapping("/account/{accountNum}")
10 public Mono<Account> getAccount(@PathVariable String accountNum) {
11 return accountRepository.findByAccountNum(accountNum).switchIfEmpty(Mono.error(new AccountNotFoundException()));
12 }
13 //...
14}
This snippet shows two endpoints:
  • A POST method endpoint that creates an account
  • A GET method endpoint that retrieves an account but throws an exception if it cannot be found
They both simply return a Mono<Account> from AccountRepository.java, a ReactiveMongoRespository interface which acts as an abstraction from the underlying Reactive Streams Driver.
  • .save(...) method creates a new document in the accounts collection in our MongoDB database.
  • .findByAccountNum() method fetches a document that matches the accountNum.
1public interface AccountRepository extends ReactiveMongoRepository<Account, String> {
2
3 @Query("{accountNum:'?0'}")
4 Mono<Account> findByAccountNum(String accountNum);
5 //...
6}
The @Query annotation allows you to specify a MongoDB query with placeholders so that it can be dynamically substituted with values from method arguments. ?0 would be substituted by the value of the first method argument and ?1 would be substituted by the second, and so on and so forth.
The built-in query builder mechanism can actually determine the intended query based on the method's name. In this case, we could actually exclude the @Query annotation but I left it there for better clarity and to illustrate the previous point.
Notice that there is no need to declare a save(...) method even though we are actually using accountRepository.save() in AccountController.java. The save(...) method, and many other base methods, are already declared by interfaces up in the inheritance chain of ReactiveMongoRepository.

Debit, credit, and transfer

This section showcases:
  • Update operations with ReactiveMongoRepository.
  • Create, Read, and Update operations with ReactiveMongoTemplate.
Back to AccountController.java:
1@RestController
2public class AccountController {
3 //...
4 @PostMapping("/account/{accountNum}/debit")
5 public Mono<Txn> debitAccount(@PathVariable String accountNum, @RequestBody Map<String, Object> requestBody) {
6 //...
7 txn.addEntry(new TxnEntry(accountNum, amount));
8 return txnService.saveTransaction(txn).flatMap(txnService::executeTxn);
9 }
10
11 @PostMapping("/account/{accountNum}/credit")
12 public Mono<Txn> creditAccount(@PathVariable String accountNum, @RequestBody Map<String, Object> requestBody) {
13 //...
14 txn.addEntry(new TxnEntry(accountNum, -amount));
15 return txnService.saveTransaction(txn).flatMap(txnService::executeTxn);
16 }
17
18 @PostMapping("/account/{from}/transfer")
19 public Mono<Txn> transfer(@PathVariable String from, @RequestBody TransferRequest transferRequest) {
20 //...
21 txn.addEntry(new TxnEntry(from, -amount));
22 txn.addEntry(new TxnEntry(to, amount));
23 //save pending transaction then execute
24 return txnService.saveTransaction(txn).flatMap(txnService::executeTxn);
25 }
26 //...
27}
This snippet shows three endpoints:
  • A .../debit endpoint that adds to an account balance
  • A .../credit endpoint that subtracts from an account balance
  • A .../transfer endpoint that performs a transfer from one account to another
Notice that all three methods look really similar. The main idea is:
  • A Txn can consist of one to many TxnEntry.
  • A TxnEntry is a reflection of a change we are about to make to a single account.
  • A debit or credit Txn will only have one TxnEntry.
  • A transfer Txn will have two TxnEntry.
  • In all three operations, we first save one record of the Txn we are about to perform, and then make the intended changes to the target accounts using the TxnService.java.
1@Service
2public class TxnService {
3 //...
4 public Mono<Txn> saveTransaction(Txn txn) {
5 return txnTemplate.save(txn);
6 }
7
8 public Mono<Txn> executeTxn(Txn txn) {
9 return updateBalances(txn)
10 .onErrorResume(DataIntegrityViolationException.class
11 /*lambda expression to handle error*/)
12 .onErrorResume(AccountNotFoundException.class
13 /*lambda expression to handle error*/)
14 .then(txnTemplate.findAndUpdateStatusById(txn.getId(), TxnStatus.SUCCESS));
15 }
16
17 public Flux<Long> updateBalances(Txn txn) {
18 //read entries to update balances, concatMap maintains the sequence
19 Flux<Long> updatedCounts = Flux.fromIterable(txn.getEntries()).concatMap(
20 entry -> accountRepository.findAndIncrementBalanceByAccountNum(entry.getAccountNum(), entry.getAmount())
21 );
22 return updatedCounts.handle(/*...*/);
23 }
24}
The updateBalances(...) method is responsible for iterating through each TxnEntry and making the corresponding updates to each account. This is done by calling the findAndIncrementBalanceByAccountNum(...) method in AccountRespository.java.
1public interface AccountRepository extends ReactiveMongoRepository<Account, String> {
2 //...
3 @Update("{'$inc':{'balance': ?1}}")
4 Mono<Long> findAndIncrementBalanceByAccountNum(String accountNum, double increment);
5}
Similar to declaring find methods, you can also declare Data Manipulation Methods in the ReactiveMongoRepository, such as update methods. Once again, the query builder mechanism is able to determine that we are interested in querying by accountNum based on the naming of the method, and we define the action of an update using the @Update annotation. In this case, the action is an $inc and notice that we used ?1 as a placeholder because we want to substitute it with the value of the second argument of the method.
Moving on, in TxnService we also have:
  • A saveTransaction method that saves a Txn document into transactions collection.
  • A executeTxn method that calls updateBalances(...) and then updates the transaction status in the Txn document created.
Both utilize the TxnTemplate that contains a ReactiveMongoTemplate.
1@Service
2public class TxnTemplate {
3 //...
4 public Mono<Txn> save(Txn txn) {
5 return template.save(txn);
6 }
7
8 public Mono<Txn> findAndUpdateStatusById(String id, TxnStatus status) {
9 Query query = query(where("_id").is(id));
10 Update update = update("status", status);
11 FindAndModifyOptions options = FindAndModifyOptions.options().returnNew(true);
12 return template.findAndModify(query, update, options, Txn.class);
13 }
14 //...
15}
The ReactiveMongoTemplate provides us with more customizable ways to interact with MongoDB and is a thinner layer of abstraction compared to ReactiveMongoRepository.
In the findAndUpdateStatusById(...) method, we are pretty much defining the query logic by code, but we are also able to specify that the update should return the newly updated document.

Multi-document ACID transactions

The transfer feature in this application is a perfect use case for multi-document transactions because the updates across two accounts need to be atomic.
In order for the application to gain access to Spring's transaction support, we first need to add a ReactiveMongoTransactionManager bean to our configuration as such:
1@Configuration
2public class ReactiveMongoConfig extends AbstractReactiveMongoConfiguration {
3 //...
4 @Bean
5 ReactiveMongoTransactionManager transactionManager(ReactiveMongoDatabaseFactory dbFactory) {
6 return new ReactiveMongoTransactionManager(dbFactory);
7 }
8}
With this, we can proceed to define the scope of our transactions. We will showcase two methods:
1. Using TransactionalOperator
The ReactiveMongoTransactionManager provides us with a TransactionOperator.
We can then define the scope of a transaction by appending .as(transactionalOperator::transactional) to the method call.
1@Service
2public class TxnService {
3 //In the actual code we are using constructor injection instead of @Autowired
4 //Using @Autowired here to keep code snippet concise
5 @Autowired
6 private TransactionalOperator transactionalOperator;
7 //...
8 public Mono<Txn> executeTxn(Txn txn) {
9 return updateBalances(txn)
10 .onErrorResume(DataIntegrityViolationException.class
11 /*lambda expression to handle error*/)
12 .onErrorResume(AccountNotFoundException.class
13 /*lambda expression to handle error*/)
14 .then(txnTemplate.findAndUpdateStatusById(txn.getId(), TxnStatus.SUCCESS))
15 .as(transactionalOperator::transactional);
16 }
17 //...
18}
2. Using @Transactional annotation
We can also simply define the scope of our transaction by annotating the method with the @Transactional annotation.
1public class TxnService {
2 //...
3 @Transactional
4 public Mono<Txn> executeTxn(Txn txn) {
5 return updateBalances(txn)
6 .onErrorResume(DataIntegrityViolationException.class
7 /*lambda expression to handle error*/)
8 .onErrorResume(AccountNotFoundException.class
9 /*lambda expression to handle error*/)
10 .then(txnTemplate.findAndUpdateStatusById(txn.getId(), TxnStatus.SUCCESS));
11 }
12 //...
13}
Read more about transactions and sessions in Spring Data MongoDB for more information.

Conclusion

We are done! I hope this post was helpful for you in one way or another. If you have any questions, visit the MongoDB Community, where MongoDB engineers and the community can help you with your next big idea!
Once again, you may access the code from the GitHub repository, and if you are just getting started, it may be worth bookmarking Spring Data MongoDB.
Top Comments in Forums
Forum Commenter Avatar
Sai_Kumar_TataSai Kumar Tatalast month

As per official doc, MongoDB has certain limitations in Reactive transactions.
Sessions & Transactions :: Spring Data MongoDB

How can we overcome this.

See More on Forums

Facebook Icontwitter iconlinkedin icon
Rate this code example
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Tutorial

Single-Collection Designs in MongoDB with Spring Data (Part 2)


Aug 12, 2024 | 10 min read
Quickstart

Java - Change Streams


Oct 01, 2024 | 10 min read
Article

Java Driver: Migrating From 4.11 to 5.0


Mar 01, 2024 | 3 min read
Quickstart

Getting Started with MongoDB and Java - CRUD Operations Tutorial


Mar 01, 2024 | 24 min read
Technologies Used
Languages
Technologies
Products
Table of Contents
  • Introduction