Skip to content

GraphQLSchema.FastBuilder for High-Performance Schema Construction #4196

@rstata

Description

@rstata

Proposal: GraphQLSchema.FastBuilder for High-Performance Schema Construction

Human-written overview

Airbnb's Viaduct platform is written on top of GraphQL Java. Airbnb's schema has grown to be over 25K types with over 120K "members" (fields and enumeration values). At this scale the construction of GraphQL Java's GraphQLSchema object has become a bottleneck. Futher, at both build time and runtime Viaduct creates the same schema (and variants of it) many times, multiplying the performance issues we've seen in this construction.

In the Viaduct project itself we addressed this problem in part by developing a file format that effectively eliminates all parsing-related overhead. By our measurements on large-schema-4:

Benchmark Avg Time Memory
From SDL Text 481 ms 626 MB/op
From optimized file 59 ms 166 MB/op

These numbers measure the time and space required to effectively create a TypeDefinitionRegistry - an 8x speedup and close to 4x reduction in space.

However, it turns out that creating a TypeDefinitionRegistry consumes only half the time it takes overall to construct a GraphQLSchema object, so the improvements just reported resulted in just a 50% improvement. We were looking for something more significant, so we took a look at optimizing GraphQLSchema.Builder. This Builder is very general, in that it allows you to "clear" the builder mid-construction, to update the code-registry factory during construction, and to add just the "roots" of the schema versus adding every single type defined by the schema. Also, the Builder always performs schema validation, which for systems like Viaduct that read the same schema over and over again is redundant and somewhat expensive.

We determined that the current semantics of this Builder were not something we should change, but also not semantics we could effectively optimize. So instead we've developed an alternative "FastBuilder", which is more restrictive in its semantics than is the current Builder, but in turn is significantly less resource intensive. On that same large schema:

Metric Standard Builder FastBuilder Improvement
Build Time 1,933 ms 278 ms 6.96x faster
Memory Allocated 2.71 GB/op 411 MB/op 85% reduction
GC Time (total) 258 ms 183 ms 29% reduction

This measures the time and space required to go from a TypeDefinitionRegistry to a GraphQLSchema.

The rest of this issue was written by an AI Agent - we've checked it for accuracy, but please forgive its length and pedantic tone.

Summary

This proposal introduces GraphQLSchema.FastBuilder, a high-performance alternative to the standard GraphQLSchema.Builder for programmatic schema construction. FastBuilder is not as general as Builder, imposing restrictions on its input that Builder does not. In return, FastBuilder eliminates multiple full-schema traversals during build(), achieving 7x faster build times and 85% reduction in memory allocations for large schemas.

Motivation

The Problem

When building a GraphQLSchema using the standard Builder.build() method, several expensive full-schema traversals occur:

  1. Type collection traversal - Walks the entire type tree to collect all types via SchemaUtil.visitPartiallySchema()
  2. Code registry wiring traversal - Full traversal for data fetcher and type resolver wiring
  3. Type reference replacement traversal - Full traversal using GraphQLTypeResolvingVisitor to replace GraphQLTypeReference placeholders with actual types
  4. Validation traversal - Full schema validation (when enabled)

For schemas with thousands of types, these traversals compound to take significant time and allocate significant (transient) space during schema construction.

Relation to Recent graphql-java Optimizations

The graphql-java project has made progress on schema construction performance in recent releases:

These optimizations focuse on parsing and type definition handling, but the Builder.build() method still performs multiple full-schema traversals that become the dominant cost for programmatic schema construction.

The current PR complements the previous improvements, focusing on the overhead of Builder.build(). Compared to Builder, FastBuilder imposes some restrictions on its clients -- restrictions that are met by the standard TypeDefinitionRegistry path for schema construction -- restrictions that allow it to eliminate the overhead of schema traversal and thus significantly reduce time and space requirements.

Benchmark Results

Using the large-schema-4.graphqls test schema (~18,800 types) with JMH and GC profiling:

Metric Standard Builder FastBuilder Improvement
Build Time 1,933 ms 278 ms 6.96x faster
Memory Allocated 2.71 GB/op 411 MB/op 85% reduction
GC Time (total) 258 ms 183 ms 29% reduction
Full JMH Output
Benchmark                                                                Mode  Cnt           Score         Error   Units
BuildSchemaBenchmark.benchmarkBuildSchemaAvgTime                         avgt    6        1932.933 ±      82.237   ms/op
BuildSchemaBenchmark.benchmarkBuildSchemaAvgTime:gc.alloc.rate           avgt    6        1339.335 ±      55.255  MB/sec
BuildSchemaBenchmark.benchmarkBuildSchemaAvgTime:gc.alloc.rate.norm      avgt    6  2714194399.111 ± 7044683.852    B/op
BuildSchemaBenchmark.benchmarkBuildSchemaAvgTime:gc.count                avgt    6          30.000                counts
BuildSchemaBenchmark.benchmarkBuildSchemaAvgTime:gc.time                 avgt    6         258.000                    ms
BuildSchemaBenchmark.benchmarkBuildSchemaAvgTimeFast                     avgt    6         277.800 ±       8.305   ms/op
BuildSchemaBenchmark.benchmarkBuildSchemaAvgTimeFast:gc.alloc.rate       avgt    6        1410.958 ±      51.101  MB/sec
BuildSchemaBenchmark.benchmarkBuildSchemaAvgTimeFast:gc.alloc.rate.norm  avgt    6   410974008.188 ± 3050567.714    B/op
BuildSchemaBenchmark.benchmarkBuildSchemaAvgTimeFast:gc.count            avgt    6          32.000                counts
BuildSchemaBenchmark.benchmarkBuildSchemaAvgTimeFast:gc.time             avgt    6         183.000                    ms

Benchmark Configuration:

  • JMH 1.37, JDK 21.0.4 (OpenJDK Corretto)
  • Warmup: 2 iterations, 5s each
  • Measurement: 3 iterations, 10s each
  • Forks: 2
  • Profiler: GCProfiler

Use Cases

FastBuilder is designed for scenarios where schemas are constructed programmatically from pre-built type objects:

  1. Code-first schema generation - Frameworks that programmatically construct types and need fast schema assembly
  2. Schema caching/rebuilding - Applications that rebuild schemas from cached type objects
  3. Dynamic schema composition - Systems that compose schemas from type registries or modules
  4. High-throughput services - Services where schema construction latency impacts startup time or request handling
  5. GraphQL federation gateways - Systems that merge multiple subgraph schemas into a supergraph

FastBuilder is not intended to replace the standard builder for:

  • SDL-first development (use SchemaParser + SchemaGenerator)
  • Small schemas where construction time is negligible
  • Cases requiring automatic type discovery from root types
  • Scenarios requiring full validation on every build

Commit Structure

To help with reviewing of this change, we implemented as a stacked set of commits. Each commit has a descriptive message body explaining its contents.

  fastbuilder (11 commits on top of assertValidName)
    └── Add FastSchemaGenerator and JMH benchmark infrastructure
    └── Phase 9: Validation and Edge Cases - FastBuilder implementation  ← now includes FindDetachedTypes
    └── Phase 8: Union Types
    └── Phase 7: Interface Types
    └── Phase 6: Object Types
    └── Phase 5: Applied directives
    └── Phase 4: Input object types
    └── Phase 3: Enumeration types
    └── Phase 2: Directives
    └── Phase 1: Scaffolding
        │
        Optimize assertValidName
        │
  upstream/master

We call out the assertValidName optimization because it's a improvement that should be considered on its own basis even if the rest of these changes are rejected.

Design

Architecture

FastBuilder is implemented as a public static inner class of GraphQLSchema:

GraphQLSchema.FastBuilder
├── typeMap: Map<String, GraphQLNamedType>           # Explicit type registry
├── directiveMap: Map<String, GraphQLDirective>      # Directive definitions
├── shallowTypeRefCollector: ShallowTypeRefCollector # Incremental type-ref tracking
├── codeRegistryBuilder: GraphQLCodeRegistry.Builder # Code registry
└── interfacesToImplementations: Map<...>            # Interface->impl mapping

Key Design Principles

FastBuilder takes an "expert mode" approach with stricter assumptions that enable incremental processing:

  • Incremental work only: When a type is added, only that node is examined (no traversals)
  • Shallow scans: Type references collected via local inspection, not recursive traversal
  • Explicit types: All named types must be explicitly provided via additionalType()
  • Optional validation: Validation is disabled by default (configurable via withValidation(true))
  • No clearing/resetting: Types and directives cannot be removed once added

Supporting Classes

  1. ShallowTypeRefCollector (@Internal)

    • Performs shallow scans of types as they're added
    • Collects type reference replacement targets without recursive traversal
    • Tracks interface-to-implementation mappings incrementally
    • Handles all GraphQL type kinds: objects, interfaces, unions, inputs, enums, scalars
  2. FindDetachedTypes (@Internal)

    • DFS traversal to compute "additional types" (types not reachable from root types)
    • Considers types reachable from Query, Mutation, Subscription, and directive arguments
    • Used at schema construction time to populate getAdditionalTypes()

Build Process

FastBuilder's build() method:

public GraphQLSchema build() {
    // Step 1: Replace type references using collected targets (no full traversal)
    shallowTypeRefCollector.replaceTypes(typeMap);

    // Step 2: Add built-in directives if missing (@skip, @include, etc.)
    addBuiltInDirectivesIfMissing();

    // Step 3: Create schema via new private constructor (no traversal)
    GraphQLSchema schema = new GraphQLSchema(this);

    // Step 4: Optional validation (only when explicitly enabled)
    if (validationEnabled) {
        List<SchemaValidationError> errors = new SchemaValidator().validateSchema(schema);
        if (!errors.isEmpty()) throw new InvalidSchemaException(errors);
    }

    return schema;
}

API

@ExperimentalApi
public static final class FastBuilder {
    // Construction - requires code registry and at least Query type
    FastBuilder(GraphQLCodeRegistry.Builder codeRegistryBuilder,
                GraphQLObjectType queryType,
                @Nullable GraphQLObjectType mutationType,
                @Nullable GraphQLObjectType subscriptionType)

    // Type management - all types must be explicitly added
    FastBuilder additionalType(GraphQLType type)
    FastBuilder additionalTypes(Collection<? extends GraphQLType> types)

    // Directive management
    FastBuilder additionalDirective(GraphQLDirective directive)
    FastBuilder additionalDirectives(Collection<? extends GraphQLDirective> directives)

    // Schema-level directives
    FastBuilder withSchemaDirective(GraphQLDirective directive)
    FastBuilder withSchemaAppliedDirective(GraphQLAppliedDirective applied)

    // Metadata
    FastBuilder description(String description)
    FastBuilder definition(SchemaDefinition def)

    // Configuration
    FastBuilder withValidation(boolean enabled)  // default: false

    // Build
    GraphQLSchema build()
}

Example Usage

// Create types with type references for circular relationships
GraphQLObjectType userType = newObject()
    .name("User")
    .field(newFieldDefinition().name("id").type(GraphQLID))
    .field(newFieldDefinition().name("posts").type(list(typeRef("Post"))))
    .build();

GraphQLObjectType postType = newObject()
    .name("Post")
    .field(newFieldDefinition().name("id").type(GraphQLID))
    .field(newFieldDefinition().name("author").type(typeRef("User")))
    .build();

GraphQLObjectType queryType = newObject()
    .name("Query")
    .field(newFieldDefinition().name("user").type(typeRef("User")))
    .build();

// Build schema with FastBuilder
GraphQLCodeRegistry.Builder codeRegistry = GraphQLCodeRegistry.newCodeRegistry();

GraphQLSchema schema = new GraphQLSchema.FastBuilder(codeRegistry, queryType, null, null)
    .additionalType(userType)
    .additionalType(postType)
    .build();

Testing

The implementation includes comprehensive testing (132 tests across 4 test classes):

Test Strategy: Comparison-First

The primary validation approach compares schemas built with FastBuilder against identical schemas built via the standard SDL parsing path. This ensures semantic equivalence with real-world schema construction.

Test Organization

Test Class Tests Focus
FindDetachedTypesTest.groovy 24 DFS traversal logic for additional types
FastBuilderComparisonAdditionalTypesTest.groovy 11 getAdditionalTypes() equivalence
FastBuilderComparisonInterfaceTest.groovy 6 Interface implementation tracking
FastBuilderComparisonTypeRefTest.groovy 18 Type reference resolution
FastBuilderComparisonComplexTest.groovy 10 Full schema equivalence
FastBuilderTest.groovy 53 FastBuilder-specific behavior

Verified Invariants

FastBuilder-built schemas maintain these invariants with standard-built schemas:

  1. Type map equivalence - Same types accessible via getType(name)
  2. Additional types - Same set of "detached" types returned by getAdditionalTypes()
  3. Interface implementations - getImplementations(interface) returns same implementations in same alphabetical order
  4. Directive definitions - Same directive definitions available
  5. Root types - Query, Mutation, Subscription types match
  6. Code registry - Data fetchers and type resolvers properly wired

Breaking Changes

None. FastBuilder is a new, additive API. Existing code using GraphQLSchema.Builder is unaffected.

Implementation Notes

  • Annotation: Marked as @ExperimentalApi initially, allowing API refinement based on real-world feedback before hardening to @PublicApi
  • Thread Safety: FastBuilder is not thread-safe (same as standard Builder)
  • Dependencies: No new dependencies added (adheres to project policy)
  • Code Style: Follows graphql-java conventions (IntelliJ code style, no wildcard imports, etc.)

Related Work

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions