Concept · Architecture & domain modeling

Package by feature

Organise code by business capability rather than technical layer, so a change to one feature stays in one place and boundaries can be enforced.

2 minpackage by layerfeature foldersvertical slicesapi and implementation modules

Package by layer groups code by technical role: all handlers together, all services together, all repositories together. Package by feature groups it by what it does for the business: everything about orders together, everything about payments together.

text
By layer                         By feature
internal/                        internal/
  handlers/  order.go user.go      ordering/  handler.go service.go store.go
  services/  order.go user.go      accounts/  handler.go service.go store.go
  repos/     order.go user.go      payments/  handler.go service.go store.go
  models/    order.go user.go      platform/  database/ httpserver/ config/

Why feature packages scale better

  • Changes stay local. Adding a field to orders touches one directory instead of four. That's high cohesion: code that changes together lives together.
  • Encapsulation becomes possible. In a layered layout, everything in models must be exported for handlers and services to use it, so every feature can reach every other feature's internals. In a feature package, most identifiers can stay unexported, and Go's internal/ directories can hide a feature's implementation from the rest of the codebase entirely.
  • The structure tells you what the system does. A newcomer reading ordering/, payments/, delivery/ learns the business; reading handlers/, services/ learns nothing.
  • Ownership is clear. A team can own a directory, and boundaries map naturally onto bounded contexts or future services.

Layers don't disappear — they move inside each feature, where they still separate HTTP from logic from storage.

In practice

  • Keep shared infrastructure (database connection, HTTP server setup, config) in a platform package that features depend on, never the reverse.
  • Avoid catch-all utils or common packages; put helpers in the feature that uses them, or name a package for what it does (money, slug).
  • When features need each other, go through a small public API rather than reaching into implementation files.
  • Large apps go further and split each feature into api and implementation modules, so features depend only on each other's APIs — the pattern used by large Android and Gradle codebases.

Very small services can stay flat in one package; feature packages pay off once there are several distinct capabilities. See the modular design lesson.

Learn it in depth