Go Architecture Basics
10 examples to get you started with Architecture - 7 basic and 3 intermediate.
Prerequisites
- Go 1.26.x installed (
go version). - Create a module:
mkdir archdemo && cd archdemo && go mod init example.com/archdemo. - Examples use only the standard library unless noted.
Basic Examples
1. Standard service layout
Separate binaries, private implementation, and public library code.
archdemo/
cmd/api/main.go
internal/service/
internal/store/
pkg/api/ # optional shared API types
cmd/holdsmainpackages - one folder per binary.internal/hides implementation from external importers.pkg/is optional for libraries you want other modules to import.
Related: Architecture in Go: Small Interfaces, Explicit Dependencies - why boundaries matter
2. Constructor injection
Pass dependencies explicitly instead of globals.
package service
type Greeter struct {
prefix string
}
func New(prefix string) *Greeter {
return &Greeter{prefix: prefix}
}
func (g *Greeter) Hello(name string) string {
return g.prefix + ", " + name
}Newvalidates inputs and returns a ready struct.- Callers control configuration; tests pass fakes without mutating globals.
- Keep exported surface small - fields can stay unexported.
Related: Dependency Injection: wire, dig & Manual Wiring - scaling wiring past hand-rolled
main
3. Consumer-side interface
Define the contract where it is used.
package handler
import "context"
type Lister interface {
List(ctx context.Context) ([]string, error)
}
type HTTP struct {
svc Lister
}
func New(svc Lister) *HTTP {
return &HTTP{svc: svc}
}Listerhas one method - easy to mock in handler tests.- Concrete storage types live in
internal/storewithout importing HTTP. - Interfaces belong near the consumer, not always at the provider.
4. Thin HTTP handler
Translate HTTP to domain calls; no SQL in handlers.
package handler
import (
"encoding/json"
"net/http"
)
func (h *HTTP) List(w http.ResponseWriter, r *http.Request) {
items, err := h.svc.List(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(items)
}- Use
r.Context()for cancellation and deadlines. - Map errors to status codes consistently (wrap domain errors at the edge).
- JSON encoding stays in the adapter layer.
Related: Handler-Service-Repository Layering - full three-layer flow
5. Service layer with domain errors
Business rules live between HTTP and storage.
package service
import (
"context"
"fmt"
)
type Repo interface {
All(ctx context.Context) ([]string, error)
}
type Service struct {
repo Repo
}
func New(repo Repo) *Service {
return &Service{repo: repo}
}
func (s *Service) List(ctx context.Context) ([]string, error) {
items, err := s.repo.All(ctx)
if err != nil {
return nil, fmt.Errorf("list: %w", err)
}
return items, nil
}- Services wrap repository errors with context (
%w) for observability. Listsatisfies the handler'sListerinterface from example 3.- Services stay ignorant of HTTP status codes.
6. Repository boundary
Isolate persistence behind a small API.
package store
import "context"
type Memory struct {
data []string
}
func (m *Memory) All(ctx context.Context) ([]string, error) {
return append([]string(nil), m.data...), nil
}- Swapping memory for Postgres changes one package.
- Context is accepted even if this example ignores cancellation - real I/O should respect it.
- Return domain-friendly types, not driver rows, from repositories.
7. Composition root in main
Wire concrete types in cmd/api/main.go only.
package main
import (
"log"
"net/http"
"example.com/archdemo/internal/handler"
"example.com/archdemo/internal/service"
"example.com/archdemo/internal/store"
)
func main() {
repo := &store.Memory{data: []string{"alpha", "beta"}}
svc := service.New(repo)
h := handler.New(svc)
mux := http.NewServeMux()
mux.HandleFunc("GET /items", h.List)
log.Fatal(http.ListenAndServe(":8080", mux))
}mainis the only place that knows all concrete types.- Routing uses Go 1.22+ method-aware patterns on
ServeMux. - Adding a database means changing wiring here, not business packages.
Intermediate Examples
8. internal/ enforcement
Packages under internal/ cannot be imported outside the parent tree.
// internal/store/pg.go
package store
// Only example.com/archdemo/... may import this package.
type PG struct{}- The compiler rejects external imports of
internalpaths. - Use for code that is not a semver promise to other modules.
- Public libraries belong in non-internal paths with documented stability.
Related: Clean & Hexagonal Architecture in Go - ports at the domain edge
9. Feature package instead of layer-only folders
Organize by capability when domains are clear.
internal/
billing/
service.go
handler.go
postgres.go
shipping/
...
- Feature folders reduce cross-team merge conflicts.
- Shared kernels (
internal/platform/) hold logging and auth helpers. - Split into services only when release and scale boundaries justify it.
Related: Monolith vs Microservices in Go - when to split binaries
10. ADR stub for a boundary decision
Record why you chose a layout before debate repeats.
# ADR 0003: Handler-service-repository layering
## Status
Accepted
## Context
New HTTP API; team wants testable business logic.
## Decision
Use handler / service / repository packages with consumer-side interfaces.
## Consequences
More packages; explicit wiring in main; easier unit tests.- Store ADRs in
docs/adr/or.adr/at repo root. - Link decisions to Go-specific constraints (no DI container in stdlib).
- Revisit when load, team topology, or compliance requirements change.
Related: Architecture Decision Records for Go Teams - ranked decisions for Go services
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).