-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
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:
- Type collection traversal - Walks the entire type tree to collect all types via
SchemaUtil.visitPartiallySchema() - Code registry wiring traversal - Full traversal for data fetcher and type resolver wiring
- Type reference replacement traversal - Full traversal using
GraphQLTypeResolvingVisitorto replaceGraphQLTypeReferenceplaceholders with actual types - 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:
- v25.0: Eliminated CompletableFuture wrapping overhead, replaced stream operations with direct loops
- ImmutableTypeDefinitionRegistry (PR Added a read only copy of a type registry for performance reasons #4021): Prevents unnecessary defensive copies during schema generation
- makeSchema Performance Fix (PR Performance issue in
makeSchemawith schemas containing a lot of extensions #4020): Addressed repeatedtypeRegistry.extensions()allocations
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:
- Code-first schema generation - Frameworks that programmatically construct types and need fast schema assembly
- Schema caching/rebuilding - Applications that rebuild schemas from cached type objects
- Dynamic schema composition - Systems that compose schemas from type registries or modules
- High-throughput services - Services where schema construction latency impacts startup time or request handling
- 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
-
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
-
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:
- Type map equivalence - Same types accessible via
getType(name) - Additional types - Same set of "detached" types returned by
getAdditionalTypes() - Interface implementations -
getImplementations(interface)returns same implementations in same alphabetical order - Directive definitions - Same directive definitions available
- Root types - Query, Mutation, Subscription types match
- 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
@ExperimentalApiinitially, 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
- graphql-java Issue 1% possible memory and cpu improvements #3939 - "1% club" micro-optimizations identifying builder pattern overhead
- graphql-java Issue Look into memory allocation of ExecutionStrategyParameters #3933 - ExecutionStrategyParameters memory allocation concerns
- graphql-java PR Added a read only copy of a type registry for performance reasons #4021 - ImmutableTypeDefinitionRegistry
- graphql-java PR Performance issue in
makeSchemawith schemas containing a lot of extensions #4020 - makeSchema performance fix