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.