Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
26 Sep 2025 · 6 min read ·Article 88 / 125
Go

88 A Scalable graphql-go Project Structure

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

88 A Scalable graphql-go Project Structure: A Guide for Backend Engineers

Building an API with GraphQL in Go offers clear advantages in scalability, maintainability, and developer experience. However, the wrong project structure can make a codebase hard to maintain even before traffic ramps up. That is exactly why we need a solid project-structure strategy.

This article takes a practical look at an 88-style scalable and maintainable GraphQL-Go project structure, based on real experience and scenarios from mid-sized to enterprise-scale projects. I have also included code snippets, flow simulations, and mermaid diagrams to make the concepts easy to digest.


Why Structure Matters?

GraphQL’s dynamic, nested contract forces us to keep our code modular and easy to customize. In Go, we also have to keep separation of concerns and type safety in mind. If you are not careful, you will quickly end up with bloated resolver files, tangled model or schema files, and unclear dependencies.

A good structure gives you:

  • Encapsulation and isolation of business logic
  • Easier testing and faster development of new features
  • Scalability as you add more team members/engineers
  • Easy integration with other systems (REST, gRPC, message bus, etc.)

Anatomy: A Modern GraphQL-Go Project Structure

Below is the directory structure (📁 for folders, 📄 for files):

text
 1📁 graph
 2    📁 schema
 3        📄 schema.graphqls
 4        📄 directives.graphqls
 5    📁 resolver
 6        📄 user_resolver.go
 7        📄 product_resolver.go
 8    📁 generated
 9        📄 generated.go
10    📄 handler.go
11    📄 loader.go
12📁 domain
13    📁 user
14        📄 model.go
15        📄 repository.go
16        📄 service.go
17        📄 service_test.go
18    📁 product
19        📄 model.go
20        📄 repository.go
21        📄 service.go
22📁 pkg
23    📄 logger.go
24    📄 config.go
25    📄 middleware.go
26    📄 util.go
27📁 cmd
28    📄 main.go
29📁 migration
30    📄 001_init.sql
31    📄 002_add_user.sql
32....

Table 1: Folder Explanation

FolderBrief explanation
graphEverything directly related to GraphQL (schema, loader, handler)
domainModular business logic per entity/function
pkgShared library/utility functions
cmdApplication entry point
migrationDatabase migration SQL
Danger
This structure is highly scalable because every flow (GraphQL, business domain, utilities, and migrations) is clearly separated.

Execution Flow of the me Query (Simulation)

The following diagram helps you understand the flow of a me GraphQL request (get current user):

MERMAID
graph LR
    A[Client] --> B(GraphQL Handler)
    B --> C(Auth Middleware)
    C --> D(UserResolver.me)
    D --> E(UserService.GetUserByID)
    E --> F(UserRepository.FindByID)
    F --> |Database| G((Postgres))
    G --> F
    F --> E
    E --> D
    D --> C
    C --> B
    B --> A

Deep Dive: Walking Through the Structure

1. graph/schema/*.graphqls

Separate your schema and directive definitions. For example:

graphql
 1# graph/schema/schema.graphqls
 2type User {
 3    id: ID!
 4    name: String!
 5    email: String!
 6}
 7
 8type Query {
 9    me: User!
10}
11
12type Mutation {
13    register(name: String!, email: String!, password: String!): User!
14}

2. graph/resolver/*.go

Resolvers should not carry too much business logic. Their only job is mapping and a little validation.

go
1// graph/resolver/user_resolver.go
2func (r *queryResolver) Me(ctx context.Context) (*model.User, error) {
3    userID := GetUserIDFromContext(ctx)
4    user, err := r.UserService.GetUserByID(ctx, userID)
5    if err != nil {
6        return nil, err
7    }
8    return user, nil
9}

3. domain/user/model.go

Entity model definitions. There must be no dependency on the GraphQL package.

go
1type User struct {
2    ID       string
3    Name     string
4    Email    string
5    Password string
6}

4. domain/user/repository.go

The abstraction over the database. Ideally use an interface so it is testable.

go
1type UserRepository interface {
2    FindByID(ctx context.Context, id string) (*User, error)
3    FindByEmail(ctx context.Context, email string) (*User, error)
4    Create(ctx context.Context, user *User) error
5}

5. domain/user/service.go

Put your business logic here.

go
1type UserService struct {
2    repo UserRepository
3}
4
5func (s *UserService) GetUserByID(ctx context.Context, id string) (*User, error) {
6    return s.repo.FindByID(ctx, id)
7}

6. pkg/

Put shared libraries here, for example env-based config, a logger, and middleware.

go
1// pkg/logger.go
2func NewLogger() *zap.Logger {
3    logger, _ := zap.NewProduction()
4    return logger
5}

Modularization Tips for 88+ Endpoints

Why do I call it an 88 structure? Because each domain can represent one or several GraphQL endpoints. A good practice is to separate the resolver, service, repository, and model per domain. For complex business APIs (for example an e-commerce app: user, product, order, payment, shipping, and so on), you can keep adding folders inside domain/*.

Example of adding a new domain

text
1📁 domain
2    📁 payment
3        📄 model.go
4        📄 repository.go
5        📄 service.go
6        ...

Code Generation: Automating with gqlgen

gqlgen is very popular for generating GraphQL code in Go with type safety. Place the configuration at the root (gqlgen.yml) and use the graph/generated folder.

Keep in mind:
The generated folder and the files inside it should never be edited by hand. All custom logic belongs in the resolver/service/domain.


Production Deployment Life Cycle

A typical deployment pipeline flow:

MERMAID
graph LR
    A[Engineer Push Code] --> B[Run Unit Test]
    B --> C[Build Binary]
    C --> D[Run Integration Test]
    D --> E[Create Container Image]
    E --> F[Deploy ke Kubernetes/VM]

Testing: Layered Unit Tests

  • Test business logic (in /domain/*/service_test.go)
  • Test resolver integration (using a test client, e.g. gqlgen)
  • Test middleware and utilities (in /pkg)

A simple unit test simulation

go
1func TestUserService_GetUserByID(t *testing.T) {
2    mockRepo := new(MockUserRepo)
3    mockRepo.On("FindByID", mock.Anything, "123").Return(&User{ID:"123", Name:"Budi"}, nil)
4    s := NewUserService(mockRepo)
5    user, err := s.GetUserByID(context.TODO(), "123")
6    require.NoError(t, err)
7    require.Equal(t, "Budi", user.Name)
8}

Checklist for a Good GraphQL-Go Project Structure

CheckExplanation
Separate schemaSplit .graphqls files per domain
Thin resolverResolvers carry minimal business logic, only delegation
Modular domainEach domain has its own model, repo, and service
Reusable pkgUtility libraries do not depend on any specific domain
Dependency inj.Services and repos use injection for easier testing/migration
Automated testUnit and integration tests are available
ConfigurableConfiguration (env) is centralized and easy to change
CI/CDSupports a build/test/deploy pipeline

Closing

A scalable project structure is not just about “having lots of folders”; it is about the discipline of separating concerns while keeping the codebase practically maintainable. With an 88-domain modular structure, you can tame the complexity of a GraphQL-Go API with a large team while still being able to iterate quickly during development.

My tip: Start with a minimal structure that is ready to grow, and always refactor whenever a new pain point appears.
Does your GraphQL-Go project structure already look like this?


Summary

  • Modularity and separation of concerns are the foundation of scalability
  • Separate resolvers, domain logic, utilities, and the schema
  • Test coverage and deployment automation are absolutely essential
  • Do not hesitate to split domains once the number of resolvers grows large

Let’s discuss:
What has your experience been with structuring GraphQL projects in Go?
Share it in the comments!

Related Articles

💬 Comments