App

Dependencies And Services

Initialize shared dependencies once and expose them through typed context helpers.

By Guillermo Alvarez - Published - Updated

The composition root

Service packages live in top-level services/, outside the web-facing app/ tree. init/dependencies.go initializes shared dependencies once:

func Dependencies(deps *lazydeps.Scope) error {
    _, err := lazydeps.Service(deps, "helloworldservice", func(ctx context.Context) (
        context.Context,
        helloworldservice.Service,
        error,
        context.CancelFunc,
    ) {
        service := helloworldservice.New()
        return helloworldservice.WithContext(ctx, service), service, nil, nil
    })
    return err
}

Views and public files are passed to lazyapp.New from init/app.go. Application context should focus on application services.

lazydeps.Service records each initialized dependency in the application dependency graph. When a service uses another service during initialization, call the dependency reference's Use method inside the service block so GoLazy can record that edge.

Typed context helpers

Each service owns an unexported key type and exported helpers:

type contextKey struct{}

func WithContext(ctx context.Context, service Service) context.Context {
    return context.WithValue(ctx, contextKey{}, service)
}

func FromContext(ctx context.Context) (Service, bool) {
    service, ok := ctx.Value(contextKey{}).(Service)
    return service, ok
}

An unexported key type prevents collisions with other packages.

Resolve dependencies in constructors

Controller constructors validate their requirements:

hello, ok := helloworldservice.FromContext(ctx)
if !ok {
    return nil, fmt.Errorf(
        "hello world service is missing from application context",
    )
}

This keeps concrete controller constructors uniform while still making missing dependencies fail explicitly.

Shared and request-local state

Application services may be shared when they are safe for concurrent use. Controller instances must not be shared.

The framework derives a request-specific context containing the http.ResponseWriter before constructing a controller. Render state stays on that new controller instance.

Run race tests whenever shared service behavior changes:

go test -race ./...

What belongs in context

In GoLazy, application context is dependency wiring. Store initialized services and framework infrastructure there.

Do not use it as a general-purpose parameter bag for:

  • Optional action arguments.
  • View data.
  • Values that can be passed directly within a service.
  • Mutable request state owned by a controller.

Keep helper APIs typed and package-owned so dependency access remains searchable and testable.

Service design

Services should expose application operations rather than HTTP concepts. The sample app's helloworldservice provides Hello; the controller decides how that value becomes the page title and template data.

This separation allows focused unit tests without constructing requests or templates.

Mail, files, and other app services

Mailers, file catalogs, object storage, and generated media services follow the same dependency pattern. Initialize them once, store them with typed context helpers, and expose application-level operations to controllers.

Continue with Mailers And Storage for the mail and file-service examples.