Understanding the Flow from HTTP Request to Database in Go Clean Architecture
In the manakuro/golang-clean-architecture repository, every HTTP request traverses five strict layers—Router → Controller → Use-case → Repository Interface → Concrete Implementation—with database transactions managed atomically at the use-case level via the DBRepository.Transaction interface.
This Go project demonstrates classic Clean Architecture principles by enforcing dependency inversion and separation of concerns. The request flow ensures that business logic remains decoupled from HTTP handlers and database implementations, making the codebase testable and framework-agnostic.
The Five-Layer Request Flow
The architecture processes incoming requests through a unidirectional pipeline where each layer has a single, well-defined responsibility.
1. HTTP Routing with Echo
The entry point resides in pkg/infrastructure/router/router.go, where the Echo framework registers endpoints and maps them to controller methods. The NewRouter function wires HTTP verbs to specific handler functions, establishing the first layer of the application boundary.
e := echo.New()
appCtrl := controller.NewAppController(...)
router.NewRouter(e, appCtrl) // Registers GET /users and POST /users
When a client sends a request, the router matches the URL pattern and forwards the context to the appropriate controller method.
2. Request Handling in Controllers
Controllers in pkg/adapter/controller/user.go act as the presentation layer, converting HTTP requests into domain objects and responses into JSON. The userController struct embeds the use-case interface, allowing it to invoke business operations without knowing implementation details.
Key methods include GetUsers for retrieval and CreateUser for persistence, both following the pattern of binding request data, invoking the use-case, and returning formatted responses.
3. Business Logic in Use-Cases
The use-case layer in pkg/usecase/usecase/user.go contains the application-specific business rules (interactors). The userUsecase struct orchestrates data flow between controllers and repositories, ensuring transaction boundaries wrap multi-step operations.
For read operations, List() delegates directly to the repository. For writes, Create() manages atomic transactions through the DBRepository interface, ensuring database consistency even when multiple tables are involved.
4. Repository Interfaces
Before reaching the database, requests pass through abstract interfaces defined in pkg/usecase/repository/. The user.go file declares UserRepository with methods like FindAll() and Create(), while db.go defines DBRepository with the critical Transaction() method.
These interfaces live in the use-case layer, not the infrastructure layer, ensuring that business logic depends on abstractions rather than concrete database technologies.
5. Database Implementation with Gorm
Concrete implementations reside in pkg/adapter/repository/. The user.go file provides Gorm-specific query logic using ur.db.Find() and ur.db.Create(), while db.go implements transaction management with tx := r.db.Begin(), deferring commit or rollback based on function execution results.
This layer is the only component aware of Gorm, allowing the entire application to swap database drivers by changing just these adapter files.
Transaction Management Strategy
The repository implements a robust transaction pattern that keeps business logic clean while ensuring atomicity. When userUsecase.Create() receives a request, it invokes uu.dBRepository.Transaction() with an anonymous function containing the actual database work.
func (uu *userUsecase) Create(u *model.User) (*model.User, error) {
data, err := uu.dBRepository.Transaction(func(i interface{}) (interface{}, error) {
// Atomic database operation
return uu.userRepository.Create(u)
})
// Type assertion returns *model.User after successful commit
return data.(*model.User), err
}
The concrete implementation in pkg/adapter/repository/db.go handles the Gorm-specific transaction lifecycle: beginning the transaction, executing the supplied function, committing on success, or rolling back on error/panic. This pattern prevents leaked connections and ensures data integrity without cluttering business logic with database boilerplate.
Practical Implementation Examples
Registering Routes
The router establishes the HTTP interface in pkg/infrastructure/router/router.go:
e.GET("/users", func(c echo.Context) error {
return appCtrl.User.GetUsers(c)
})
e.POST("/users", func(c echo.Context) error {
return appCtrl.User.CreateUser(c)
})
Controller Request Binding
The controller in pkg/adapter/controller/user.go handles input validation and response formatting:
func (uc *userController) CreateUser(ctx Context) error {
var params model.User
if err := ctx.Bind(¶ms); err != nil {
return err
}
created, err := uc.userUsecase.Create(¶ms)
if err != nil {
return err
}
return ctx.JSON(http.StatusCreated, created)
}
Concrete Gorm Repository
The database adapter in pkg/adapter/repository/user.go executes the actual SQL through Gorm:
func (ur *userRepository) Create(u *model.User) (*model.User, error) {
if err := ur.db.Create(u).Error; err != nil {
return nil, err
}
return u, nil
}
Summary
- Dependency Inversion: The use-case layer depends on
UserRepositoryandDBRepositoryinterfaces, not concrete Gorm implementations. - Layer Isolation: HTTP handling (
controller), business rules (usecase), and database access (adapter/repository) remain strictly separated. - Transaction Safety: The
DBRepository.Transactionmethod inpkg/adapter/repository/db.goencapsulates Gorm's transaction logic, exposing a clean functional interface to the use-case layer. - Framework Decoupling: Only the
routerandadapter/repositorypackages import Echo and Gorm, respectively, making the core business logic framework-agnostic.
Frequently Asked Questions
How does the repository pattern prevent database leaks in this architecture?
The DBRepository.Transaction implementation in pkg/adapter/repository/db.go uses Go's defer statement to ensure tx.Rollback() or tx.Commit() always executes, even if the use-case function panics. This pattern guarantees that database connections return to the pool regardless of application errors.
Can I replace Gorm with raw SQL or another ORM without changing the use-case code?
Yes. Since pkg/usecase/usecase/user.go only references interfaces defined in pkg/usecase/repository/, you can rewrite pkg/adapter/repository/user.go and db.go to use database/sql, SQLx, or another ORM. The use-case layer remains unchanged because it depends on the UserRepository and DBRepository abstractions, not Gorm-specific types.
Where should I add input validation in this Clean Architecture flow?
Validation occurs in two stages: format-level validation (JSON binding, type checking) happens in the controller at pkg/adapter/controller/user.go, while business-rule validation (domain invariants, uniqueness checks) belongs in the use-case layer at pkg/usecase/usecase/user.go. This separation ensures HTTP concerns don't leak into business logic and vice versa.
How does the router know which controller method to invoke?
The router.NewRouter function in pkg/infrastructure/router/router.go receives an AppController struct containing all controller instances. It explicitly maps URL patterns to specific methods (e.g., e.GET("/users", ctrl.User.GetUsers)), creating a declarative routing table that centralizes API endpoint definitions in one location.
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 →