Implementing Clean Architecture with ent and gqlgen – Part 2

This article walks through integrating the four Clean Architecture layers—entity, use‑case, interface‑adapter, and framework/driver—into a Go application using ent for ORM and gqlgen for GraphQL, covering folder layout, ULID primary keys, Node interface support, and pagination implementation.

Code DAO
Code DAO
Code DAO
Implementing Clean Architecture with ent and gqlgen – Part 2

Continuing from the previous post, the Clean Architecture is split into four layers that map to a folder structure:

pkg
├── adapter
│   ├── controller   // Controller
│   ├── repository   // Specific implementation of repository
│   └── resolver     // GraphQL resolvers
├── entity
│   └── model        // Entity definitions (e.g., ent.User, ent.Todo)
├── infrastructure
│   ├── datastore    // MySQL configuration
│   ├── graphql      // GraphQL server configuration
│   └── router       // Echo router
├── usecase
│   ├── repository   // Interface for adapters
│   └── usecase      // Application logic

Entity layer – Define model aliases that re‑export ent types to avoid importing ent in every package:

package model
import "golang-clean-architecture-ent-gqlgen/ent"

type User = ent.User
type CreateUserInput = ent.CreateUserInput
type UpdateUserInput = ent.UpdateUserInput

Use‑case layer – Create a repository interface and a use‑case struct that delegates to the repository:

package repository
import (
    "context"
    "golang-clean-architecture-ent-gqlgen/pkg/entity/model"
)

type User interface {
    Get(ctx context.Context, id *int) (*model.User, error)
}

package usecase
import (
    "context"
    "golang-clean-architecture-ent-gqlgen/pkg/entity/model"
    "golang-clean-architecture-ent-gqlgen/pkg/usecase/repository"
)

type user struct { userRepository repository.User }

type User interface { Get(ctx context.Context, id *int) (*model.User, error) }

func NewUserUsecase(r repository.User) User { return &user{userRepository: r} }

func (u *user) Get(ctx context.Context, id *int) (*model.User, error) { return u.userRepository.Get(ctx, id) }

Interface‑adapter layer – Implement controller, repository, and resolver that wire the use‑case to the GraphQL layer:

package controller
type Controller struct { User interface{ User } }

func NewUserController(u usecase.User) User { return &user{userUsecase: u} }

package repository
type userRepository struct { client *ent.Client }
func NewUserRepository(c *ent.Client) repository.User { return &userRepository{client: c} }
func (r *userRepository) Get(ctx context.Context, id *int) (*model.User, error) {
    return r.client.User.Query().Where(user.IDEQ(*id)).Only(ctx)
}

package resolver
type Resolver struct { client *ent.Client; controller controller.Controller }
func NewSchema(c *ent.Client, ctrl controller.Controller) graphql.ExecutableSchema {
    return generated.NewExecutableSchema(generated.Config{Resolvers: &Resolver{client: c, controller: ctrl}})
}

Framework/driver layer – Provide datastore, GraphQL server, and router helpers:

package datastore
func New() string { /* build DSN from config */ }
func NewClient() (*ent.Client, error) { return ent.Open(dialect.MySQL, New(), ent.Debug()) }

package graphql
func NewServer(c *ent.Client, ctrl controller.Controller) *handler.Server {
    srv := handler.NewDefaultServer(resolver.NewSchema(c, ctrl))
    srv.Use(entgql.Transactioner{TxOpener: c})
    return srv
}

package router
func New(srv *handler.Server) *echo.Echo {
    e := echo.New()
    e.Use(middleware.Recover())
    e.POST("/query", echo.WrapHandler(srv))
    e.GET("/playground", func(c echo.Context) error { playground.Handler("GraphQL", "/query").ServeHTTP(c.Response(), c.Request()); return nil })
    return e
}

Main entry point wires everything together:

func main() {
    config.ReadConfig(config.ReadConfigOption{})
    client := newDBClient()
    ctrl := newController(client)
    srv := graphql.NewServer(client, ctrl)
    e := router.New(srv)
    e.Logger.Fatal(e.Start(":" + config.C.Server.Address))
}

ULID primary keys – Replace auto‑increment int IDs with sortable ULIDs. Install the package and define a custom ID type:

go get github.com/oklog/ulid/v2

package ulid
type ID string
func MustNew(prefix string) ID { return ID(prefix + fmt.Sprint(ulid.MustNew(...))) }

Update schema fields to use ulid.ID and generate code with go generate ./ent. Adjust migrations to change column types to varchar(255).

Node interface – Add a GraphQL Node interface and bind it to the ULID model:

interface Node { id: ID! }

type User implements Node { id: ID! name: String! ... }

package model
type Node = ent.Noder

Configure gqlgen to use the custom ID and Node models, then implement the resolver:

func (r *queryResolver) Node(ctx context.Context, id ulid.ID) (ent.Noder, error) {
    return r.client.Noder(ctx, id, ent.WithNodeType(ent.IDToType))
}

Global ID mapping – Prefixes (e.g., 0AA for users, 0AB for todos) are mapped to table names via a globalid package, enabling IDToType to resolve the underlying table from a ULID.

type field struct { Prefix, Table string }
type GlobalIDs struct { User, Todo field }
func (g GlobalIDs) FindTableByID(id string) (string, error) { /* lookup */ }

func IDToType(_ context.Context, id ulid.ID) (string, error) {
    prefix := id[:3]
    return globalIDs.FindTableByID(string(prefix))
}

Pagination – Implement Relay‑style cursor connections. Add Cursor and PageInfo scalars to the GraphQL schema and map them to ent types:

scalar Cursor
scalar Time

type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: Cursor endCursor: Cursor }

type UserConnection { totalCount: Int! pageInfo: PageInfo! edges: [UserEdge] }

type UserEdge { node: User cursor: Cursor! }

Generate the code with gqlgen and later extend the resolver to return paginated results.

The article ends with a note that pagination implementation will continue in the next section.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GoClean ArchitecturePaginationGraphQLulidentgqlgen
Code DAO
Written by

Code DAO

We deliver AI algorithm tutorials and the latest news, curated by a team of researchers from Peking University, Shanghai Jiao Tong University, Central South University, and leading AI companies such as Huawei, Kuaishou, and SenseTime. Join us in the AI alchemy—making life better!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.