skip to content

How does Spring merge multiple .graphqls files, and how do you use `extend type` and split a large schema across files?

level: principalimportance: nice to knowfreq 25%

answer

  1. classpath:graphql/**/ merges all files
  2. base type once + extend type many times
  3. duplicate non-extend type = merge error
  4. extended field still needs a resolver
  5. GraphQlSourceBuilderCustomizer.schemaResources(...) / federation

basics

~20 s

All files matching classpath:graphql/**/ are parsed and merged into one schema, so you can split types across files by domain. To add fields to a type defined elsewhere, use extend type Query { ... }. The base type must be defined exactly once across the merged set.

solid answer

~50 s

Because the default location is classpath:graphql/**/, Spring reads every matching SDL file — across subfolders — parses each into a TypeDefinitionRegistry, and merges them into a single schema before building. This lets you organize SDL by domain: one file per bounded context, each contributing to the root types. The idiomatic pattern is that one file declares base `type Query`/`type Mutation`, and other files use `extend type Query { bookById(id: ID!): Book }` to append their fields; GraphQL requires the base type be declared once, with any number of `extend`s. You can also split object types the same way. Beyond files, GraphQlSourceBuilderCustomizer can add schema resources programmatically or from other locations, and Spring supports federation (spring-graphql federation support) when composing a subgraph. Care points: duplicate non-extend definitions of the same type fail merging, and every extended field still needs a resolver.

code

java · 22 lines
java
// resources/graphql/base.graphqls
//   type Query    { ping: String }
//   type Mutation { ping: String }
//
// resources/graphql/book/book.graphqls
//   extend type Query    { bookById(id: ID!): Book }
//   extend type Mutation { addBook(title: String!): Book }
//   type Book { id: ID!  title: String! }

// Optional: add SDL from a non-default location programmatically.
@Bean
GraphQlSourceBuilderCustomizer extraSchema(
        @Value("classpath:extra/legacy.graphqls") Resource legacy) {
    return builder -> builder.schemaResources(legacy);
}

// Each extended field still needs a resolver:
@Controller
class BookController {
    @QueryMapping   Book bookById(@Argument String id) { ... }
    @MutationMapping Book addBook(@Argument String title) { ... }
}

go deeper

for a junior

Know multiple files can exist and merge into one schema.

for a middle

Use extend type Query to split root fields; know duplicate base types fail.

for a senior

Organize SDL by domain, wire each extended field, add resources via GraphQlSourceBuilderCustomizer.

for a principal

Reason about modular schema governance, composed-SDL artifacts in CI, module coupling via cross-type extends, and federation across services.

**Merging model.** The auto-configuration resolves `spring.graphql.schema.locations` (default `classpath:graphql/**/`) to *all* matching resources — the `**/` makes it recursive across subfolders — parses each into a GraphQL-Java **`TypeDefinitionRegistry`**, and **merges** the registries into one before generating the executable `GraphQLSchema`. Practically: drop as many `.graphqls` files as you like under `resources/graphql/` (including nested folders like `graphql/book/`, `graphql/author/`), and they compose into one schema. **Splitting by domain — the `extend` pattern.** GraphQL's root operation types (`Query`, `Mutation`, `Subscription`) must each be declared **once**. To let many files contribute root fields, one file declares the base and others *extend* it: ```graphql # base.graphqls type Query { _service: String } # or a placeholder / first real field # book.graphqls extend type Query { bookById(id: ID!): Book } extend type Mutation { addBook(title: String!): Book } type Book { id: ID! title: String! } ``` Rules of `extend`: - The base `type X { ... }` must appear **exactly once** across all merged files. - `extend type X { ... }` may appear any number of times and only **adds** fields (or interfaces/directives); it cannot redefine existing fields. - If two files both do `type Query { ... }` (not `extend`), merging fails with a duplicate-type error. - You can `extend` any object type, not just roots — useful when a cross-cutting module adds a field to a shared type. Field-name collisions across extends of the same type are errors. - If `type Mutation`/`type Subscription` don't exist yet, some setups still require a base declaration before extending; declare a base to be safe. **Every extended field still needs wiring.** `extend type Query { bookById... }` only declares the field; you still provide `@QueryMapping bookById(...)` (or a `DataFetcher`). The schema-mapping inspection report will flag extended fields left unimplemented. **Programmatic / non-classpath schemas.** For advanced composition — pulling SDL from a URL, another module's jar at a non-default path, or building parts in code — register a **`GraphQlSourceBuilderCustomizer`** and call `builder.schemaResources(...)` to add resources, or change `spring.graphql.schema.locations` to include extra locations (comma-separated). **Federation.** For large orgs splitting a graph across services, Spring for GraphQL offers **Apollo Federation** support (the `spring-graphql` federation module / `FederationSchemaFactory`), where each service owns a **subgraph** SDL and a gateway composes them. That is a distributed cousin of multi-file merging: instead of merging files in one app, you merge subgraph schemas across services, using `@key`/`extend`/entity resolvers. **Gotchas & governance (principal lens).** - Ordering of files doesn't matter for merging, but a missing base type does — enforce a convention (one `base.graphqls` owning root type stubs). - Splitting SDL improves modularity but can hide the full contract; teams often generate a *composed* SDL artifact in CI for review and for schema-registry breaking-change checks. - Keep module boundaries: a module extending another module's type couples them — decide deliberately, as with Spring Modulith module boundaries. - Custom scalars and type resolvers are still global to the merged schema; register them once via `RuntimeWiringConfigurer` regardless of how many files declare usages.

  • Two files each contain `type Query { ... }`. What happens at startup?
    Merging fails with a duplicate type definition error. Exactly one file may declare the base `type Query`; the rest must use `extend type Query`.
  • How would you compose a schema across multiple microservices rather than files in one app?
    Use Spring for GraphQL's Apollo Federation support: each service exposes a subgraph SDL with @key/entity resolvers, and a federated gateway composes them into one supergraph.

saying these in an interview costs you the question

  • Thinking each file becomes a separate schema/endpoint — they merge into one.
  • Redefining `type Query` in multiple files instead of using `extend`.
  • Assuming `extend` auto-implements the field — it still needs a resolver.

context