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