How to Add New Use Cases for Business Logic in Go Clean Architecture

To add new use cases for business logic in Go, define an interface in pkg/usecase/usecase, implement it with repository dependencies, wire it through pkg/registry, and expose it via pkg/adapter/controller.

The manakuro/golang-clean-architecture repository demonstrates strict Clean Architecture principles in Go, separating domain logic from infrastructure concerns. When adding new use cases for business logic in Go, you must follow the dependency rule: inner layers define interfaces that outer layers implement. This ensures your core business rules remain framework-agnostic and testable.

Understanding the Clean Architecture Layers

The repository organizes code into concentric layers with strict dependency direction:

Layer Responsibility Key Packages
Domain Core business entities and rules pkg/domain/model
Use-case Application-specific business logic pkg/usecase/usecase & pkg/usecase/repository
Adapter Delivery mechanisms (HTTP, DB) pkg/adapter/controller & pkg/adapter/repository
Infrastructure External frameworks (Gin, GORM) pkg/infrastructure/router
Registry Dependency injection wiring pkg/registry

Step-by-Step Guide to Adding a New Use Case

Define the Use Case Interface

Create a new interface in pkg/usecase/usecase that describes the operation. This contract defines what the use case does without specifying how.

// pkg/usecase/usecase/user_profile.go
type UserProfileUpdater interface {
    UpdateProfile(id uint, data *model.User) (*model.User, error)
}

Implement the Business Logic

Create a struct that implements the interface. It receives repository interfaces via constructor injection, maintaining the dependency inversion principle.

type userProfileUsecase struct {
    userRepo repository.UserRepository
    dbRepo   repository.DBRepository
}

func NewUserProfileUsecase(u repository.UserRepository, d repository.DBRepository) UserProfileUpdater {
    return &userProfileUsecase{u, d}
}

func (up *userProfileUsecase) UpdateProfile(id uint, data *model.User) (*model.User, error) {
    result, err := up.dbRepo.Transaction(func(i interface{}) (interface{}, error) {
        user, err := up.userRepo.FindByID(id)
        if err != nil {
            return nil, err
        }
        user.Name = data.Name
        user.Age = data.Age
        return up.userRepo.Update(user)
    })
    if err != nil {
        return nil, err
    }
    return result.(*model.User), nil
}

Extend Repository Contracts

If the use case requires new persistence operations, add them to the interface in pkg/usecase/repository. The use case layer defines what it needs; the adapter layer will implement it.

// pkg/usecase/repository/user.go
type UserRepository interface {
    FindAll([]*model.User) ([]*model.User, error)
    Create(*model.User) (*model.User, error)
    FindByID(uint) (*model.User, error)   // new method
    Update(*model.User) (*model.User, error) // new method
}

Wire Dependencies in the Registry

The pkg/registry/registry.go file acts as the composition root. Add a factory method that instantiates the new use case with its dependencies.

// pkg/registry/registry.go
type registry struct {
    db *gorm.DB
}

func (r *registry) NewUserProfileUsecase() usecase.UserProfileUpdater {
    repo := r.NewUserRepository()
    dbRepo := r.NewDBRepository()
    return usecase.NewUserProfileUsecase(repo, dbRepo)
}

Expose via the Controller

Add a handler in pkg/adapter/controller that receives the use case interface and invokes it based on HTTP requests.

// pkg/adapter/controller/user.go
func (c *UserController) UpdateProfile(ctx *gin.Context) {
    var payload model.User
    if err := ctx.ShouldBindJSON(&payload); err != nil {
        ctx.JSON(http.StatusBadRequest, err.Error())
        return
    }
    id, _ := strconv.ParseUint(ctx.Param("id"), 10, 64)
    updated, err := c.usecase.UpdateProfile(uint(id), &payload)
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, err.Error())
        return
    }
    ctx.JSON(http.StatusOK, updated)
}

Register the Route

Finally, map the new endpoint in pkg/infrastructure/router/router.go.

// pkg/infrastructure/router/router.go
userGroup.PUT("/:id", appCtrl.User.UpdateProfile)

Complete Code Example: UpdateUserProfile Use Case

Here is the full implementation flow for adding an UpdateUserProfile use case to the manakuro/golang-clean-architecture project:

1. Interface Definition (pkg/usecase/usecase/user_profile.go):

package usecase

import (
    "golang-clean-architecture/pkg/domain/model"
    "golang-clean-architecture/pkg/usecase/repository"
)

type UserProfileUpdater interface {
    UpdateProfile(id uint, data *model.User) (*model.User, error)
}

type userProfileUsecase struct {
    userRepo repository.UserRepository
    dbRepo   repository.DBRepository
}

func NewUserProfileUsecase(u repository.UserRepository, d repository.DBRepository) UserProfileUpdater {
    return &userProfileUsecase{u, d}
}

func (up *userProfileUsecase) UpdateProfile(id uint, data *model.User) (*model.User, error) {
    result, err := up.dbRepo.Transaction(func(i interface{}) (interface{}, error) {
        user, err := up.userRepo.FindByID(id)
        if err != nil {
            return nil, err
        }
        user.Name = data.Name
        user.Age = data.Age
        return up.userRepo.Update(user)
    })
    if err != nil {
        return nil, err
    }
    return result.(*model.User), nil
}

2. Repository Extension (pkg/usecase/repository/user.go):

type UserRepository interface {
    FindAll([]*model.User) ([]*model.User, error)
    Create(*model.User) (*model.User, error)
    FindByID(uint) (*model.User, error)
    Update(*model.User) (*model.User, error)
}

3. Registry Wiring (pkg/registry/registry.go):

func (r *registry) NewUserProfileUsecase() usecase.UserProfileUpdater {
    repo := r.NewUserRepository()
    dbRepo := r.NewDBRepository()
    return usecase.NewUserProfileUsecase(repo, dbRepo)
}

4. Controller Handler (pkg/adapter/controller/user.go):

func (c *UserController) UpdateProfile(ctx *gin.Context) {
    var payload model.User
    if err := ctx.ShouldBindJSON(&payload); err != nil {
        ctx.JSON(http.StatusBadRequest, err.Error())
        return
    }
    id, _ := strconv.ParseUint(ctx.Param("id"), 10, 64)
    updated, err := c.usecase.UpdateProfile(uint(id), &payload)
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, err.Error())
        return
    }
    ctx.JSON(http.StatusOK, updated)
}

5. Route Registration (pkg/infrastructure/router/router.go):

userGroup.PUT("/:id", appCtrl.User.UpdateProfile)

Summary

  • Define interfaces in pkg/usecase/usecase to declare what operations your business logic performs without specifying implementation details.
  • Implement use cases as structs that receive repository interfaces via constructor injection, keeping business rules independent of databases and frameworks.
  • Extend repository contracts in pkg/usecase/repository when new persistence operations are needed, letting the use case layer define its dependencies.
  • Wire dependencies in pkg/registry/registry.go to compose use cases with their concrete repositories, maintaining a single composition root.
  • Expose functionality through pkg/adapter/controller handlers and register routes in pkg/infrastructure/router/router.go to make the use case accessible via HTTP.

Frequently Asked Questions

What is the difference between a use case and a repository in Clean Architecture?

In Clean Architecture, the use case contains application-specific business logic and orchestrates the flow of data to and from entities. It defines what the application does. The repository is an interface defined in the use case layer but implemented in the adapter layer; it handles how data is persisted and retrieved. The use case depends on the repository interface, not the concrete implementation, ensuring business logic remains decoupled from database details.

How do I handle database transactions in a Go use case?

Database transactions are handled through the DBRepository interface, which provides a Transaction method. In your use case implementation, wrap multiple repository operations inside the transaction function passed to dbRepo.Transaction(). If any operation returns an error, the transaction rolls back automatically; if all succeed, it commits. This pattern ensures atomicity for complex business operations that modify multiple entities.

Can I group multiple use cases in a single file?

While the repository structure suggests one use case per file (e.g., user.go for user-related operations), you can group closely related use cases in a single file if they share the same domain entity and repository dependencies. However, as your application grows, separating distinct business operations into individual files (such as user_profile.go for profile updates) improves maintainability and makes the codebase easier to navigate.

Why is the registry pattern used for wiring dependencies?

The registry pattern centralizes dependency injection in pkg/registry/registry.go, acting as the single composition root for the application. It instantiates concrete repositories and use cases, injecting them according to the interfaces defined in inner layers. This approach keeps framework-specific initialization code (like GORM or Gin) isolated from business logic, makes testing easier by allowing mock injection, and ensures that dependency direction always points inward toward the domain layer.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →