In this post, we are going to implement multi-tenancy using Spring Boot WebFlux and MongoDB.
Multitenancy applications allow multiple customers or tenants to use a single resource without seeing each other’s data. This is highly common in SaaS solutions. There are three main approaches to isolating information in these multitenant systems (Separate database, Separate schema, Partitioned (Discriminator) Data).
Each solution approach has its pros and cons. It is recommended to implement an approach according to your use cases.
Get Started
In this story, we will implement the approach with a separate database. So each tenant’s data is kept in a physically separate database instance.
Prerequisites
- Spring Boot 2.4
- Maven 3.6.+
- JAVA 17
- Mongo 4.4

We will start by creating a simple Spring Boot project from start.spring.io, with the following dependencies: Spring Reactive Web, Spring Data Reactive MongoDB, and Lombok.
Resolving the Current Tenant ID
The goal is to pass the current tenant ID to the controller using an HTTP header named X-Tenant. Let’s create a WebFilter that will intercept all HTTP requests.
@Component
public class TenantFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(AppConstant.TENANT_HEADER_KEY))
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED)))
.flatMap(tenantKey -> chain.filter(exchange).contextWrite(ctx -> ctx.put(AppConstant.TENANT_ID, tenantKey)));
}
}
When the tenant id is intercepted, we save it in a reactor context which will be available to any class that requests it.
Get Tenant Data Sources
The second main purpose is to load the tenant data sources and select the corresponding data source based on the tenantId retrieved from the context. For this story, we will define data sources for all tenants in the YAML file.
tenants:
datasources:
- id: tenant1
host: localhost
port: 27017
database: booktenant1
username: admin
password: admin
- id: tenant2
host: localhost
port: 27017
database: booktenant2
username: admin
password: admin
- id: tenant3
host: localhost
port: 27017
database: booktenant3
username: admin
password: admin
In the next step, we need to create Mongo Database instances from a MongoClient instance using SimpleReactiveMongoDatabaseFactory class.
@Configuration
public class MultiTenantMongoDBFactory extends SimpleReactiveMongoDatabaseFactory {
private final MongoDataSources mongoDataSources;
/**
* @param mongoClient default mongo client connection
* @param databaseName default database
* @param mongoDataSources datasource bean component
*/
public MultiTenantMongoDBFactory(@Qualifier("createMongoClient") MongoClient mongoClient, String databaseName, MongoDataSources mongoDataSources) {
super(mongoClient, databaseName);
this.mongoDataSources = mongoDataSources;
}
@Override
public Mono<MongoDatabase> getMongoDatabase() throws DataAccessException {
return mongoDataSources.mongoDatabaseCurrentTenantResolver();
}
}
The Override getMongoDatabase method is the part that loads the current data source. It calls our custom bean MongoDataSources.
@Component
public class MongoDataSources {
private List<TenantClient> tenantClients;
private TenantClient defaultTenant = new TenantClient();
private final DataSourceProperties dataSourceProperties;
public MongoDataSources(DataSourceProperties dataSourceProperties) {
this.dataSourceProperties = dataSourceProperties;
}
/**
* Initialize all mongo tenant datasource
*/
@PostConstruct
@Lazy
public void initTenant() {
tenantClients = new ArrayList<>();
List<TenantDatasource> tenants = dataSourceProperties.getDatasources();
tenantClients = tenants.stream().map(t -> new TenantClient(t.getId(), t.getDatabase(),t.getPort(), t.getHost(), t.getUsername(), t.getPassword())).toList();
tenantClients.stream().findFirst().ifPresent(t -> defaultTenant = t);
}
/**
* Default Database name for spring initialization. It is used to be injected into the constructor of MultiTenantMongoDBFactory.
*
* @return String of default database.
*/
@Bean
public String databaseName() {
return defaultTenant.getDatabase();
}
/**
* Default Mongo Connection for spring initialization.
* It is used to be injected into the constructor of MultiTenantMongoDBFactory.
*/
@Bean
public MongoClient createMongoClient() {
MongoCredential credential = MongoCredential.createCredential(defaultTenant.getUsername(), defaultTenant.getDatabase(), defaultTenant.getPassword().toCharArray());
return MongoClients.create(MongoClientSettings.builder()
.applyToClusterSettings(builder ->
builder.hosts(Collections.singletonList(new ServerAddress(defaultTenant.getHost(),defaultTenant.getPort()))))
.credential(credential)
.build());
}
/**
* This will get called for each DB operations
*
* @return MongoDatabase
*/
public Mono<MongoDatabase> mongoDatabaseCurrentTenantResolver() {
return Mono
.deferContextual(Mono::just)
.filter(ct -> ct.hasKey(AppConstant.TENANT_ID))
.map(ct -> ct.get(AppConstant.TENANT_ID))
.map(tenantId -> {
TenantClient currentTenant = getCurrentTenant(tenantId.toString());
return currentTenant.getClient().getDatabase(currentTenant.getDatabase());
});
}
/**
* @return TenantClient tenant client.
*/
private TenantClient getCurrentTenant(String tenantId){
return tenantClients.stream().filter(c -> c.getId().equals(tenantId))
.findFirst().orElseThrow(() -> new TenantDataSourceNotFoundException("Tenant ID not found"));
}
}
CRUD sample
Let’s create an example of CRUD with a Book document.
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document
public class Book {
@Id
private String id;
private String title;
private int page;
private String isbn;
private String description;
private double price;
private LocalDate publicationDate;
private String language;
}
Also we need to create BookRepository, BookService and BookController.
@Service
@Slf4j
public record BookServiceImpl(BookRepository bookRepository) implements BookService{
@Override
public Flux<Book> getAllBook() {
return bookRepository.findAll();
}
@Override
public Mono<Book> addBook(Book book) {
LOGGER.info("addBook : {} " , book );
return bookRepository.save(book)
.log();
}
@Override
public Mono<Book> getBookById(String id) {
return null;
}
@Override
public Mono<Book> updateBook(Book book, String id) {
return bookRepository.findById(id)
.flatMap(book1 -> {
book1.setTitle(book.getTitle());
book1.setIsbn(book.getIsbn());
book1.setPublicationDate(book.getPublicationDate());
return bookRepository.save(book1);
});
}
@Override
public Mono<Void> deleteBookById(String id) {
return bookRepository.deleteById(id);
}
}
Test the REST APIs:
Run the Spring Boot Application.
curl — location — request POST ‘http://localhost:8080/v1/book’ \
— header ‘X-Tenant: tenant2’ \
— header ‘Content-Type: application/json’ \
— data-raw ‘{
“id”:”1234567890jjh9poj”,
“title”:”Thejjj”,
“totalPage”:480,
“isbn”:”726971586–6″,
“description”: “book description”,
“price”: 500
}’


- Get data with the wrong tenant ID

Conclusion
In this story, we implemented multi-tenancy using Spring Boot WebFlux and MongoDB.
The complete source code is available on GitHub.
References
- https://projectreactor.io/docs/core/release/reference/
- https://dzone.com/articles/multi-tenancy-implementation-using-spring-boot-and
** Cover image by 30daysreplay Social Media Marketing on Unsplash