Best for small applications, CLIs, microservices with fewer than ~10 handlers, and projects where a single developer or small team owns the entire codebase.
Scanned 6/1/2026
Install via CLI
openskills install tools-only/X-Skills# Go Project Structure
## Flat Structure
Best for small applications, CLIs, microservices with fewer than ~10 handlers, and projects where a single developer or small team owns the entire codebase.
```text
myapp/
├── main.go
├── server.go
├── handlers.go
├── middleware.go
├── models.go
├── store.go
├── server_test.go
├── handlers_test.go
└── go.mod
```
### When to Use
- CLI tools and small utilities
- Single-purpose microservices (one bounded context)
- Prototypes and proofs of concept
- Fewer than ~10 HTTP handlers
- One or two developers working on the codebase
### Benefits
- Zero cognitive overhead for navigation -- everything is in one place
- No circular dependency issues (single package)
- Easy to refactor -- just move functions between files
- `go test ./...` covers everything in one pass
- New contributors can understand the layout immediately
### File Responsibilities
| File | Contains |
|------|----------|
| `main.go` | `func main()`, wiring, configuration loading, `run()` function |
| `server.go` | `Server` struct, `NewServer()`, `routes()`, `ServeHTTP()` |
| `handlers.go` | All HTTP handler methods on `Server` |
| `middleware.go` | Middleware functions (`requestLogger`, `authenticate`, etc.) |
| `models.go` | Domain types, request/response structs |
| `store.go` | Database access layer (queries, store struct) |
For very small apps, `server.go` and `handlers.go` can be the same file.
### Example: Flat Server
```go
// main.go
package main
import (
"context"
"database/sql"
"log/slog"
"os"
)
func main() {
if err := run(context.Background()); err != nil {
slog.Error("application error", "err", err)
os.Exit(1)
}
}
func run(ctx context.Context) error {
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
return fmt.Errorf("opening db: %w", err)
}
defer db.Close()
srv := NewServer(db, slog.Default())
// ... start and graceful shutdown ...
return nil
}
```
```go
// server.go
package main
type Server struct {
db *sql.DB
logger *slog.Logger
router *http.ServeMux
}
func NewServer(db *sql.DB, logger *slog.Logger) *Server {
s := &Server{db: db, logger: logger, router: http.NewServeMux()}
s.routes()
return s
}
func (s *Server) routes() {
s.router.HandleFunc("GET /api/items", s.handleListItems)
s.router.HandleFunc("GET /api/items/{id}", s.handleGetItem)
s.router.HandleFunc("POST /api/items", s.handleCreateItem)
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}
```
---
## Modular / Domain-Driven Structure
For larger applications with multiple bounded contexts, multiple teams, or significant growth expected.
```text
myapp/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── user/
│ │ ├── handler.go
│ │ ├── service.go
│ │ ├── store.go
│ │ ├── model.go
│ │ └── handler_test.go
│ ├── order/
│ │ ├── handler.go
│ │ ├── service.go
│ │ ├── store.go
│ │ ├── model.go
│ │ └── handler_test.go
│ └── platform/
│ ├── middleware/
│ │ ├── auth.go
│ │ └── logging.go
│ ├── database/
│ │ └── postgres.go
│ └── config/
│ └── config.go
├── migrations/
│ ├── 001_create_users.up.sql
│ └── 001_create_users.down.sql
├── go.mod
└── go.sum
```
### Directory Conventions
#### `cmd/`
Entry points for the application. Each subdirectory produces one binary.
```text
cmd/
├── server/
│ └── main.go # HTTP server
├── worker/
│ └── main.go # Background job processor
└── migrate/
└── main.go # Database migration tool
```
Each `main.go` is the composition root: it reads config, creates dependencies, wires them together, and starts the program. Keep `main.go` small -- delegate to a `run()` function that returns an error.
#### `internal/`
The `internal/` directory is enforced by the Go toolchain. Code inside `internal/` cannot be imported by external modules. Use it for all application-specific code.
```go
// This import is only allowed from within the same module:
import "myapp/internal/user"
```
This gives you the freedom to refactor internal packages without worrying about breaking external consumers.
#### Domain Packages (`internal/user/`, `internal/order/`)
Each domain package owns its:
- **Models** -- domain types and validation
- **Store** -- database queries, implements a store interface
- **Service** -- business logic, orchestrates store calls
- **Handler** -- HTTP handlers, request parsing, response writing
```go
// internal/user/service.go
package user
type Service struct {
store Store
}
type Store interface {
GetByID(ctx context.Context, id string) (*User, error)
Create(ctx context.Context, u *User) error
List(ctx context.Context, limit, offset int) ([]User, error)
}
func NewService(store Store) *Service {
return &Service{store: store}
}
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
if id == "" {
return nil, fmt.Errorf("user id is required")
}
return s.store.GetByID(ctx, id)
}
```
#### `internal/platform/`
Shared infrastructure code that is not domain-specific:
- `middleware/` -- HTTP middleware (logging, auth, CORS)
- `database/` -- connection helpers, migration runners
- `config/` -- configuration loading and validation
Platform packages are imported by domain packages and `cmd/`, but never import domain packages.
### Package Design Principles
**Dependencies flow inward.** Domain packages should not import other domain packages. If `order` needs user data, it defines its own interface:
```go
// internal/order/service.go
package order
type UserLookup interface {
GetByID(ctx context.Context, id string) (*User, error)
}
type Service struct {
store Store
userLookup UserLookup
}
```
The `cmd/server/main.go` wires the `user.Service` (which satisfies `order.UserLookup`) into the order service.
**Avoid circular dependencies.** If package A imports package B, package B cannot import package A. This is a compile error in Go. Solutions:
1. Extract shared types into a separate package (e.g., `internal/domain`)
2. Use interfaces at the consumer side
3. Merge the packages if they are tightly coupled
**Keep packages focused.** A package named `utils` or `helpers` is a code smell. If a function doesn't belong to a domain, it belongs in `platform/` with a descriptive package name.
**Export only what is needed.** Start with unexported types and functions. Export them only when another package needs access.
### Wiring in main.go
```go
// cmd/server/main.go
package main
import (
"myapp/internal/order"
"myapp/internal/platform/config"
"myapp/internal/platform/database"
"myapp/internal/user"
)
func run(ctx context.Context) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
db, err := database.Open(cfg.DatabaseURL)
if err != nil {
return fmt.Errorf("opening db: %w", err)
}
defer db.Close()
// Build dependency graph
userStore := user.NewPostgresStore(db)
userService := user.NewService(userStore)
orderStore := order.NewPostgresStore(db)
orderService := order.NewService(orderStore, userService)
// Build server
mux := http.NewServeMux()
user.RegisterRoutes(mux, userService)
order.RegisterRoutes(mux, orderService)
// ... start HTTP server with graceful shutdown ...
return nil
}
```
### Route Registration in Domain Packages
Each domain package provides a `RegisterRoutes` function:
```go
// internal/user/handler.go
package user
func RegisterRoutes(mux *http.ServeMux, svc *Service) {
h := &handler{svc: svc}
mux.HandleFunc("GET /api/users", h.list)
mux.HandleFunc("GET /api/users/{id}", h.get)
mux.HandleFunc("POST /api/users", h.create)
}
type handler struct {
svc *Service
}
func (h *handler) get(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
u, err := h.svc.GetUser(r.Context(), id)
// ...
}
```
---
## Migration Signals: Flat to Modular
Move from flat to modular when you notice:
1. **File length** -- `handlers.go` exceeds ~500 lines or contains unrelated handlers
2. **Naming collisions** -- You prefix functions like `userGetHandler`, `orderGetHandler` to avoid confusion
3. **Multiple developers** -- Merge conflicts in shared files become frequent
4. **Distinct domains** -- The application clearly has separate bounded contexts (users, orders, billing)
5. **Separate deployment needs** -- You want a CLI tool and an HTTP server from the same codebase (`cmd/server/`, `cmd/cli/`)
6. **Test isolation** -- You want to test one domain without loading all the others
### How to Migrate
1. Create `cmd/server/main.go` and move wiring code there
2. Create `internal/` and make one domain package for the most independent domain
3. Move its models, handlers, store, and tests into the new package
4. Update imports in `main.go`
5. Repeat for each domain
6. Extract shared infrastructure into `internal/platform/`
Migrate incrementally. Do not restructure everything in one commit.
No comments yet. Be the first to comment!