On this page

Dependency injection

Pass dependencies through ordinary Go constructors. App creates shared clients; module assembly selects the clients its services need. A handler receives its operation directly, so it never needs to discover or construct dependencies.

Construction and ownership#

flowchart TB
    accTitle: Construct once, borrow explicitly
    accDescr: App opens and owns infrastructure, constructs capabilities and authentication, and passes shared operations into HTTP, CLI, task and event handlers. Consumers borrow dependencies and never close app resources.
    App[App assembly] -->|Opens and owns| Resources[(Pools and clients)]
    Resources --> Capabilities[Notifier and Media library]
    Resources --> Module[Module registration]
    App --> Auth[Authentication service]
    Auth --> Module
    Capabilities --> Module
    Module --> Service[Business service]
    Service --> HTTP[HTTP handler]
    Service --> CLI[CLI command]
    Service --> Jobs[Task and event handlers]

Two assembly structs have different purposes:

Struct Contains Used by
dependencies.Resources Existing clients, pools and shared capabilities Module Register functions and app assembly
dependencies.Targets Routers and registration destinations Module Register functions

Copying Resources shares the clients; it does not clone connections. Modules never mutate its maps or close borrowed clients. Services and handlers receive specific fields rather than retaining the complete bundle.

App keeps both bundles on one private application, alongside configuration, the lifecycle owner and process handles. Native registries live directly in application.targets; the worker and module handlers use the same instances. The private application is never passed into modules or platform capabilities.

Construct a service explicitly#

Identity assembles its profile operations with ordinary constructors:

repo := repository.NewUserRepository(resources.DB)
users, err := service.NewUserService(repo)
if err != nil {
    return err
}

This module-registration fragment imports github.com/wisalgroup/labena/internal/modules/identity/repository and github.com/wisalgroup/labena/internal/modules/identity/service. resources is the borrowed dependencies.Resources argument. No query runs here.

The same users value is passed to HTTP registration and modulecli.New(users). They call the same rules; only their input/output adaptation differs. Follow the persisted HTTP and CLI workflow to see both callers.

Service and repository contracts#

Keep public contracts in the owner's package:

identity/service/
  service.go      UserService and AuthService interfaces
  user.go         userService and NewUserService
  auth.go         authService and NewAuthService
  session.go      Authentication methods on *authService

identity/repository/
  repository.go   Focused repository interfaces
  user.go         userRepository and NewUserRepository
  session.go      sessionRepository and NewSessionRepository

A larger use case can live in another file on the same receiver. File organization does not require another abstraction, service instance or constructor.

Multiple consumers import the same owner contract. Avoid duplicating UserService, mail.Sender or messaging.Publisher in each caller just to change the package name. Add interfaces where substitution or a stable business API is useful; concrete capabilities such as *notification.Notifier and *media.Library are intentional too.

Required arguments and options#

Use explicit arguments for a small required dependency set. Use a cohesive options struct when construction has several collaborators or optional controls:

notifier, err := notification.New(db, notification.Options{
    Queue:     queue,
    Mailer:    mailer,
    MailRenderer: views,
})
if err != nil {
    return err
}

This construction fragment assumes already constructed clients and imports github.com/wisalgroup/labena/internal/platform/notification. The caller retains the resulting notifier for its operations; constructing it sends nothing.

Config describes settings; Options describes constructor controls and collaborators. A business service must not import app or read environment variables to complete its construction.

Cross-module calls and transactions#

Pass a business service from one module to another as an explicit assembly argument. Keep business services out of Resources. App may construct services needed before registration using owner constructors; transport handlers never construct repositories.

Use a direct call when the current operation requires a result. Use an event for an independent reaction with an accepted delivery guarantee. The local event bus does not make a required operation reliable across a process crash.

Interaction Ownership boundary
Read/update an account Call Identity's service and apply caller authorization
Store notification intent with a business change Use Notifier.Enqueue inside the same database transaction
Send email or write an object External effect; cannot roll back with SQL
Publish a local event after commit Commit remains valid if publication fails

DB and Transactions must use the same pool. Repositories resolve their executor from context to join the transaction. Normal service operations own transaction scope; Identity's security operations have explicit exceptions for durable failed proof counters and refresh-reuse revocation. See transactions.

Context carries scope, not dependencies#

Propagate the caller's context to every operation. It carries cancellation, deadlines, transaction scope and logging/trace metadata. Inject clients through arguments instead of retrieving them from context or a global container.

Handlers must finish their work before returning. Put deferred processing on the queue instead of spawning an unowned goroutine that outlives the handler.

Optional capabilities#

Resource Disabled behavior
Cache, Pusher, RateLimiter May be nil when their feature is disabled
Disks Only selected disks are opened; disabled storage has no usable disk
TracerProvider, MeterProvider App supplies providers; disabled telemetry is no-op
Mailer Always constructed; SMTP connects on sending
DB/Transactions Can be absent for limited assembly; installed modules cannot serve without them

Validate the dependencies required by an enabled operation during construction. Do not defer an inevitable nil dereference until a request. A nil registration target omits a transport; it does not grant access or change a service policy.

Use a capability independently#

Within this application template, instantiate a platform capability from its actual clients and options without importing app or Resources. Its integration package is needed only when registering framework transports.

For reuse from another Go module, choose a pkg capability: Go's internal import rule deliberately limits platform packages. See the package index.

Optional push#

Resources.Pusher is the optional push.Sender; it is nil when disabled. App also passes it to Notifier for durable channel delivery. Choose direct push for an immediate provider call or notifications for persisted recipient delivery. Inject the selected collaborator into your service.

Account and authentication operations#

Identity separates profile operations (UserService) from proof/token/session operations (AuthService). Both expose owner contracts with private implementations. SignInResult carries the committed user, tokens and Created flag; HTTP maps the flag to 201/200. See Identity for account policy and its security transaction exceptions.

Verify wiring#

Use narrow fakes for service decisions and real disposable backends for SQL, Redis, retries and ownership isolation. App tests call public Serve/Execute; module tests can supply only the targets they need. Keep business tests under tests/<module>.

Continue with module registration, shared types, or lifecycle.

Documentation

Search guides, sections, and API names.