Multi-tenancy in the API world made easy

18 January

Multi-tenancy is a fundamental architecture which can be used to share IT resources cost-efficiently and securely in cloud environments, in which a single instance of software runs on a server and serves multiple tenants.

It's been years since we first heard about it; it came out again riding the wave of cloud computing, so we can assume that multi-tenancy is a consolidated architecture and the benefits in terms of maintainability and costs are well known.

APIs are the backbone of a distributed cloud architecture, so building a multi-tenant API is the natural aftermath in this scenario.

In this article I will show you how to build a simple multi-tenant RESTful API using Java in a quick and easy way, dramatically reducing the configuration code amount required by the most frequently adopted solutions.

A multi-tenancy implementation may be cumbersome

The first impression, looking for multi-tenancy implementation strategies, is that there's no specific standard and no well defined best practices.

One of the most common solution is relying on Hibernate to implement multi-tenancy behaviour at DataSource level. Hibernate requires two interfaces to be implemented: CurrentTenantIdentifierResolver to resolve what the application considers the current tenant identifier and MultiTenantConnectionProvider to obtain Connections in a tenant specific manner. This could be tricky and not so trivial, possibly leading to boilerplate code rising.

And finally, last but not least, this solution is only suitable for a JPA implementation, since Hibernate is directly used as tenant resolver and connection router.

Furthermore, the Spring Framework is often used in conjunction with Hibernate to build a multi-tenant API or application. One of the most useful aspects of the core Spring architecture is the availability of scopes: in a multi-tenant scenario, sooner or later you'll surely miss a tenant scope, which is not available out-of-the-box. Creating a custom scope in Spring is not trivial, it requires a deep knowledge of the Spring architecture and a lot of code to implement and register the new scope.

You can check the Spring+Hibernate version of the example shown in the second part of this article here: https://github.com/fparoni/spring-boot-hibernate-multitenant-jaxrs-api.

The easy way

Now let's streamline the process, in order to highly simplify the multi-tenancy setup and to cut down the required configuration effort, getting rid of all the boilerplate code.

We will use the Holon platform to achieve this goal, relying on the platform's native multi-tenant support. I'll show you a working example of a simple RESTful API, using the following Holon platform's components and configuration facilities:

  1.  The TenantResolver interface to provide the current tenant identifier
  2. A tenant Spring scope provided out-of-the-box, automatically configurated and registered when using Spring Boot
  3.  The Holon Platform JDBC datastore module to use JDBC for multi-tenant data access
  4.  The Holon Platform JAX-RS module to easily create a RESTful API using Java

The complete example is available on GitHub: https://github.com/holon-platform/spring-boot-jaxrs-multitenant-api-example.

Our RESTful API implements the CRUD operations to manage a simple Product, providing methods to create, update, delete and query the product data.

The multi-tenancy architecture of this example is organized per schema: each tenant is bound to a separate database schema and will access data through a different Java DataSource.

Each time an API request is performed we need to know the caller tenant identifier, to correctly route the persistence operations to the right database schema.

The three most common ways to provide the tenant identifier are:

  1. Providing the tenant identifier as a URL part, for example using a query parameter
  2. Using a custom HTTP request header
  3. Using JWTs to provide the tenant identifier as a JSON token claim

We will use the second option, through a custom HTTP header called 'X-TENANT-ID'.

An example to use JWT to provide the tenant identifier is available in the jwt branch of the GitHub example repository.

Besides the Holon platform components listed above, for our example we will use:

  1. Spring Boot for application auto-configuration and to create a runnable jar with an embedded servlet container
  2. The H2 in-memory database for data persistence
  3. JSON as data exchange format

The Holon Datastore API will be used to access data in a technology-indipendent way (using JDBC in this example is only a matter of configuration) and the Holon Property model to define the product data model. Check to Holon reference documentation for detailed information.

The result will be a fully working example made of just three Java classes: now let's build our example step by step.

Step 1: Project configuration

First of all, we will use Maven for project configuration and dependency management.

We'll use the Holon Platform BOM (Bill of Materials) to easily obtain the dependencies we need.  

<properties>
  <holon.platform.version>5.0.6</holon.platform.version>
</properties>

<dependencyManagement>
		<dependencies>
			<!-- Holon Platform BOM -->
			<dependency>
				<groupId>com.holon-platform</groupId>
				<artifactId>bom</artifactId>
				<version>${holon.platform.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
</dependencyManagement>

The Holon JDBC Datastore Spring Boot starter is used to automatically configure the data access layer:

<!-- Holon JDBC Datastore with HikariCP starter -->
<dependency>
	<groupId>com.holon-platform.jdbc</groupId>
	<artifactId>holon-starter-jdbc-datastore-hikaricp</artifactId>
</dependency>

Likewise we declare the Holon JAX-RS Spring Boot starter dependency to use and auto-configure Jersey as JAX-RS implementation.

<!-- Holon JAX-RS using Jersey -->
<dependency>
	<groupId>com.holon-platform.jaxrs</groupId>
	<artifactId>holon-starter-jersey</artifactId>
</dependency>

At last, the H2 database dependency:

<!-- H2 database -->
<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<version>1.4.196</version>
</dependency>

To configure our Spring application we will use an application.yml configuration file.

We simulate that two tenants are available, the first bound to a schema named tenant1 and the second associated to a schema named tenant2. So we need two DataSource instances, one for each schema.

Auto-configure more than one DataSource with the default Spring Boot DataSource auto-configuration is not possible without writing additional code. For this reason, we'll rely on the Holon platform DataSource auto-configuration facilities to define the two tenant DataSources, through a holon.datasource configuration property structure like this:

holon:
  datasource:
        tenant1:
      		url: "jdbc:h2:mem:tenant1"
      		username: "sa"
    	tenant2:
      		url: "jdbc:h2:mem:tenant2"
      		username: "sa" 

By default, for each auto-configured DataSource, a Holon JDBC Datastore is created too. Each DataSource/Datastore pair will be available as a Spring bean and qualified with the name specified through the holon.datasource.* properties. For example, the DataSource bound to the first tenant can be injected in a Spring component using the @Qualifier(“tenant1”) annotation.

To be up and running at startup we will use a couple of .sql file to create a sample table named 'product' and one row for each tenant.

Step 2: Writing the code

As I said before we need just 3 Java classes.

1. Application definition and configuration

First of all, we create the Application class, which acts as the Spring Boot application entry-point and as the main Spring configuration class:

@SpringBootApplication
public class Application {

	@Bean
	@RequestScope
	public TenantResolver tenantResolver(HttpServletRequest request) {
		return () -> Optional.ofNullable(request.getHeader("X-TENANT-ID"));
	}

	@Bean
	@ScopeTenant
	public Datastore datastore(BeanFactory beanFactory, TenantResolver tenantResolver) {
		// get current tenant id
		String tenantId = tenantResolver.getTenantId()
				.orElseThrow(() -> new IllegalStateException("No tenant id available"));
		// get Datastore using tenantId qualifier
		return BeanFactoryAnnotationUtils.qualifiedBeanOfType(beanFactory, Datastore.class, tenantId);
	}

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

We have configured two Spring beans:

  • A TenantResolver implemetation to provide the current tenant identifier and to enable the Holon platform tenant scope. In this example, we want to use a custom HTTP header named “X-TENANT-ID” to obtain the tenant identifier from the caller. We need the current HttpServletRequest to obtain the header value, so we declare the TenantResolver as request-scoped and rely on Spring to obtain a reference to the current request.
  • A tenant-scoped Datastore is correctly retrieved by Spring through the tenant identifier which acts as bean qualifier. The two Datastore instances have already been configured by the Holon Platform Spring Boot autoconfiguration facilities, using the two DataSources declared through the holon.datasource.* configuration properties. The current tenant-related Datastore is retrieved using the bean qualifier which matches with the current tenant id, provided by the TenantResolver interface.

2. The data model

The Product interface represents our data model:

/**
 * Product model
 */
public interface Product {

	static final PathProperty<Long> ID = PathProperty.create("id", Long.class);

	static final PathProperty<String> SKU = PathProperty.create("sku", String.class);

	static final PathProperty<String> DESCRIPTION = PathProperty.create("description", String.class);

	static final PathProperty<String> CATEGORY = PathProperty.create("category", String.class);

	static final PathProperty<Double> UNIT_PRICE = PathProperty.create("price", Double.class)
			// not negative value validator
			.validator(Validator.notNegative());

	// Product property set
	static final PropertySet<?> PRODUCT = PropertySet.of(ID, SKU, DESCRIPTION, CATEGORY, UNIT_PRICE);

	// "products" table DataTarget
	static final DataTarget<String> TARGET = DataTarget.named("products");

}

The Holon platform Property model is used to define the product data model. Deepening this topic is out of the scope of this article, check the official Holon documentation for further details.

3. The API endpoint

The ProductEndpoint class represents the JAX-RS resource that will provide the API CRUD, using JSON as data exchange format. The class is annotated with @Component to make it available as a Spring component and we rely on the Holon platform Jersey auto-configuration facilities to automatically register the endpoint in the JAX-RS application:

@Component
@Path("/api")
public class ProductEndpoint {

	@Autowired
	private Datastore datastore;

	/*
	 * Get a list of products PropertyBox in JSON.
	 */
	@GET
	@Path("/products")
	@Produces(MediaType.APPLICATION_JSON)
	public List<PropertyBox> getProducts() {
		return datastore.query().target(TARGET).list(PRODUCT);
	}

	/*
	 * Get a product PropertyBox in JSON.
	 */
	@GET
	@Path("/products/{id}")
	@Produces(MediaType.APPLICATION_JSON)
	public Response getProduct(@PathParam("id") Long id) {
		return datastore.query().target(TARGET).filter(ID.eq(id)).findOne(PRODUCT).map(p -> Response.ok(p).build())
				.orElse(Response.status(Status.NOT_FOUND).build());
	}

	/*
	 * Create a product. The @PropertySetRef must be used to declare the request
	 * PropertyBox property set.
	 */
	@POST
	@Path("/products")
	@Consumes(MediaType.APPLICATION_JSON)
	public Response addProduct(@PropertySetRef(Product.class) PropertyBox product) {
		// set id
		long nextId = datastore.query().target(TARGET).findOne(ID.max()).orElse(0L) + 1;
		product.setValue(ID, nextId);
		// save
		datastore.save(TARGET, product);
		return Response.created(URI.create("/api/products/" + nextId)).build();
	}

	/*
	 * Update a product. The @PropertySetRef must be used to declare the request
	 * PropertyBox property set.
	 */
	@PUT
	@Path("/products/{id}")
	@Consumes(MediaType.APPLICATION_JSON)
	public Response updateProduct(@PropertySetRef(Product.class) PropertyBox product) {
		return datastore.query().target(TARGET).filter(ID.eq(product.getValue(ID))).findOne(PRODUCT).map(p -> {
			datastore.save(TARGET, product);
			return Response.noContent().build();
		}).orElse(Response.status(Status.NOT_FOUND).build());
	}

	/*
	 * Delete a product by id.
	 */
	@DELETE
	@Path("/products/{id}")
	@Consumes(MediaType.APPLICATION_JSON)
	public Response deleteProduct(@PathParam("id") Long id) {
		datastore.bulkDelete(TARGET).filter(ID.eq(id)).execute();
		return Response.noContent().build();
	}

}

Note that we injected the Datastore instance to perform data access operations: thanks to tenant scope with which the Datastore bean is declared, we'll obtain the right Datastore instance according to the current tenant identifier.

Run the application

The spring-boot-maven-plugin will create a runnable jar to have our application up and running using our JRE. We can run application using this maven command:

mvn spring-boot:run

Application is running under Tomcat, listening to port 8080. We can test our endpoint using Postman application and making GET or POST request to the path we've defined before. Let's test the /products operation using both tenants. Setting the X-TENANT-ID header to tenant1 will produce the following result:

Using tenant2 as the X-TENANT-ID header value, the result is:

Summary

We've seen how to setup a multi-tenant API quickly, highly reducing the configuration effort and boilerplate code that is often required for this kind of task. The multi-tenant architecture that we've used in our simple API implementation can of course be leveraged for other, more complex, services or applications. The source code of the application can be found at https://github.com/holon-platform/spring-boot-jaxrs-multitenant-api-example.

  • Fabio Paroni

We use cookies to ensure that we give you the best experience on our website.