DataSources
Spring Boot
Spring Data JPA
AWS Aurora Serverless
PostgreSQL
Separating Read and Write DataSources in Spring Boot

by: Jerrish Varghese

May 03, 2024

titleImage

Introduction

In modern software development, achieving optimal performance and scalability often involves separating read and write operations. This separation allows each operation type to be independently optimized and scaled. In this blog post, we'll explore how to implement separate read and write DataSources in a Spring Boot application using Spring Data JPA and AWS Aurora Serverless PostgreSQL.

Why Separate Read and Write Operations?

Separating read and write operations offers several benefits:

  • Performance Optimization: Read-heavy applications can benefit from databases optimized for querying, while write-heavy operations can be handled separately.
  • Scalability: Independently scaling read and write operations helps handle different load characteristics.
  • Fault Isolation: Issues in one type of operation (e.g., heavy writes) do not affect the other (e.g., read operations).

Setting Up Separate DataSources

We'll use AWS Aurora Serverless PostgreSQL, which provides separate endpoints for read and write operations. Here’s how to configure Spring Boot to use these endpoints.

Step 1: Add Dependencies

Ensure you have the necessary dependencies in your pom.xml:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
    </dependency>
</dependencies>

Step 2: Configure DataSources

Define the DataSources for reading and writing in your application.yml.

spring:
  datasource:
    write:
      url: jdbc:postgresql://write-endpoint-url:5432/yourdb
      username: yourusername
      password: yourpassword
      driver-class-name: org.postgresql.Driver
    read:
      url: jdbc:postgresql://read-endpoint-url:5432/yourdb
      username: yourusername
      password: yourpassword
      driver-class-name: org.postgresql.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true

Step 3: Create Configuration Classes

Create configuration classes to set up the DataSources and routing logic.

ReadWriteDataSourceConfig.java
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class ReadWriteDataSourceConfig {

    @Bean(name = "writeDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.write")
    public DataSource writeDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "readDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.read")
    public DataSource readDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "dataSource")
    public DataSource dataSource(@Qualifier("writeDataSource") DataSource writeDataSource,
                                 @Qualifier("readDataSource") DataSource readDataSource) {
        AbstractRoutingDataSource routingDataSource = new ReplicationRoutingDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(DataSourceType.WRITE, writeDataSource);
        dataSourceMap.put(DataSourceType.READ, readDataSource);

        routingDataSource.setDefaultTargetDataSource(writeDataSource);
        routingDataSource.setTargetDataSources(dataSourceMap);
        return routingDataSource;
    }
}
ReplicationRoutingDataSource.java
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }
}

Step 4: Implement DataSource Routing Logic

DataSourceType.java
public enum DataSourceType {
    READ, WRITE
}
DataSourceContextHolder.java
public class DataSourceContextHolder {
    private static final ThreadLocal<DataSourceType> CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setDataSourceType(DataSourceType dataSourceType) {
        CONTEXT_HOLDER.set(dataSourceType);
    }

    public static DataSourceType getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

Step 5: Create AOP Aspect for Routing

Use Aspect-Oriented Programming (AOP) to route read and write operations to the correct DataSource.

DataSourceAspect.java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class DataSourceAspect {

    @Before("execution(* com.yourpackage.repository..*.save*(..)) || execution(* com.yourpackage.repository..*.insert*(..)) || execution(* com.yourpackage.repository..*.update*(..)) || execution(* com.yourpackage.repository..*.delete*(..))")
    public void setWriteDataSourceType() {
        DataSourceContextHolder.setDataSourceType(DataSourceType.WRITE);
    }

    @Before("execution(* com.yourpackage.repository..*.find*(..)) || execution(* com.yourpackage.repository..*.get*(..)) || execution(* com.yourpackage.repository..*.read*(..))")
    public void setReadDataSourceType() {
        DataSourceContextHolder.setDataSourceType(DataSourceType.READ);
    }
}

Step 6: Configure JPA EntityManager

Configure the LocalContainerEntityManagerFactoryBean to use the custom DataSource.

JpaConfig.java
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.yourpackage.repository",
        entityManagerFactoryRef = "entityManagerFactory",
        transactionManagerRef = "transactionManager"
)
public class JpaConfig {

    @Bean(name = "entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder,
            @Qualifier("dataSource") DataSource dataSource) {
        return builder
                .dataSource(dataSource)
                .packages("com.yourpackage.domain")
                .persistenceUnit("default")
                .build();
    }

    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(
            @Qualifier("entityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory.getObject());
    }
}

Conclusion

By following these steps, you can effectively separate read and write DataSources in your Spring Boot application. This approach provides better performance, scalability, and fault isolation. The use of AOP ensures that read and write operations are correctly routed, maintaining a clean separation between them. This setup is particularly beneficial for applications with heavy read and write operations, allowing each to be optimized and scaled independently.

Implementing such a pattern can lead to significant improvements in the efficiency and reliability of your application, making it better suited to handle varying loads and operational requirements.

contact us

Get started now

Get a quote for your project.
logofooter
title_logo

USA

Edstem Technologies LLC
254 Chapman Rd, Ste 208 #14734
Newark, Delaware 19702 US

INDIA

Edstem Technologies Pvt Ltd
Office No-2B-1, Second Floor
Jyothirmaya, Infopark Phase II
Ernakulam, Kerala 682303

© 2024 — Edstem All Rights Reserved

Privacy PolicyTerms of Use