Organizing Generated Code
GoForj make commands generate code around Go packages.
This follows a common Go pattern: organize by package ownership, not by global resource type.
Many web frameworks teach folders such as controllers, jobs, models, and commands. In Go, it is often easier to work in packages where related code lives side by side with the behavior it owns.
internal/reports/
controller.go
sync_cmd.go
generate_job.go
daily_schedule.go
report_generated_event.go
report_generated_subscriber.go
service.go
repository.goThe package is the boundary. The files inside it are different entry points into the same App behavior.
What Package Scope Means In Go
Every .go file in a directory belongs to the same package when it declares the same package name.
// internal/reports/service.go
package reports
type Service struct{}
func NewService() *Service {
return &Service{}
}
func DoThing() {}Another file in the same directory is still part of package reports:
// internal/reports/sync_cmd.go
package reports
type SyncCmd struct {
service *Service
}
func NewSyncCmd(service *Service) *SyncCmd {
return &SyncCmd{service: service}
}Code outside the package imports it and uses the package name as the scope:
package app
import "example.com/app/internal/reports"
func Register() {
service := reports.NewService()
_ = reports.NewSyncCmd(service)
reports.DoThing()
}That reports. prefix is the Go package boundary. It is why the generated files are placed beside each other: Controller, SyncCmd, GenerateJob, DailySchedule, Repository, and Service all become names inside the same reports package.
The Mental Model
A package should answer one question:
What part of the App owns this behavior?For example, a reports package might own:
- HTTP handlers for report routes
- CLI commands operators can run
- jobs that do background report work
- schedules that enqueue or run report work
- events the package publishes
- subscribers that react to report-related events
- services and repositories used by those entry points
The controller, command, job, and schedule are not separate architectural layers. They are entry points into the same package-owned behavior.
HTTP request -> reports.Controller -> reports.Service
CLI command -> reports.SyncCmd -> reports.Service
Queue worker -> reports.GenerateJob -> reports.Service
Scheduler process -> reports.DailySchedule -> reports.Service
Event bus -> reports.ReportGeneratedSubscriberThis keeps the dependency direction simple: entry points call package services, and package services use repositories, clients, caches, queues, storage, or events through explicit constructor dependencies.
Why This Shape
Go packages are small, named ownership units.
When related files live together:
- names stay shorter because the package provides context
- imports reveal ownership
- Wire constructors stay close to the code they construct
- controllers do not need to know where jobs or repositories live elsewhere
- deleting or moving a feature is easier because the package is visible as a unit
This also makes generated files less mysterious. A make command creates a starting point, wires the constructor where needed, and leaves the package for you to finish.
Names Become Packages
Most make commands turn the resource name into a package path. Start with flat packages such as internal/reports; add nesting only when the extra package boundary is useful.
Read grouped names from left to right:
- package ownership comes first, even when it is a single segment
- the generated resource name usually comes last
- controllers are package anchors, so the whole grouped name becomes the controller package
Examples:
| Command | Creates |
|---|---|
forj make:controller reports | internal/reports/controller.go |
forj make:job reports:generate | internal/reports/generate_job.go |
forj make:schedule reports:daily | internal/reports/daily_schedule.go |
For jobs and schedules, reports is the package path and generate or daily is the generated file name. For controllers, reports names the controller package itself.
A Complete Package Example
Start with an HTTP controller:
forj make:controller reports
# creates internal/reports/controller.go
# injects app/wire/inject_http_controllers_app.go
# updates app/routes.goAdd an operator command:
forj make:command reports:sync
# creates internal/reports/sync_cmd.go
# exposes reports:sync
# injects app/wire/inject_cmd_app.go
# updates app/commands.goAdd a background job:
forj make:job reports:generate --queue reports
# creates internal/reports/generate_job.go
# injects app/wire/inject_jobs_app.goAdd a schedule:
forj make:schedule reports:daily --every 24h
# creates internal/reports/daily_schedule.go
# injects app/wire/inject_schedules_app.go
# updates app/schedules.goAdd an event and subscriber:
forj make:event reports:report-generated
# creates internal/reports/report_generated_event.go
forj make:subscriber reports:report-generated
# creates internal/reports/report_generated_subscriber.go
# injects app/wire/inject_subscribers_app.goAdd a model and repository:
forj make:model reports --package reports
# creates internal/reports/report.go
# wires ReportRepo in app/wire/inject_repositories_app.goThe package starts to look like this:
internal/reports/
controller.go
daily_schedule.go
generate_job.go
report.go
report_generated_event.go
report_generated_subscriber.go
sync_cmd.goYou would usually add service code beside those files:
internal/reports/
service.go
report.goThe generated files are entry points and adapters. The service owns the workflow.
Commands Are Slightly Different
Application command names are operator-facing names.
Prefer short command names:
forj make:command reports:syncThis creates:
internal/reports/sync_cmd.goand exposes the command as:
reports:syncIf the command belongs somewhere other than its default package, keep the command name short and use -d for placement:
forj make:command reports:sync -d ./internal/opsThat creates internal/ops/sync_cmd.go while still exposing reports:sync. Use command names for operator meaning, not as a full package path.
Bare Commands Stay In internal/cmd
A bare command is treated as a top-level App command:
forj make:command syncCreates:
internal/cmd/sync_cmd.goThat is useful for simple App-wide commands. It does not create internal/sync because a single word does not necessarily describe a package boundary.
Use a grouped name or -d when the command belongs to a feature package. These are equivalent:
forj make:command reports:sync
forj make:command sync -d ./internal/reports --name reports:syncBoth create internal/reports/sync_cmd.go and expose the command as reports:sync. The first form derives placement from the grouped command name. The second form keeps the generated type name short and sets placement/signature explicitly.
Package Names Are Go Names
Generated package directories and package declarations use compact lowercase Go names.
forj make:controller BillingPortalCreates a package named:
billingportalnot:
billing_portalThat matches normal Go package style. File names can still use underscores, but package names should be short lowercase identifiers.
What Each Make Command Adds
| Command | Package behavior |
|---|---|
make:controller reports | creates internal/reports/controller.go |
make:command reports:sync | creates internal/reports/sync_cmd.go and exposes reports:sync |
make:job reports:generate | creates internal/reports/generate_job.go |
make:schedule reports:daily | creates internal/reports/daily_schedule.go |
make:event reports:report-generated | creates internal/reports/report_generated_event.go |
make:subscriber reports:report-generated | creates internal/reports/report_generated_subscriber.go |
make:model reports --package reports | creates internal/reports/report.go and wires ReportRepo |
make:migration create_reports | creates migration files in migrations/ |
make:queue is resource configuration rather than package generation. It updates queue environment keys.
Removing Generated Package Entries
Use --remove with the same make command when a generated entry point no longer belongs in the package:
forj make:job reports:generate --remove
forj make:schedule reports:daily --remove
forj make:subscriber reports:report-generated --removeThe command removes the generated file and the wiring it knows how to manage. For colocated packages, that usually means the package gets smaller without forcing you to hunt through Wire files manually.
--remove does not delete the package directory, service code, repositories, tests, or hand-written references. That is intentional: those files are App-owned code, and GoForj should not guess whether they still matter.
Run forj build after removal. If application code still imports or references the removed type, the build will point at the remaining work.
A Good Package Shape
A useful package usually has entry points near the top and workflow code underneath:
internal/reports/
controller.go # HTTP entry point
sync_cmd.go # CLI entry point
generate_job.go # queue entry point
daily_schedule.go # scheduler entry point
report.go # model and repository helpers
report_generated_event.go # event contract
report_generated_subscriber.go # event entry point
service.go # workflow behaviorThe entry points should stay thin. They translate input, call services, and return output.
The service should own the business workflow:
package reports
type Service struct {
repository *Repository
}
func NewService(repository *Repository) *Service {
return &Service{repository: repository}
}If a generated controller, command, job, or schedule needs *Service, add it to the constructor and wire NewService through the App services provider set.
Common Mistakes
Common mistakes
- Do not create one package per file just because a generator can.
- Do not put all controllers, jobs, commands, and services in one global package.
- Do not make command names longer just to mirror package depth.
- Do not use snake case package names.
- Do not put business workflows directly in generated entry points.
- Do not hand-edit generated wiring before trying the make command path.
Next Steps
- Make Commands lists each generator and the wiring it updates.
- Naming Conventions explains stable runtime names.
- Wiring Recipes shows where service providers belong.
- Controllers explains HTTP entry points.
- Commands explains App CLI entry points.
