The example below integrates GraphQL Java with Spring transaction management to ensure each query/mutation is executed in a transaction (similar to request-scoped transactions for RESTful APIs) so as to ensure consistency when different loaders are executed. This is done by providing a custom ExecutionStrategy which starts a transaction before executing the query/mutation and commits it afterwards. In order for this to work properly, it is necessary that for a single query all loaders are executed in sequential order on the same thread.

It was built using version 2.0 of graphql-java-spring-boot-starter-webmvc, but it should be easy to carry over to other setups.


package at.sigmoid

import java.util.concurrent.CompletableFuture
import org.dataloader.BatchLoader
import org.dataloader.BatchLoaderEnvironment
import org.dataloader.BatchLoaderWithContext
import org.springframework.core.task.SyncTaskExecutor

// Make sure *all* loaders go through this function
fun <T> runLoader(f: () -> T): CompletableFuture<T> {
    return CompletableFuture.supplyAsync(f, SyncTaskExecutor())

fun <K, V> makeBatchLoader(load: (List<K>) -> List<V?>): BatchLoader<K, V> {
    return BatchLoader { keys -> runLoader { load(keys) } }

fun <K, V> makeBatchLoaderWithContext(load: (List<K>, BatchLoaderEnvironment) -> List<V?>): BatchLoaderWithContext<K, V> {
    return BatchLoaderWithContext { keys, env -> runLoader { load(keys, env) } }


package at.sigmoid

import graphql.execution.AsyncExecutionStrategy
import graphql.execution.DataFetcherExceptionHandler
import graphql.execution.ExecutionContext
import graphql.execution.ExecutionStrategyParameters
import java.util.concurrent.CompletableFuture
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.annotation.Isolation
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional

class TransactionalExecutionStrategy(
    val transactionManager: PlatformTransactionManager,
    val exceptionHandler: DataFetcherExceptionHandler,
) : AsyncExecutionStrategy(exceptionHandler) {
    override fun execute(
        executionContext: ExecutionContext,
        parameters: ExecutionStrategyParameters
    ): CompletableFuture<ExecutionResult> {
        // Create transaction for query execution
        // Adjust transaction settings as required
        val tx = transactionManager.getTransaction(
            object : TransactionDefinition {
                override fun getPropagationBehavior(): Int {
                    return Propagation.REQUIRED

                override fun getIsolationLevel(): Int {
                    return Isolation.SERIALIZABLE

                override fun getTimeout(): Int {
                    return TransactionDefinition.TIMEOUT_DEFAULT

                override fun isReadOnly(): Boolean {
                    return false

                override fun getName(): String? {
                    return super.getName()

        return super.execute(executionContext, parameters)
            // Commit/rollback transaction after execution completes
            .whenComplete { _, throwable ->
                // Roll back for any exception, customize as required
                if (throwable != null) {

                if (tx.isRollbackOnly) {
                } else {


package at.sigmoid

import at.sigmoid.TransactionExecutionStrategy
import graphql.GraphQL
import graphql.schema.idl.RuntimeWiring
import graphql.schema.idl.SchemaGenerator
import graphql.schema.idl.SchemaParser
import graphql.execution.DataFetcherExceptionHandler
import org.springframework.transaction.PlatformTransactionManager

class GraphQLConfiguration {

    fun transactionalExecutionStrategy(
      val transactionManager: PlatformTransactionManager,
      val exceptionHandler: DataFetcherExceptionHandler, // Provide custom exception handler as separate bean
    ) {
      return TransactionalExecutionStrategy(transactionManager, exceptionHandler)

    fun graphql(
        runtimeWiring: RuntimeWiring, // Provide custom runtime wiring as a separate bean
        executionStrategy: TransactionalExecutionStrategy,
    ): GraphQL {
        val sdl = ClassPathResource("schema.graphql").inputStream.use { it.bufferedReader().readText() }
        val typeRegistry = SchemaParser().parse(sdl)
        val schemaGenerator = SchemaGenerator()
        val schema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring)

        return GraphQL