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.

Loaders.kt

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) } }
}

TransactionalExecutionStrategy.kt

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) {
                    tx.setRollbackOnly()
                }

                if (tx.isRollbackOnly) {
                    transactionManager.rollback(tx)
                } else {
                    transactionManager.commit(tx)
                }
            }
    }
}

GraphQLConfiguration.kt

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.core.io.ClassPathResource
import org.springframework.transaction.PlatformTransactionManager

@Configuration
class GraphQLConfiguration {

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

    @Bean
    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
            .newGraphQL(schema)
            .mutationExecutionStrategy(executionStrategy)
            .queryExecutionStrategy(executionStrategy)
            .build()
    }
}