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/usecaseto 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/repositorywhen new persistence operations are needed, letting the use case layer define its dependencies. - Wire dependencies in
pkg/registry/registry.goto compose use cases with their concrete repositories, maintaining a single composition root. - Expose functionality through
pkg/adapter/controllerhandlers and register routes inpkg/infrastructure/router/router.goto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →