Go/Gin

Go Gin Project Structure for Large APIs With Handlers Services and Repositories

4 min read by DebuggedIt

Quick answer

A maintainable Gin API separates transport, business logic, and data access into handlers, services, and repositories, with dependencies pointing inward from...

A maintainable Gin API separates transport, business logic, and data access into handlers, services, and repositories, with dependencies pointing inward from HTTP toward the domain. Handlers translate HTTP into function calls, services hold the rules, and repositories talk to the database. The structure pays off as the codebase grows, but for a small service it can add ceremony without benefit.

What Does Each Layer Own?

The goal of layering is to keep each piece of code easy to change and easy to test. Each layer has a narrow job, and knowing where a rule belongs prevents logic from leaking into the wrong place.

  • Handlers: parse and validate the request, call a service, and map the result or error to an HTTP response. They know about gin.Context and nothing about SQL.
  • Services: implement business rules and orchestrate repositories. They take plain Go types and context values, and they know nothing about HTTP.
  • Repositories: run queries and map rows to domain types. They hide the database details from the rest of the code.

A frequent mistake is passing gin.Context down into services. It couples business logic to the web framework and makes tests harder. Pass the standard context.Context instead, and extract request values in the handler.

A useful test is to ask whether a function could be called from a command line tool or a background worker without changes. If it can, it belongs in the service or repository layer. If it needs an HTTP request to make sense, it belongs in a handler.

How Should You Lay Out the Packages?

Go favors packages organized by responsibility and kept small. A common layout uses a cmd directory for entry points and an internal directory for application code that should not be imported by other modules.

cmd/api/main.go
internal/
  config/
  http/          # router, middleware, handlers
  user/          # domain types, service, repository interface
  order/
  storage/
    postgres/    # repository implementations
pkg/             # only if you truly share code externally

Two organizing styles compete. Layer-first groups all handlers together, all services together, and so on. Feature-first groups by domain, such as user and order, with each feature containing its own handler, service, and repository. Feature-first usually scales better, because a change to one feature touches one directory.

Pick one style and stay consistent. The worst outcome is a mix that forces developers to search for code in several places.

Where Do Interfaces and Dependency Injection Fit?

In Go, define interfaces where they are consumed, not where they are implemented. The service declares the small interface it needs from a repository, and the repository package provides a concrete type that happens to satisfy it. This keeps interfaces minimal and makes faking dependencies in tests easy.

type UserRepository interface {
    FindByID(ctx context.Context, id int64) (*User, error)
}

type UserService struct {
    repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

Wire everything in main using plain constructors. Explicit construction is easy to read and debug, and many teams never need a dependency injection framework. Avoid global variables for databases or loggers, since they hide dependencies and make tests interfere with each other.

When Are Layers Too Much?

Layers have a cost: more files, more indirection, and boilerplate that merely forwards calls. A small CRUD service with little logic may not need a separate service layer at all. Letting a handler call a repository directly can be perfectly reasonable when there are no business rules to protect.

  • Introduce a service layer when rules, validation across entities, or transactions appear.
  • Avoid creating an interface for every struct. Add one when you need a seam for testing or multiple implementations.
  • Keep domain types free of framework tags where practical, and map between transport and domain types at boundaries.
  • Handle transactions in the service or a unit-of-work abstraction, not inside individual repository methods.

Revisit the structure as the application evolves. Refactoring toward more layers when the need is real is easier than removing premature abstractions later.

Frequently Asked Questions

Should handlers contain business logic?

No. Keep handlers thin. They should translate HTTP concerns and delegate to a service, which makes rules reusable and testable.

Where should I define repository interfaces?

Next to the code that uses them, typically in the service package, following the Go idiom of consumer-defined interfaces.

Do I need a DI framework in Go?

Usually not. Manual constructor wiring in main is clear and sufficient for most projects.

Is the internal directory required?

No, but it prevents other modules from importing your application code, which keeps your public surface small.

Summary

Separate handlers, services, and repositories, organize by feature, define interfaces at the consumer, and wire dependencies explicitly. Add layers only when they earn their cost. Review the Go documentation and project layout guidance for conventions that apply to your Go version.