Skip to main content

What is a Central Schema?

Viaduct serves a central schema: a single, integrated GraphQL schema connecting all domains across your organization. While this schema is developed in a decentralized manner by many teams, it presents as one highly connected graph to clients.
The central schema is the complete, unified GraphQL schema that results from merging all tenant module schemas together. It represents the full graph of your organization’s data and capabilities.

Key Principles

Single Graph

One unified schema connecting all domains, not a collection of separate schemas

Decentralized Development

Multiple teams independently contribute types and fields to the central schema

Type Extensions

Teams can extend types owned by other teams without direct code dependencies

Schema Composition

All team schemas are merged at build time into the central schema

How Teams Contribute

Teams contribute to the central schema through tenant modules. Each module contains:
  • GraphQL SDL files defining types, queries, mutations
  • Resolvers implementing the business logic for those fields
  • Independence from other modules’ code

Example: Two Teams Collaborating

Let’s look at how two teams contribute to a User type:
// modules/universe/schema/User.graphqls
type User implements Node {
  id: ID!
  firstName: String
  lastName: String
  email: String
}

extend type Query {
  user(id: ID!): User
}
After composition, the central schema contains:
type User implements Node {
  id: ID!
  firstName: String
  lastName: String
  email: String
  # Fields added by Messaging team:
  displayName: String
  messageCount: Int
}
The Messaging team’s resolvers can access firstName and lastName through GraphQL fragments without depending on the Core User team’s code.

Schema Composition Process

Viaduct merges tenant schemas at build time through a multi-step process:
1

Discovery

Scan all tenant module directories for .graphqls files in src/main/viaduct/schema/
2

Collection

Gather all SDL files per module, respecting module dependencies
3

Merging

Combine types using GraphQL’s extend keyword, following type system rules
4

Validation

Ensure the composed schema is valid according to GraphQL specification
5

Code Generation

Generate GRTs and resolver base classes from the central schema

Module Dependencies

Modules form a dependency graph that determines composition order:
presentation → data → entity → entity/common
When you build a tenant, Viaduct automatically includes schemas from ancestor modules:
  • Building presentation/checkout includes: presentation, data, entity, entity/common
  • Building entity/user includes: entity, entity/common
When extending types across modules, ensure the extending module depends on the module containing the original type definition.

Real-World Example: Star Wars Demo

The Star Wars demo application demonstrates central schema composition with two modules:

Universe Module

Defines core Star Wars entities:
modules/universe/src/main/viaduct/schema/Planet.graphqls
type Planet implements Node @scope(to: ["default"]) @resolver {
  id: ID!
  name: String
  diameter: Int
  rotationPeriod: Int
  orbitalPeriod: Int
  gravity: Float
  population: Float
  surfaceWater: Float
  terrains: [String]
  climates: [String]
  created: String
  edited: String
}

extend type Query @scope(to: ["default"]) {
  allPlanets(limit: Int): [Planet] @resolver
}

Filmography Module

Extends entities with film-related fields:
modules/filmography/src/main/viaduct/schema/Character.graphqls
type Character implements Node @scope(to: ["default"]) @resolver {
  id: ID!
  name: String
  birthYear: String
  eyeColor: String
  gender: String
  hairColor: String
  height: Int
  mass: Float
  created: String
  edited: String
  
  # Filmography module adds these relationships:
  isAdult: Boolean @resolver
  homeworld: Planet @resolver
  species: Species @resolver
  displayName: String @resolver
  filmCount: Int @resolver
}

extend type Query @scope(to: ["default"]) {
  allCharacters(limit: Int): [Character] @resolver
  searchCharacter(search: CharacterSearchInput!): Character @resolver
}
Notice how the Character type references Planet and Species from the universe module. The central schema makes these cross-module relationships seamless.

Common Types and Shared Modules

For types used across multiple tenants, create a common module:
modules/common/schema/CommonTypes.graphqls
type Address @scope(to: ["default"]) {
  street: String!
  city: String!
  country: String!
}

type Money @scope(to: ["default"]) {
  amount: BigDecimal!
  currency: String!
}

type Coordinates @scope(to: ["default"]) {
  latitude: Float!
  longitude: Float!
}
All modules depending on common can reference these types:
type Listing {
  id: ID!
  title: String!
  address: Address      # References common type
  price: Money          # References common type
  location: Coordinates # References common type
}

Schema Variants with Scopes

The central schema can expose different variants using scopes. All tenants contribute to the same central schema, but fields can be marked with different scopes:
type Species @scope(to: ["default"]) {
  id: ID!
  name: String
  classification: String
  
  # Only visible in "extras" schema:
  technologicalLevel: String @scope(to: ["extras"])
  culturalNotes: String @scope(to: ["extras"])
  rarityLevel: String @scope(to: ["extras"])
}

Registering Schema Variants

Service engineers configure which scopes map to which schema IDs:
val schemaConfig = SchemaConfiguration.builder()
    .registerSchema(
        schemaId = "PUBLIC_SCHEMA",
        scopes = setOf("default")
    )
    .registerSchema(
        schemaId = "PUBLIC_SCHEMA_WITH_EXTRAS",
        scopes = setOf("default", "extras")
    )
    .build()
At runtime, the controller selects which schema variant to use:
val schemaId = when {
    request.scopes.contains("extras") -> "PUBLIC_SCHEMA_WITH_EXTRAS"
    else -> "PUBLIC_SCHEMA"
}

val result = viaduct.execute(executionInput, schemaId)
Scopes provide schema visibility control without creating separate graphs. All teams still contribute to the same central schema.

Benefits of a Central Schema

For Developers

One Graph

Query across domains in a single request without orchestrating multiple services

Type Safety

Strong typing across module boundaries with generated code

No Duplication

Shared types prevent duplicate definitions across teams

Discovery

Browse the entire graph through introspection

For Organizations

  • Faster Development: Teams extend existing types instead of creating new APIs
  • Better Client Experience: Single endpoint for all data needs
  • Reduced Coordination: Teams work independently on their schema portions
  • Consistent Data Model: One source of truth for your domain model

Schema Governance

With a central schema, consider these governance practices:

Naming Conventions

Establish consistent naming for types and fields:
# Good: Descriptive, consistent naming
type ListingReservation {
  id: ID!
  checkInDate: Date!
  checkOutDate: Date!
}

# Avoid: Inconsistent or unclear naming
type Reservation {
  id: ID!
  start: String  # What format? Date or DateTime?
  finish: String
}

Field Ownership

Clearly document which team owns each type:
"""
User entity owned by the Core User team.
Contact @core-user-team before extending.
"""
type User implements Node {
  id: ID!
  # ...
}

Breaking Change Prevention

Removing or renaming fields in the central schema is a breaking change. Use deprecation instead:
type User {
  id: ID!
  # Deprecated - use displayName instead
  name: String @deprecated(reason: "Use displayName")
  displayName: String
}

Schema Reviews

Implement review processes for schema changes:
  • Automated validation: Check for breaking changes in CI
  • Type ownership: Require approval from owning team for extensions
  • Naming review: Ensure consistency with existing conventions

Compilation Schemas

While Viaduct maintains a central schema, each tenant module only generates code for the types it actually uses:
A compilation schema is a per-tenant-module, private view of the central schema consisting of only the schema elements that module uses.
This optimization:
  • Keeps build times fast even with a large central schema
  • Enables parallel builds of tenant modules
  • Only rebuilds when a module’s dependencies change
Example:
  • Central schema has 500 types
  • filmography/characters module only uses 20 types
  • Only those 20 types (and their dependencies) are in the compilation schema
  • Generated code is minimal and builds quickly

Troubleshooting

Type Not Found Errors

If you get “unresolved reference” errors for a type:
  1. Ensure the module defining that type is in your dependencies
  2. Check that schema files are in the correct location: src/main/viaduct/schema/
  3. Verify the type is included in your scope configuration

Duplicate Type Definitions

If you see “type already defined” errors:
  1. Use extend type instead of redefining the type
  2. Check that only one module defines the base type
  3. Review module dependencies to ensure proper composition order

Schema Composition Failures

If schema validation fails after composition:
  1. Run introspection queries to examine the composed schema
  2. Check for conflicting field definitions on extended types
  3. Ensure all referenced types are defined or extended with proper scopes

See Also

Build docs developers (and LLMs) love