Forge
1.x
Docs/Forge/Lazy and Optional
Open

Reading4 min
Updated31 Jul 2026
Sourcev1/vessel/lazy-optional.mdx

Vessel provides wrapper types that let you defer dependency resolution, handle missing services gracefully, and create fresh instances on demand.

Lazy Dependencies01

vessel.Lazy[T] wraps a dependency that is resolved on first access. The resolution happens once; subsequent calls return the cached result. This is useful for breaking circular dependencies or deferring expensive initialization.

import "github.com/xraph/vessel"

type UserService struct {
    cache *vessel.Lazy[*CacheService]
}

func NewUserService(c vessel.Vessel) *UserService {
    return &UserService{
        cache: vessel.NewLazy[*CacheService](c, "cache"),
    }
}

func (s *UserService) GetUser(id string) (*User, error) {
    cache, err := s.cache.Get()
    if err != nil {
        return nil, err
    }
    return cache.Get("user:" + id)
}

Lazy API

MethodDescription
Get() (T, error)Resolve on first call, return cached value after
MustGet() TLike Get but panics on error
IsResolved() boolWhether the dependency has been resolved
Name() stringThe service name being resolved

Optional Lazy Dependencies02

vessel.OptionalLazy[T] resolves on first access but returns the zero value (without error) if the service is not registered. Use this when a dependency is nice to have but not required.

type MetricsService struct {
    tracer *vessel.OptionalLazy[*Tracer]
}

func NewMetricsService(c vessel.Vessel) *MetricsService {
    return &MetricsService{
        tracer: vessel.NewOptionalLazy[*Tracer](c, "tracer"),
    }
}

func (s *MetricsService) Record(name string) {
    tracer, err := s.tracer.Get()
    if err != nil {
        // Handle unexpected resolution error
        return
    }
    if tracer != nil {
        tracer.Span(name)
    }
}

OptionalLazy API

MethodDescription
Get() (T, error)Resolve on first call; zero value if not found
MustGet() TLike Get but panics on error (not on missing)
IsResolved() boolWhether resolution has been attempted
IsFound() boolWhether the dependency was found (valid after resolution)
Name() stringThe service name being resolved

Provider (Transient Access)03

vessel.Provider[T] wraps a dependency and resolves a fresh instance on every .Provide() call. Use this when you need a new instance each time (e.g., per-request objects).

type Handler struct {
    reqProvider *vessel.Provider[*Request]
}

func NewHandler(c vessel.Vessel) *Handler {
    return &Handler{
        reqProvider: vessel.NewProvider[*Request](c, "request"),
    }
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    req, err := h.reqProvider.Provide()
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    // req is a fresh instance each call
    _ = req
}

Provider API

MethodDescription
Provide() (T, error)Resolve and return a (potentially new) instance
MustProvide() TLike Provide but panics on error
Name() stringThe service name being resolved

Discovery Checks04

Check whether a service is registered before resolving.

// Check by type (constructor-registered services)
if vessel.HasType[*CacheService](c) {
    cache, _ := vessel.Inject[*CacheService](c)
    _ = cache
}

// Check named type
if vessel.HasTypeNamed[*Database](c, "replica") {
    replica, _ := vessel.InjectNamed[*Database](c, "replica")
    _ = replica
}

Must Variants05

For startup code where failure should be fatal, use the Must variants that panic instead of returning errors:

// Type-based must
userSvc := vessel.MustInject[*UserService](c)

// Named must
primary := vessel.MustInjectNamed[*Database](c, "primary")

// Group must
handlers := vessel.MustInjectGroup[Handler](c, "http")

Use Must variants only during application startup or in tests. Prefer error-returning variants in production request paths.