Mastering Datasource Routing in Spring for Transactions

Snippet of programming code in IDE
Published on

Mastering Datasource Routing in Spring for Transactions

Spring Framework has become a cornerstone of Java development, particularly for building enterprise applications. The ability to manage transactions effectively is essential for maintaining data integrity, and datasource routing plays a crucial role in that aspect. In this post, we will explore how to implement datasource routing in Spring to manage transactions across multiple databases. This technique allows us to achieve better performance, scalability, and fault tolerance.

Why Use Datasource Routing?

Datasource routing allows us to dynamically switch between multiple database connections based on the context of the application. This becomes particularly useful in scenarios where:

  • Sharding: Distributing the load across multiple databases to improve performance.
  • Multi-tenancy: Serving multiple customers from the same application but keeping their data isolated.
  • Failover: Automatically switching to a backup datasource in case of failure.

In short, datasource routing provides flexibility in transaction management, enabling effective resource allocation based on specific needs.

Setting Up Spring Boot Project

To get started with datasource routing, we need to set up a Spring Boot application. You can quickly generate a new Spring Boot project using Spring Initializr. Make sure to include the following dependencies:

  • Spring Web
  • Spring Data JPA
  • H2 Database (for in-memory testing)

Project Structure

Your project structure should look like this:

src
└── main
    ├── java
    │   └── com
    │       └── example
    │           └── datasource
    │               ├── DatasourceConfig.java
    │               ├── DataSourceRouter.java
    │               ├── User.java
    │               ├── UserRepository.java
    │               ├── UserService.java
    │               └── Application.java
    └── resources
        └── application.yml

Configuration File

Start by configuring your application.yml like so:

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          url: jdbc:h2:mem:master;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
          driver-class-name: org.h2.Driver
          username: sa
          password:
        secondary:
          url: jdbc:h2:mem:secondary;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
          driver-class-name: org.h2.Driver
          username: sa
          password:

Here, we have defined two in-memory datasources: master and secondary. The primary datasource is defined as master.

Creating the DataSource Router

The first step is to implement the DataSourceRouter, which extends AbstractRoutingDataSource. This class will help us determine which datasource to route to at runtime.

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DataSourceRouter extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionContext.getCurrentDatasourceKey();
    }
}

Explanation

  • determineCurrentLookupKey(): This method is overridden to return the key of the datasource we want to route to. We will use a thread-local context (TransactionContext) to keep track of the current datasource key.

Implementing TransactionContext

Next, we define a context holder that will store the current datasource key in a thread-local variable:

public class TransactionContext {

    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

    public static void setCurrentDatasourceKey(String key) {
        CONTEXT.set(key);
    }

    public static String getCurrentDatasourceKey() {
        return CONTEXT.get();
    }

    public static void clear() {
        CONTEXT.remove();
    }
}

Explanation

  • ThreadLocal: Allows us to store values that are accessible to the current thread. This is essential for transactions where each thread may work with different datasource contexts.

Configuring DataSources

Next, you need to configure the DataSourceRouter bean in your DatasourceConfig.java.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import org.springframework.boot.jdbc.DataSourceBuilder;

import java.util.HashMap;

@Configuration
public class DatasourceConfig {

    @Bean
    public DataSource dataSource() {
        DataSourceRouter router = new DataSourceRouter();
        router.setTargetDataSources(dataSources());
        router.setDefaultTargetDataSource(dataSources().get("master"));
        return router;
    }

    private HashMap<Object, Object> dataSources() {
        HashMap<Object, Object> dataSources = new HashMap<>();
        dataSources.put("master", DataSourceBuilder.create().url("jdbc:h2:mem:master").build());
        dataSources.put("secondary", DataSourceBuilder.create().url("jdbc:h2:mem:secondary").build());
        return dataSources;
    }
}

Explanation

  • setTargetDataSources(): This method is used to set the available datasources for the router.
  • setDefaultTargetDataSource(): Specifies which datasource to use by default. In this case, we've chosen master.

Using Datasource Routing in Services

Now that we've set everything up, we can define a service that routes transactions based on context. For illustration, let’s create a simple User entity and its repository.

User Entity

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;

    // Getters and Setters
}

UserRepository

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

UserService Implementation

We will have a service that allows us to switch datasources and perform operations.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void createUserInMaster(String username) {
        TransactionContext.setCurrentDatasourceKey("master");
        User user = new User();
        user.setUsername(username);
        userRepository.save(user);
    }

    @Transactional
    public void createUserInSecondary(String username) {
        TransactionContext.setCurrentDatasourceKey("secondary");
        User user = new User();
        user.setUsername(username);
        userRepository.save(user);
    }
}

Explanation

  • createUserInMaster and createUserInSecondary: These methods set the current datasource key in the TransactionContext before performing repository operations, allowing us to switch the underlying datasource seamlessly.

Testing the Implementation

Finally, let’s test our implementation using the main application class.

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {

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

    @Bean
    public CommandLineRunner demo(UserService userService) {
        return args -> {
            userService.createUserInMaster("Alice");
            userService.createUserInSecondary("Bob");
            // Add a method to fetch and display users from both datasources to verify
        };
    }
}

Explanation

  • Using CommandLineRunner, we can execute our datasource operations on application startup for testing purposes.

Lessons Learned

In this blog post, we explored how to master datasource routing in Spring for transactional management. This powerful approach enables optimal resource utilization and better performance for applications needing multi-database support. By leveraging Spring's features and implementing a simple routing mechanism, we can build resilient and scalable applications.

This is a stepping stone into the vast world of Spring Framework. For further reading, consider exploring the following resources:

Implement these techniques and start mastering your application's transaction management today!