Core Framework

Services and Context

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

By Guillermo Alvarez - Published - Updated

The composition root

init/context.go initializes shared dependencies once:

func Context(ctx context.Context) context.Context {
    posts, err := postservice.New()
    if err != nil {
        panic(err)
    }

    ctx = timeservice.WithContext(ctx, timeservice.New())
    ctx = postservice.WithContext(ctx, posts)
    return ctx
}

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

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:

posts, ok := postservice.FromContext(ctx)
if !ok {
    return nil, fmt.Errorf(
        "posts 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.

When lazyapp.App.ListenAndServe starts the server, it installs the application context as the server base context. Inside the app, request processing uses r.Context() so services and framework dependencies come from one context path.

Routes construct controller prototypes at startup. For each request, GoLazy copies the prototype into a pooled instance, binds the current request and response writer, then resets request state before returning the instance to the pool. Do not store request-specific data in constructor fields. Put request setup in actions or BeforeAction.

If you build your own http.Server, set BaseContext to return the application context:

server := &http.Server{
    Addr:    ":3000",
    Handler: app,
    BaseContext: func(_ net.Listener) context.Context {
        return app.Context
    },
}

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. A posts service can provide List and Get; the controller decides how those results become status codes and HTML.

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