Reactive Java Spring Boot with MongoDB
Teo Wen Jie5 min read • Published Apr 02, 2024 • Updated Apr 02, 2024
FULL APPLICATION
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.
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.
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:
1 git clone git@github.com:mongodb-developer/mdb-spring-boot-reactive.git
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.
This section showcases how you can perform Create and Read operations with
ReactiveMongoRepository
.1 2 public class AccountController { 3 //... 4 5 public Mono<Account> createAccount( Account account) { 6 return accountRepository.save(account); 7 } 8 9 10 public Mono<Account> getAccount( 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 theaccountNum
.
1 public interface AccountRepository extends ReactiveMongoRepository<Account, String> { 2 3 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
.This section showcases:
- Update operations with
ReactiveMongoRepository
. - Create, Read, and Update operations with
ReactiveMongoTemplate
.
Back to
AccountController.java
:1 2 public class AccountController { 3 //... 4 5 public Mono<Txn> debitAccount( String accountNum, Map<String, Object> requestBody) { 6 //... 7 txn.addEntry(new TxnEntry(accountNum, amount)); 8 return txnService.saveTransaction(txn).flatMap(txnService::executeTxn); 9 } 10 11 12 public Mono<Txn> creditAccount( String accountNum, Map<String, Object> requestBody) { 13 //... 14 txn.addEntry(new TxnEntry(accountNum, -amount)); 15 return txnService.saveTransaction(txn).flatMap(txnService::executeTxn); 16 } 17 18 19 public Mono<Txn> transfer( String from, 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 manyTxnEntry
. - 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 oneTxnEntry
. - A transfer
Txn
will have twoTxnEntry
. - 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 2 public 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.1 public interface AccountRepository extends ReactiveMongoRepository<Account, String> { 2 //... 3 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 aTxn
document intotransactions
collection. - A
executeTxn
method that callsupdateBalances(...)
and then updates the transaction status in theTxn
document created.
Both utilize the
TxnTemplate
that contains a ReactiveMongoTemplate
.1 2 public 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.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 2 public class ReactiveMongoConfig extends AbstractReactiveMongoConfiguration { 3 //... 4 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 2 public 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 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.1 public class TxnService { 2 //... 3 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 }
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
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.