
At Assist Scout, we make investments a number of time to make sure our functions make the perfect out of their databases.
As Spring functions scale, managing datasources can grow to be unwieldy. Massive monolithic functions typically accumulate a number of modules, every with distinct logic but sharing the identical datasource. Over time, this may result in points like efficiency bottlenecks, connection pool exhaustion, and entangled dependencies that decelerate growth and upkeep.
A single datasource configuration could also be enough within the early phases of growth. Nevertheless, as the applying scales, visitors spikes in a single module can starve others of connections, main to at least one specific module immediately impacting others (growing latency, degrading efficiency, and so forth.).
That is the place a per-module datasource strategy may help break down complexity by assigning devoted datasources to modules. A extra granular management over pool sizes and settings permits modules to scale independently whereas isolating efficiency points.
On this put up, we’ll stroll via organising a per-module datasource configuration in a Spring MVC utility utilizing Spring Boot and Kotlin. Therefore, this would possibly not cowl WebFlux since it’s architecturally completely different and would require an tailored strategy. Whether or not coping with a big monolith or transitioning to microservices, this sample will assist streamline your datasource administration.
Undertaking setup
To get began, you’ll be able to create your multi-module app from scratch by way of Spring Initializr or take a look at our pattern undertaking: https://github.com/helpscout/connection-pool-per-module.
The undertaking construction contains two modules (notification andperson) and ashared module for widespread datasource configuration:
connection-pool-per-module/
├── buildSrc
├── src/
│ └── helpscout
├── notification/
│ └── helpscout.notification
├── shared/
│ └── helpscout.shared
└── person/
└── helpscout.personEvery module comprises a easy JPA entity, repository, and controller for itemizing the entries.
We can’t delve deeper into the particular Gradle, Spring, Kotlin, Testcontainers, or Flyway (database versioning) configurations as they’re exterior the scope of this put up. For particulars, please confer with the undertaking’s README.
The best way to run it
Upon beginning the applying by way of./gradlew bootRun, it’s possible you’ll discover the next traces in your logs:
Routing datasource to DEFAULT
...That is the default datasource to be chosen every time no request is coming from a controller. Let’s make the next two subsequent calls:
Double-check your logs to confirm whether or not the routing occurred successfully:
Routing datasource to USER
Routing datasource to NOTIFICATIONDatasource configuration
Spring’s auto-configuration is probably the most easy approach to configure datasources. You set the param values in your app’sutility.yml (orutility.properties) file, and Spring will handle wiring up theDataSource bean and attaching all of the underlying dependencies.
It may possibly shortly grow to be cumbersome to handle a number of datasources this manner, particularly in case you have a number of modules. Repeating the identical properties for each module will increase upkeep overhead.
As a substitute, we use a programmatic strategy to keep away from duplication. Let’s check out theutility.yml:
spring:
datasource:
url: jdbc:mysql://localhost:3306/helpscout
password: check
username: check
hikari:
connection-timeout: 20000
max-lifetime: 300000
situations:
- title: default_pool
pool-dimension: 10
- title: notification_pool
pool-dimension: 10
- title: user_pool
pool-dimension: 10Since all of the datasources share the identical connection and HikariCP properties, we segregated the particular pool settings below a brand new property referred to assituations for simpler fine-tuning.
Discover that we use a setpool-dimension since HikariCP recommends utilizing fixed-size swimming pools “for optimum efficiency and responsiveness to spike calls for.”
These properties could be simply captured in a POJO by way of@ConfigurationProperties:
@ConfigurationProperties("spring.datasource")
class ModuleDataSourceProperties : DataSourceProperties() {
lateinit var hikari: HikariConfig
lateinit var situations: Record<InstanceProperties>
class InstanceProperties {
lateinit var title: String
var poolSize: Int by notNull()
}
}We’re additionally reusing theHikariConfig fromcom.zaxxer.hikari (no have to reinvent the wheel right here).
Subsequent, we configure the datasources programmatically:
@Configuration
@EntityScan("helpscout")
@EnableConfigurationProperties(ModuleDataSourceProperties::class)
class SpringDataJpaConfiguration {
@Bean
@Main
enjoyable dataSource(dsProperties: ModuleDataSourceProperties): DataSource {
val targetDataSources = dsProperties.situations.affiliate { occasion ->
ModuleName.getStartingWith(occasion.title) to dsProperties.initDataSource(occasion)
}
return createRoutingDataSource(targetDataSources)
}
non-public enjoyable ModuleDataSourceProperties.initDataSource(occasion: InstanceProperties) =
(initializeDataSourceBuilder().construct() as HikariDataSource).apply {
poolName = occasion.title
maximumPoolSize = occasion.poolSize
connectionTimeout = hikari.connectionTimeout
maxLifetime = hikari.maxLifetime
}
non-public enjoyable createRoutingDataSource(targetDataSources: Map<ModuleName, DataSource>) =
ModuleRoutingDataSource().apply {
setTargetDataSources(targetDataSources.toMap())
setDefaultTargetDataSource(targetDataSources.getValue(DEFAULT))
}
}
Discover how we’re making use ofModuleDataSourceProperties to iterate via all of the configured situations and create our goal datasources.
The map keys might be easy enums matching the identical module names (that is how we’ll later map every request’s class package deal to its equal module title):
enum class ModuleName {
DEFAULT,
NOTIFICATION,
USER
}Context dealing with
To inform Spring to route every request to its correct datasource connection, we have to prolong AbstractRoutingDataSource: an summaryDataSource implementation that routes getConnection() calls to one of many numerous goal datasources primarily based on a lookup key that is normally, however not essentially, decided via some thread-bound transaction context.
class ModuleRoutingDataSource : AbstractRoutingDataSource()
override enjoyable determineCurrentLookupKey(): ModuleName = DataSourceContext.present.additionally {
println("Routing datasource to $it")
}
}Theprintln will assist validate which datasource is at present sure to every thread.
The context holder implementation is essential because it acts as a centralized storage mechanism for the present context by way of aThreadLocal and supplies static strategies to set, retrieve, and clear the context.
UsingThreadLocal is important for concurrent transactions, because it binds the context to the thread that’s executing the present operation:
object DataSourceContext {
non-public val contextHolder = ThreadLocal.withInitial { DEFAULT }
@JvmStatic
enjoyable setDataSource(module: ModuleName) = contextHolder.set(module)
@JvmStatic
val present: ModuleName
get() = contextHolder.get()
@JvmStatic
enjoyable reset() = contextHolder.take away()
}TheAbstractRoutingDatasource, in flip, leverages theDataSourceContext to fetch the present context, which it then makes use of to find out the suitable datasource.
Intercepting and routing
There are a couple of methods to successfully set up the suitable lookup key throughout the context holder, like making a service layer that accepts the context as an enter parameter, configures the context earlier than invoking the data-access layer, and clears it afterward.
A extra pragmatic strategy we selected consists of leveraging AspectJ to intercept a particular stereotype (e.g.,@RestController), establish from which package deal the request originated, and choose the proper datasource:
@Facet
@Element
class RoutingDataSourceTransactionInterceptors {
@Pointcut("@inside(org.springframework.internet.bind.annotation.RestController)")
non-public enjoyable inController() {
}
@Round("inController()")
enjoyable proceedFromController(pjp: ProceedingJoinPoint): Any? {
val module = inferModuleName(pjp)
return useReplica(pjp) { setDataSource(module) }
}
non-public enjoyable inferModuleName(pjp: ProceedingJoinPoint): ModuleName {
val packageName = pjp.staticPart.signature.declaringType.packageName
val predicate: (ModuleName) -> Boolean = {
packageName.comprises(it.title, ignoreCase = true)
}
return ModuleName.entries.discover(predicate) ?: DEFAULT
}
non-public enjoyable useReplica(pjp: ProceedingJoinPoint, setDbReplica: () -> Unit): Any? = attempt {
setDbReplica()
pjp.proceed()
} lastly {
reset()
}
}Understand that this technique was simplified to maintain issues quick, however you may take into account a extra sturdy implementation that higher fits your wants (e.g., dealing with async utility context occasions).
TheDEFAULT is at all times picked up when no module has been recognized. It’s possible you’ll adapt this technique to log and observe outliers or throw an exception in case you would like no connection to occur exterior of the module boundaries.
Integration assessments
You also needs to discover that our pattern undertaking contains integration assessments for every module to make sure they’re choosing up the proper connections.
Check out howDefaultDataSourceIntegrationTest validates the routing toDEFAULT every time a request is triggered from a package deal that is not mapped:
class DefaultDataSourceIntegrationTest {
@Autowired
non-public lateinit var mvc: MockMvc
@Take a look at
enjoyable `check endpoint for wrongly mapped package deal`() {
mvc.carry out(get("/check/default").contentType(APPLICATION_JSON))
.andExpect(standing().isOk())
}
@RestController
@RequestMapping("/check")
class TestController {
@GetMapping("/default")
enjoyable getDefault() {
assertThat(DataSourceContext.present).isEqualTo(DEFAULT)
}
}
}Summing up
Splitting datasource configurations by module improves scalability, reduces competition, and simplifies monitoring.
The segregated metrics as a substitute of single metrics for every pool assist us higher troubleshoot whether or not a particular module’s connections are inflicting spikes or outages.
Just a few different issues to remember:
Extending this setup to separate learn and write connections could be simple and may additional enhance efficiency, stopping long-running learn queries from blocking important write operations.
For those who want the datasource beans registered into Spring’s utility context, think about using ImportBeanDefinitionRegistrar as a substitute.
Think about adopting Java 9 modules to implement encapsulation and clear separation between your modules. This manner, you’ll be able to simplify your interception logic by switching the package deal by the Module by way of
pjp.staticPart.signature.declaringType.module.

