Skip to content

Project Structure

Fairway encourages a flat, slice-oriented structure. Each command, view, or automation lives in its own package and self-registers via init().


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:

//go:generate go run github.com/err0r500/fairway/cmd

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.