Multi-tenancy
Viaduct multi-tenancy is a source and build structure for letting multiple teams contribute independently owned GraphQL schema partitions and resolver code to one Viaduct application.
A tenant is one Gradle project that applies the Viaduct module plugin. It contributes a schema partition and the resolver implementations for that partition. The settings topology declares the application project, tenant projects, and package layout. The application project applies the Viaduct application plugin and builds the Viaduct runtime that serves the composed schema.
This page is for service engineers setting up that source and build structure. For resolver authoring patterns, see the Developers section. For a worked example, see the Star Wars tutorial.
The Two Viaduct Gradle Plugins¶
A multi-tenant Viaduct application uses two Gradle plugins:
com.airbnb.viaduct.application-gradle-pluginmarks the project that builds theViaductruntime. This is usually the host service project: the project with the HTTP server, dependency-injection setup, runtime configuration, observability, and deployment packaging.com.airbnb.viaduct.module-gradle-pluginmarks a project that contributes tenant code. A tenant project contains GraphQL SDL files and the Kotlin resolvers that implement that SDL.
The application plugin coordinates the application-level build, aggregating the results of module-level builds into the larger application. The module plugin handles the module-local build process.
Recommended Project Shape¶
Use a Gradle multi-project build. One common shape is:
settings.gradle.kts
build.gradle.kts # application project
common/build.gradle.kts # optional shared JVM code, not a Viaduct tenant
tenants/catalog/build.gradle.kts # tenant project
tenants/orders/build.gradle.kts # tenant project
tenants/users/build.gradle.kts # tenant project
The exact directory names are your choice; modules, tenants, or domain-specific names are all fine as long as Gradle includes the projects and the Viaduct topology declares them. The application project applies viaduct.application. Tenant projects apply viaduct.module. Shared library projects do not apply either Viaduct plugin.
Settings Topology¶
Declare the Viaduct application and tenant modules in settings.gradle.kts:
plugins {
id("com.airbnb.viaduct.settings-gradle-plugin")
}
includeViaductApplication {
project(":")
modulePackagePrefix("com.example.myservice")
includeModule {
project(":tenants:catalog")
modulePackageSuffix("catalog")
}
includeModule {
project(":tenants:orders")
modulePackageSuffix("orders")
}
includeModule {
project(":tenants:users")
modulePackageSuffix("users")
}
}
The application plugin uses this topology to wire module schema partitions and runtime dependencies. Module plugins use the same topology to find the owning application and generate code under the declared package layout.
Application Project¶
The application project owns the runtime and should apply the application plugin:
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.viaduct.application)
}
dependencies {
implementation(libs.viaduct.api)
implementation(libs.viaduct.runtime)
}
The application project is also where service-engineering concerns usually live:
- constructing the
Viaductinstance withBasicViaductFactoryorViaductBuilder - integrating with the web framework or message consumer
- configuring dependency injection for resolver classes
- configuring schema variants, observability, feature flags, and deployment packaging
Tenant projects should normally be listed in the Viaduct settings topology rather than manually added as application project dependencies. The application plugin adds topology modules to the application runtime classpath so Viaduct can discover their generated metadata and resolver classes, while keeping tenant implementation code out of the application project's compilation classpath.
Tenant Projects¶
Each tenant project applies the module plugin:
plugins {
`java-library`
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.ksp)
alias(libs.plugins.viaduct.module)
}
dependencies {
api(libs.viaduct.api)
implementation(libs.viaduct.runtime)
}
A tenant project usually contains:
tenants/catalog/
build.gradle.kts
src/main/viaduct/schema/
Product.graphqls
Query.graphqls
src/main/kotlin/com/example/myservice/catalog/
ProductNodeResolver.kt
ProductSearchResolver.kt
The src/main/viaduct/schema directory is the convention the module plugin uses for tenant SDL. The Kotlin package should align with the settings topology modulePackagePrefix plus the tenant project's modulePackageSuffix.
Warning
Viaduct validates topology package prefixes and suffixes as dotted identifier segments, but it does not reject every reserved word from every resolver implementation language. Service engineers should consider adding application-specific Gradle linting to ensure modulePackagePrefix and modulePackageSuffix values are valid package names for the languages their resolver teams use.
For example, with modulePackagePrefix("com.example.myservice") on the application topology and modulePackageSuffix("catalog") on a tenant module, resolver implementations for that tenant should live under a package such as:
Central Schema Assembly¶
Each tenant contributes SDL files from its own src/main/viaduct/schema directory. During the application build, Viaduct assembles those schema partitions into one central schema.
This central schema is the schema served by the application runtime. A type defined in one tenant can be referenced or extended by another tenant because all tenant schema partitions contribute to the same central schema.
For example, one tenant can define a type:
type Product @scope(to: ["default"]) {
id: ID!
title: String!
}
Another tenant can extend it:
extend type Product @scope(to: ["default"]) {
recentOrders: [Order!]! @resolver
}
This is schema composition, not Gradle dependency resolution. If tenant code needs to call Kotlin classes from another project, model that as a normal Gradle dependency. But GraphQL type visibility comes from central schema assembly, not from tenant-to-tenant Gradle dependencies.
Source Layout Choices¶
The service engineer should choose a layout that makes ownership and build behavior obvious. Good layouts usually have these properties:
- Tenant projects are small enough that one team can own their schema and resolver code.
- Shared JVM code lives in ordinary library projects, separate from tenant schema partitions.
- Tenant project names and package suffixes are stable, because they shape generated code and resolver discovery.
- The Viaduct settings topology lists every tenant served by the application.
- The application project is the only place that owns runtime integration decisions.
Organizing Large Schemas by Domain¶
For large schemas, use ordinary source layout and package naming to group tenants by domain or subdomain. The intermediate directories do not need to be Gradle projects; they can simply create a readable namespace.
In that layout, the tenant modules might use settings-topology package suffixes such as:
and:
This keeps generated code and resolver discovery aligned with the ownership structure without implying that every directory is a Gradle project.
What Belongs in Each Project¶
Application project:
- web server or message consumer entry point
BasicViaductFactoryorViaductBuilderconfiguration- dependency injection integration
- schema scoping configuration
- observability and error handling configuration
Tenant project:
- GraphQL SDL for one owned slice of the central schema
- resolvers for fields declared by that tenant
- tenant-local tests
- tenant-specific data loaders or adapters when they are not shared elsewhere
Shared library project:
- domain models shared by multiple tenants
- backend clients used by multiple tenants
- framework-neutral utilities
- no Viaduct plugin unless the project also contributes SDL and resolvers
Common Setup Mistakes¶
Tenant Project Not on the Runtime Classpath¶
If a tenant project applies the module plugin but the application does not package its tenant metadata and resolver classes at runtime, first check that the tenant is declared in the settings topology:
includeViaductApplication {
project(":app")
modulePackagePrefix("com.example.app")
includeModule {
project(":tenants:catalog")
modulePackageSuffix("catalog")
}
}
The application plugin wires topology-declared tenant modules onto the runtime classpath. If runtime metadata is still missing, verify that the application and tenant plugins are applied to the projects named in settings, and inspect the application's runtime classpath rather than adding a manual runtimeOnly(project(...)) edge.
Package Prefix and Suffix Do Not Line Up¶
If resolvers are not found at runtime, check that the settings topology modulePackagePrefix and each tenant project's modulePackageSuffix match the Kotlin packages where resolver classes are compiled.
Treating Tenant Schema Composition as a Gradle Dependency Graph¶
Tenant projects build independently as Gradle projects. Viaduct assembles their schema partitions into the central schema at the application level. Do not document tenant-to-tenant GraphQL relationships as Gradle dependency resolution.
Use normal Gradle dependencies only for ordinary Kotlin/JVM code dependencies.