Skip to content

Application Services ​

Application Services own business behavior.

Controllers, commands, jobs, events, and schedules should call services rather than each reimplementing workflows at their own runtime boundary.

Service Shape ​

go
package reports

type Service struct {
	repo   *Repository
	queue  Queue
	cache  Cache
	events Events
}

func NewService(repo *Repository, queue Queue, cache Cache, events Events) *Service {
	return &Service{
		repo:   repo,
		queue:  queue,
		cache:  cache,
		events: events,
	}
}

Required dependencies stay required. Optional dependencies should be modeled explicitly.

One Service, Multiple Callers ​

The JSON API Route is the smallest complete example: users.Service.Find is constructed by Wire, called by a controller, unit-tested without HTTP, and observed through the generated route. A command, job handler, subscriber, or schedule can call the same typed service method without importing web or copying the workflow.

Verify a service-first change before running the HTTP runtime:

bash
forj build
go test ./...

Expected result: generated wiring succeeds and service tests pass without starting a listener, worker, or scheduler. Use the scenario's forj api and curl check only when confirming the HTTP boundary.

Inputs and Outputs ​

Use typed inputs for service operations. This illustrative excerpt omits the workflow inside Create:

go
type CreateReportInput struct {
	Name      string
	OwnerID   string
	Immediate bool
}

func (s *Service) Create(ctx context.Context, input CreateReportInput) (Report, error) {
	// ...
}

This keeps service APIs independent from HTTP request structs, CLI flag structs, and queue payload structs.

Runtime Boundaries ​

Multiple entry points may call the same service:

  • HTTP controller
  • CLI command
  • queue job handler
  • event subscriber
  • scheduler entry

The service should not need to know whether HTTP, a command, a job, or a schedule called it unless that distinction is part of the business behavior.

Infrastructure Access ​

Services should receive infrastructure through constructor injection.

Prefer generated accessors and interfaces at the consumer boundary:

  • cache accessors for derived data
  • storage disks for files and blobs
  • queues for background work
  • event buses for fan-out
  • repositories for persistence
  • metrics wrappers for application metrics

Avoid importing backend driver packages into services.

Transactions and Consistency ​

Keep consistency decisions close to the service method that owns the workflow.

If a workflow writes to the database, dispatches a job, updates cache, and publishes an event, document the ordering and failure behavior in the service or feature docs. Do not hide consistency policy in controllers or middleware.

Next Steps ​