DGS Meets Spring GraphQL: The Merger Nobody Asked For, Everyone Needed

For six years, the Java GraphQL world had two doors. Door One: Netflix DGS, born at a company whose entire product is one giant graph. Door Two: Spring GraphQL, blessed by the Spring team itself. Pick a door, write your code, and pray you never had to switch. Then somebody at Netflix and somebody at VMware had coffee, and the doors got knocked down. Here's what actually happened, what it means for your codebase, and whether you should care.
Two Frameworks, Same Engine
First, the thing nobody tells newcomers: both DGS and Spring GraphQL sit on top of the same execution engine.
Your annotated Spring controllers
Your @DgsComponent classes
↓↓
Spring GraphQL / DGS
↓↓
graphql-java
↓↓
Your schema
Underneath, it's graphql-java running the actual query. The frameworks are two different layers of Java ergonomics piled on top. Different annotations, different lifecycles, different ways to wire a DataLoader. But the query parsing, execution, and subscription plumbing is the same code underneath.
This is important because it's why the integration was even possible. They weren't arguing about the engine. They were arguing about the steering wheel.
A Brief History of the Two Doors
DGS (Domain Graph Service) shipped from Netflix in February 2021. Netflix had been running graphql-java at scale for years and wanted a more opinionated developer experience. They built annotations like @DgsQuery, @DgsData, @DgsDataLoader, and @DgsEntityFetcher. They added codegen. They baked in federation support. It felt like a framework, not a library.
Spring GraphQL shipped in 2022 from the Spring team. It arrived with @QueryMapping, @MutationMapping, @SchemaMapping, @BatchMapping. It looked like the rest of Spring: same lifecycle, same conventions, same @Controller mental model. It was less opinionated than DGS, more integrated with Spring Security and Spring WebFlux.
For roughly two years (Spring GraphQL 1.0 in May 2022 to DGS 8.5.0 with first-class Spring GraphQL integration in March 2024), picking one meant you couldn't use the other.
| Feature | DGS | Spring GraphQL |
|---|---|---|
| Query mapping | @DgsQuery | @QueryMapping |
| Field resolver | @DgsData | @SchemaMapping |
| Batching | @DgsDataLoader | @BatchMapping / DataLoader |
| Subscriptions | @DgsSubscription | @SubscriptionMapping |
| Federation | First-class | Via extension |
| Code generation | Built-in plugin | External (graphql-codegen) |
| Testing | DgsQueryExecutor | GraphQlTester |
Both work. Both are production-grade. But for years, adopting one was a bet, and the bets diverged.
The Merger, or "Integration," or Whatever We're Calling It
The Netflix blog post A Tale of Two Frameworks tells the story. Boiled down: DGS now runs on top of Spring GraphQL.
This is not "DGS is deprecated." This is not "Spring GraphQL absorbed DGS." This is "DGS keeps its annotations and developer experience, but the request handling, transport, and wiring happen inside Spring GraphQL." The DGS Schema Provider is responsible for wiring up data fetchers for both programming models simultaneously.
- Your existing DGS code keeps working.
- Your existing Spring GraphQL code keeps working.
- You can mix them in the same app, with the caveat that Netflix recommends sticking with one style outside of migration windows.
- Transport, websockets, security integration: one implementation, shared.
- The DGS team focuses on the developer experience layer.
- The Spring GraphQL team focuses on the framework plumbing.
The elegant outcome: two teams stop building the same wheel twice, and users stop having to pick a door.
Mixing the Two: A Concrete Example
Here's what was impossible in 2022 and is expected of any server now:
// DGS-style for the parts where DGS feels right
@DgsComponent
public class MovieFetcher {
private final MovieService movieService;
public MovieFetcher(MovieService movieService) {
this.movieService = movieService;
}
@DgsQuery
public Movie movie(@InputArgument Long id) {
return movieService.findById(id);
}
@DgsEntityFetcher(name = "Movie")
public Movie resolveMovieEntity(Map<String, Object> values) {
return movieService.findById(Long.valueOf((String) values.get("id")));
}
}
// Spring GraphQL style for the parts where it feels right
@Controller
public class MovieController {
private final ReviewService reviewService;
public MovieController(ReviewService reviewService) {
this.reviewService = reviewService;
}
@BatchMapping(typeName = "Movie")
public Map<Movie, List<Review>> reviews(List<Movie> movies) {
return reviewService.findByMovies(movies);
}
@SubscriptionMapping
public Flux<Rating> ratingUpdates(@Argument Long movieId) {
return reviewService.streamRatings(movieId);
}
}
Same schema. Same app. One process. One port. Both frameworks coexist because underneath, they're writing data fetchers into the same graphql-java GraphQL instance.
This matters for teams with legacy DGS code who want to start using @BatchMapping without a rewrite. It matters for Spring teams that inherit a DGS module and want to keep it.
Worth flagging: Netflix's own DGS documentation recommends sticking with one programming model in steady-state code. The integration calls mixing "technically possible" instead of "officially endorsed," and some advanced features (notably DataLoader Scheduled Dispatch) do not work cleanly across both styles in the same app. Treat the mixing pattern as a migration tool, not a permanent architecture.
The Version Map (As of June 2026)
Keeping versions straight has been a whole subplot of the Java GraphQL story. Here's where things actually land:
| Component | Current LTS | Status |
|---|---|---|
| DGS 12.x | Spring Boot 4.x, Java 17+ | Current (Jackson 3 by default; Jackson 2 opt-in) |
| DGS 11.x | Spring Boot 4.x, Java 17+ | Prior major (Jackson 2 only) |
| DGS 10.x | Spring Boot 3.x, Java 17+ | Expected backport maintenance through 2026 |
| Spring GraphQL 1.4.x | Spring Boot 3.x | Maintenance |
| Spring GraphQL 2.x | Spring Boot 4.x | Current |
| graphql-java 25.x | Java 11+ (Java 17+ in the Spring Boot 4 stack) | Current (baselined by Spring for GraphQL 2.0.0, Nov 2025) |
Your runway to upgrade is roughly the back half of 2026. Netflix has not committed to a hard end date, but the DGS 11 release notes nudge Spring Boot 3 users to stay on 10.x only until they are ready for Spring Boot 4. Plan the Spring Boot 4 + DGS 12 upgrade now. The Spring Boot 4.0 Release Notes cover the bulk of the migration; most of the pain is not DGS-specific.
The Opinionated vs. Unopinionated Divide
Now for the thing everyone really wants to know: which one should you use for a greenfield project in 2026?
DGS and Spring GraphQL make different bets about what your team values.
DGS: Opinionated By Design
DGS has conventions. The codegen output has a shape. The annotations have expectations. If you pick up a DGS codebase from another team, you can mostly predict where things are.
DGS leans into:
- Code generation as a first-class workflow (schema-first from day one)
- Federation with first-class directives
- Entity fetchers for subgraph membership
DgsQueryExecutorfor unit tests that are actually easy to write
Spring GraphQL: Conventions Over Opinions
Spring GraphQL is lighter. It plugs into the Spring way: @Controller, @Component, @ConditionalOnMissingBean. If you know Spring, you already know where things go.
Spring GraphQL leans into:
- Less framework, more library
- Tight integration with Spring Security, Spring Data, WebFlux
GraphQlTesterthat composes naturally with@SpringBootTest- No codegen by default (bring your own)
The Decision Tree
- You want strong schema-first workflow with built-in codegen
- You're doing Apollo Federation and want first-class
@keyhandling - You came from a DGS app or Netflix's ecosystem
- You want more "framework" in your framework
- You want the lightest-weight approach that still gives you batching and subscriptions
- You're already deep in Spring Security, Reactor, or Spring Data conventions
- You prefer code-first (return real domain objects from your resolvers)
- You're skeptical of codegen and want to stay close to
ExecutionGraphQlRequest
- You inherit a DGS codebase and want to gradually migrate
- You're building new subgraphs but need to stay compatible with older DGS ones
- You want
@BatchMappingin a DGS app without rewriting everything - Your team has strong opinions on both sides and nobody wants to lose
Migrating: It's Not Actually Scary
For most teams, mixing the two is the migration plan. You don't rip out DGS on Monday and arrive at Spring GraphQL by Friday. You start writing new resolvers in the style you prefer, leave the old ones alone, and delete the DGS-specific bits (codegen, DgsRuntimeWiring) once the last @DgsData is gone.
The one thing to watch: scalars and directives. If your DGS app registers custom scalars via @DgsRuntimeWiring, those need to migrate to Spring GraphQL's RuntimeWiringConfigurer pattern. It's a mechanical change, not a semantic one:
// DGS style
@DgsComponent
public class ScalarsConfig {
@DgsRuntimeWiring
public RuntimeWiring.Builder addScalar(RuntimeWiring.Builder builder) {
return builder.scalar(ExtendedScalars.DateTime);
}
}
// Spring GraphQL style
@Configuration
public class ScalarsConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return wiringBuilder -> wiringBuilder.scalar(ExtendedScalars.DateTime);
}
}
Both call the same graphql-java builder. The wrapper is what differs.
What This Changes About Testing
Testing used to be the sharpest point of divergence. DGS shipped DgsQueryExecutor which was synchronous, simple, and let you execute raw queries against your resolver beans. Spring GraphQL shipped GraphQlTester which was fluent, supported WebTestClient, and composed with @SpringBootTest.
Post-merge, you can use whichever you prefer. Both work. Mix them in the same test class:
@SpringBootTest
class MovieApiTests {
@Autowired
private DgsQueryExecutor dgsExecutor;
@Autowired
private GraphQlTester graphQlTester;
@Test
void dgsStyleTest() {
Long id = dgsExecutor.executeAndExtractJsonPath(
"query MovieId { movie(id: 1) { id } }",
"data.movie.id"
);
assertThat(id).isEqualTo(1L);
}
@Test
void springStyleTest() {
graphQlTester.document("query Movie { movie(id: 1) { id title } }")
.execute()
.path("movie.id").entity(Long.class).isEqualTo(1L)
.path("movie.title").entity(String.class).isEqualTo("Inception");
}
}
Two styles, same app, same schema, both pass. This is the whole point.
The Real Winner: Your Employability
Less romantic but true: for a few years, Java GraphQL roles split into DGS teams and Spring GraphQL teams. Postings would say "experience with DGS required" or "Spring GraphQL preferred" and candidates had to pick a side. With the frameworks now pointing at the same underlying execution engine and interoperable at the annotation level, the divide matters less. Knowing one is enough to start; the second is a few hours of "oh, this annotation is just that annotation."
Same schema. Same engine. Different flavor of sugar on top. The merger was always going to happen. The surprise is that it happened gracefully.
What To Actually Do Monday Morning
If you're on DGS, you're fine. If you're on Spring GraphQL, you're fine. If you're greenfield, pick the one whose annotations your team reads more naturally and move on. The days of choosing between two doors are over; you're just choosing which side of the hallway to stand on.
The more interesting question isn't "DGS or Spring GraphQL." It's "are you writing good schemas, batching your N+1s, and evolving your types without breaking clients?" That's what actually matters. The annotation flavor is just garnish.
This post works equally well whether you read it in a DGS accent or a Spring GraphQL accent. The underlying graphql-java does not care either way.