Project Structure
Fairway encourages a flat, slice-oriented structure. Each command, view, or automation lives in its own package and self-registers via init().
Recommended Layout
myapp/
├── main.go # wires everything, starts server
├── init_imports.gen.go # generated: imports all slices
├── event/ # shared event definitions
│ └── events.go
├── change/ # commands (write side)
│ ├── registry.go # HttpChangeRegistry
│ ├── registeruser/
│ │ └── register_user.go # one command per package
│ └── changeemail/
│ └── change_email.go
├── view/ # views (read side)
│ ├── registry.go # HttpViewRegistry
│ └── getcurrentuser/
│ └── get_current_user.go
└── automate/ # automations
├── registry.go # AutomationRegistry
└── userregistered/
└── send_welcome.go
Self-Registration via init()
Each slice registers itself:
// change/registeruser/register_user.go
package registeruser
import "github.com/err0r500/fairway/examples/realworldapp/change"
func init() {
Register(&change.ChangeRegistry)
}
func Register(registry *fairway.HttpChangeRegistry) {
registry.RegisterCommand("POST /users", httpHandler)
}
Why extract Register? It's the recommended entrypoint for tests:
func TestRegisterUser(t *testing.T) {
given.Events(store, userAlreadyExists)
when.CallingEndpoint(Register, "POST /users", body)
then.ResponseIs(http.StatusConflict)
then.EventsInStore(store, /* expected events */)
}
Tests can call Register directly without importing the global registry.
The registry is declared in the parent package:
// change/registry.go
package change
import "github.com/err0r500/fairway"
var ChangeRegistry = fairway.HttpChangeRegistry{}
Code Generation
Slices register via init(), but Go only runs init() for imported packages. Fairway provides a generator that scans your project and creates an import file:
This generates init_imports.gen.go:
// Code generated by go generate; DO NOT EDIT.
package main
import (
_ "myapp/change/registeruser"
_ "myapp/change/changeemail"
_ "myapp/view/getcurrentuser"
_ "myapp/automate/userregistered"
)
Run go generate ./... after adding new slices.
Why This Structure?
No shared code between slices. Each command/view/automation is a standalone package. No merge conflicts.
No manual wiring. Slices self-register. Adding a feature = adding a package + running go generate.
Easy to delete. Remove the package, run go generate, done.
Example
See the realworldapp example for a complete implementation.