Recently I performed a large refactor in a Go project and wrote down the approach.
Originally, the project used a flat layout like this:
- cmd/api/main.go
- user.go
- user_db.go
- user_db_test.go
- user_handler.go
- user_handler_test.go
- server.go
This worked for a while, but after about a year the number of files grew and overall readability deteriorated.
Keeping everything in one package avoids cyclic imports, but we started to hit symbol name collisions that made certain names unusable.
After considering options, we adopted the layout that places the domain package at the repository root.
What this layout means
By “placing the domain package at the repository root,” I mean creating a domain package at the repo root and organizing implementations into subpackages according to their dependencies. The idea is inspired by Ben Johnson’s article “Standard Package Layout”.
Although the article calls it a “Standard Package Layout,” that name can invite bikeshedding. In this post I’ll simply call it the “root‑domain package layout.”
For example, for a backend API using MySQL, the root domain package might look like this:
- cmd/api/main.go
- mysql/
- user.go
- http/
- user_handler.go
- server.go
- user.go
You don’t have to place Go files directly at the Git repository root. If you prefer not to, or if the repository name doesn’t match the domain name, create a directory for the root domain and put the code there.
If the product is named myapp
, the domain package becomes myapp
.
- cmd/api/main.go
- myapp/
- mysql/
- user.go
- http/
- user_handler.go
- server.go
- user.go
myapp/user.go
defines models used across the codebase:
package myapp
type User struct {
Name string
Age int
}
type UserService interface {
CreateUser(uid string) (*User, error)
GetUser(uid string) (*User, error)
UpdateUser(uid string) (*User, error)
DeleteUser(uid string) error
}
In the mysql package, implement the myapp.UserService
interface.
package mysql
type UserService struct {
db *sqlx.DB
}
func (s *UserService) CreateUser(uid string) (*myapp.User, error) {
// Concrete implementation using MySQL
}
In the http package, depend on the interface from myapp instead of using mysql.UserService
directly.
package http
type UserHandler struct {
userService myapp.UserService
}
For testing, I generated mocks of the root package via gomock under the mocks
directory and had subpackages depend on those mocks when they needed services from the root package.
package myapp
//go:generate mockgen -destination=./mocks/user_service_mock.go -package=mocks . UserService
type UserService interface {
CreateUser(uid string) (*User, error)
GetUser(uid string) (*User, error)
UpdateUser(uid string) (*User, error)
DeleteUser(uid string) error
}
This way, the mysql package focuses solely on MySQL‑related logic, while the http package avoids a hard dependency on MySQL and can write tests focused on the HTTP server behavior.
- cmd/api/main.go
- myapp/
- mocks/
- user_service_mock.go
- mysql/
- user.go
- user_test.go
- http/
- user_handler.go
- user_handler_test.go
- user.go
- user_test.go
In the actual project, the structure looked like this:
- cmd/api/main.go
- myapp
- http (HTTP server)
- grpc (gRPC server)
- rdb (MySQL)
- s3 (Amazon S3)
- dydb (DynamoDB)
- iot (AWS IoT)
- ssm (AWS SSM)
- fcm (Firebase Cloud Messaging)
- ... (omitted)
In these subpackages we run tests as close to reality as possible using dockertest and LocalStack when applicable.
Why not name the package domain
It’s perfectly fine to create a domain
package instead of placing the domain model at the root:
- cmd/main.go
- domain/
- user.go
- user_test.go
- http/
- user_handler.go
- user_handler_test.go
- mysql/
- user.go
- user_test.go
In that case, code would refer to types as domain.XXX
:
func main() {
user := domain.User{}
}
If placed at the root as myapp
, it becomes myapp.XXX
:
func main() {
user := myapp.User{}
}
Using the product name as the package makes it more obvious that these types are used across the whole product.
Comparison with other layouts
Some projects reference specific architectures like Clean Architecture and introduce directories named after layers:
- cmd/main.go
- domain/
- infrastructure/
- presenter/
- repository/
- usecase/
Such naming assumes familiarity with the architectural vocabulary and can be opaque to newcomers.
Similar to why I prefer a product name over domain
, I find more direct naming makes code easier to navigate.
Grouping subpackages by dependency keeps placement obvious even without special background knowledge:
- cmd/main.go
- mysql/
- http/
- user.go
Takeaways
Refactoring to place the domain package at the repository root made the project easier to understand and to test. It encourages a design that’s approachable without prior context.