- Go 1.22+,
net/http(http.ServeMux) sqlc(SQL-first)- Modular monolith
- Clean + Hexagonal + DDD-inspired
github.com/rluders/httpsuitefor HTTP
- Modular monolith → modules must stay independent
- Prefer stdlib and explicit code (no framework magic)
- Keep dependencies minimal and intentional
sqlcis the source of truth (never edit generated code)- Routing via
http.ServeMux
- Domain owns business logic
- Services = use cases (no HTTP concerns)
- Repository interfaces in domain, implementations in infra
- HTTP is an adapter only
- Modules must not know each other directly
- Communication only via:
- interfaces
- event bus
System is organized by module, not layers.
Each module encapsulates:
- domain (aggregates, entities, errors)
- services (use cases)
- repository interfaces
- repository implementations
- HTTP handlers
- route registration
- module bootstrap
A module must be:
- independently understandable
- independently testable
- cohesive and isolated
Never create shared dumping packages.
Preferred:
internal/
<module>/
domain/
aggregate/
entity/
repository.go
service.go
errors.go
application/ # optional
infra/
repository/
eventbus/
transport/
http/
handlers/
requests/
responses/
routes.go
module.goSmall modules:
internal/<module>/
aggregate/
entity/
repository.go
service.go
handler_create.go
routes.go
module.go
- Aggregates define consistency boundaries
- Only aggregate roots have repositories
- Entities contain behavior (not just data)
- Avoid anemic models
- Use value objects when useful
- Hold use cases + orchestration
- Depend only on:
- repository interfaces
- event bus
- external interfaces
- No transport concerns
- Interface → domain
- Implementation → infra
- Use
sqlc - Keep SQL explicit
- Methods reflect business intent
- One handler per endpoint (
New...) - Responsibilities:
- parse
- validate
- extract context/auth
- call service
- encode response
Must NOT:
- contain business logic
- access DB directly
Naming:
- create_user_handler.go
- get_user_handler.go
- Use
github.com/rluders/httpsuite - Keep types near transport layer
- Validate early
- Do not leak domain/DB models
- Each module owns routes
module.RegisterRoutes(mux)
- SQL-first (
sqlc) - Explicit queries
- Transactions in services
- Use
WithTx - Add indexes when needed
- Handle explicitly
- Wrap internally
- Return safe responses
- Avoid panic (except startup)
- Always propagate context
- No unmanaged goroutines
- Use only when justified
- Explicit config, fail fast
- No secrets in code
- Prefer structured logs
- Validate all external input
- Use parameterized SQL
- Do not trust client headers
Avoid:
- utils.go, models.go, handlers.go
Prefer:
- create_order_handler.go
- order_repository.go
- Table-driven tests
- Unit: domain/services
- Integration: DB
- HTTP: httptest
Cover:
- happy path
- validation errors
- edge cases
- transactions
- Optimize when needed
- Watch DB round trips, allocations, indexes
main.go wires:
- config, DB, infra, modules, router, server
Modules expose:
- NewModule(...)
- RegisterRoutes(...)
Reject if:
- modules coupled
- business logic in handlers
- services bypassed
sqlcbypassed- transactions hidden
- generic files introduced
- gofmt, go vet, tests, race detector
- sqlc generate if needed
- boundaries preserved
- handlers thin
- services own use cases
- transactions correct