A gqlgen plugin that splits resolvers into per-domain Go packages, so domain
code no longer imports graph/generated.
Requires Go 1.26+. Licensed under MIT.
Standard gqlgen puts every resolver in one package that imports
graph/generated. On large schemas every edit invalidates a build artifact
that can reach gigabytes — incremental compilation grinds to a halt.
The plugin produces a two-tier layout:
- Tier 1 — root resolver package. Thin glue:
Mutation()/Query()/Subscription()constructors, wrapper structs, embeds of the per-domain mixins. Methods reach callers via Go method promotion. - Tier 2 — per-domain packages. One package per subdirectory of
graph/schema/. Real business logic lives here. These packages never importgraph/generated— gqlgen interfaces are satisfied structurally.
A domain is the parent directory name of a .graphqls file:
graph/schema/todos/todo.graphqls → domain todos. Files placed directly
under graph/schema/ have no domain and stay in the root package.
go get github.com/prusov/gqldomainresolvergqlgen's default go run github.com/99designs/gqlgen cannot load plugins:
// cmd/gqlgen/main.go
package main
import (
"log"
"github.com/99designs/gqlgen/api"
"github.com/99designs/gqlgen/codegen/config"
"github.com/prusov/gqldomainresolver"
)
func main() {
cfg, err := config.LoadConfig("gqlgen.yml")
if err != nil {
log.Fatal(err)
}
plugin, err := gqldomainresolver.New()
if err != nil {
log.Fatal(err)
}
if err := api.Generate(cfg, api.AddPlugin(plugin)); err != nil {
log.Fatal(err)
}
}New() with no options migrates every domain in the schema. New domains
added later are picked up automatically.
The plugin ships its own safety-net resolver template and injects it into
gqlgen automatically — no resolver_template entry is required, and the
build no longer depends on go mod vendor or copying files out of the
module cache.
# gqlgen.yml
resolver:
layout: follow-schema
dir: graph/resolver
package: resolverSetting resolver_template explicitly is still honoured if you need a
custom template; the plugin yields to your override.
The plugin does not generate graph/resolver/resolver.go. Create it:
package resolver
type Resolver struct {
DomainMutationResolvers
DomainQueryResolvers
DomainSubscriptionResolvers
}Drop any embed whose root type your schema doesn't define.
go run ./cmd/gqlgenEach domain gets graph/resolver/<domain>/*.resolvers.go with panic stubs.
Replace each panic(...) with the real implementation — bodies are
preserved across regeneration via AST extraction.
Big-bang migration is impractical for any non-trivial codebase. The plugin
supports incremental migration via WithEnabledDomains — wire the plugin in
as a no-op first, then move one domain per PR. For projects that want the
greenfield default minus a handful of large or in-flight domains, pair
New() with WithExcludedDomains("...").
See MIGRATION.md for the full playbook.
- Godoc: https://pkg.go.dev/github.com/prusov/gqldomainresolver
- Domain-name normalization, keyword prefix, allowlist semantics — see
godoc on
New,WithEnabledDomains,WithExcludedDomains,WithKeywordPrefix.
- A given resolver field belongs to exactly one domain — splitting one root field across multiple domain packages isn't supported.
- Only one plugin per gqlgen run can implement
ResolverImplementer— don't combine with another plugin that hooks the same interface. - Two raw directory names that normalize to the same Go package (e.g.
order-flowandorder_flow) fail at codegen with a clear collision error. Rename one or passWithKeywordPrefixto disambiguate.