Dependency Injection

Rays relies on struct tags to identify injection points.

Basic Injection

Add @:"Inject" to any field that is a pointer to a struct or an interface. The container will automatically find and wire the matching Beam by type.

Note: You cannot inject into a concrete value type (like db Database instead of db *Database), as this would break the singleton pattern by copying the struct by value.

import . "github.com/BeamFoundry/rays/pkg/core"

type OrderService struct {
    Service
    inventory *InventoryService `@:"Inject"`
    payment   PaymentGateway    `@:"Inject"` // Interfaces do not need a '*'
}

Private Field Superiority

Idiomatic Go encourages hiding internal state. Rays uses reflection to inject into private fields, meaning your dependencies aren’t exposed to other packages.

Qualifier & Primary (Resolving Ambiguity)

If you have multiple Beams of the same type, the container will panic with an ambiguity error. Use Qualifier to differentiate them, and Primary to set a default fallback.

A default Qualifier is always defined for all beams. When using Configuration, the qualifier it is taken from the method’s name, uncapitialized. On it can be overwitten using the Qualifier interface in an anonymous struct (a non-named type struct) as the first argument to the method.

import . "github.com/BeamFoundry/rays/pkg/core"

type CloudConfig struct {
    Configuration
}

// Define identical types, marking one as Primary
func (c *CloudConfig) PrimaryStorage(
    _ struct {
        Primary
        Qualifier `Name:"aws"`
    },
) *Storage {
    return &Storage{provider: "AWS"}
}

func (c *CloudConfig) BackupStorage(
    _ struct {
        Qualifier `Name:"gcp"`
    },
) *Storage {
    return &Storage{provider: "GCP"}
}

// 1. Inject a specific one using the Qualifier tag syntax
type BackupService struct {
    Service
    store *Storage `@:"Inject,Qualifier{gcp}"`
}

// 2. Inject the primary one implicitly (no Qualifier needed)
type ImageService struct {
    Service
    store *Storage `@:"Inject"` // Automatically resolves to the PrimaryStorage (AWS)
}

BeamAttributes

Other attributes can be defined. Those are easily extendable.

For example the Server BeamAttribute, defined in the core package, with a corresponding BeamAttributeEvaluator implementation (hidden in an internal package), lets you another attribute in the injection tag.

type Server interface {
	  BeamAttribute
	  _Server()
}

type ServerEvaluator struct {
	  Server
}

// Ensute type is compiled in.
var _, _ = any(&ServerEvaluator{}).(BeamAttributeEvaluator)

// Ensure interface
var _ BeamAttributeEvaluator = &ServerEvaluator{}

// EvaluateBeamAttribute implements [BeamAttributeEvaluator].
func (q *ServerEvaluator) EvaluateBeamAttribute(beam Beam) string {
    // FindFieldForInterface() and GetFieldFirstTag() are defined in the [core] package.
    field := FindFieldForInterface[Server](beam.AnnotatedType())
    return GetFieldFirstTag(field, []string{"_", "Name", "Server"})
}

So example your could wire on the Server attribute :

import . "github.com/BeamFoundry/rays/pkg/core"

type CloudConfig struct {
    Configuration
}

func (c *CloudConfig) AwsStorage(
    _ struct {
        Primary
    },
) *Storage {
    return &Storage{provider: "AWS"}
}

func (c *CloudConfig) GcpStorage(
    _ struct {
        Server `Name:"gcp"`
    },
) *Storage {
    return &Storage{provider: "GCP"}
}

type BackupService struct {
    Service
    store *Storage `@:"Inject,Server{gcp}"`
}

type ImageService struct {
    Service
    store *Storage `@:"Inject"`
}

Optional Injection (Optional[T])

By default, if you inject a dependency that does not exist, the container panics and stops booting. If a dependency is truly optional, wrap it in Optional[T] (in the lang package).

import (
    . "github.com/BeamFoundry/rays/pkg/core"
    . "github.com/BeamFoundry/rays/pkg/lang"
)

type MetricsService struct {
    Service
    tracer Optional[*DatadogTracer] `@:"Inject"`
}

func (m *MetricsService) Report() {
    if m.tracer.IsPresent() {
        m.tracer.Get().Send(...)
    }
}

Multi-Binding (Slices & Maps)

Advanced architectural patterns (like the Strategy Pattern) rely on injecting multiple implementations of an interface or struct. Rays natively supports multi-binding into slices and maps for both interface types and struct pointers.

Slice Injection ([]T)

Injecting into a slice will populate it with all registered Beams that match the slice’s underlying type.

Map Injection (map[string]T)

Injecting into a map requires the key to be of type string. Rays will map the injected Beams by their Qualifier name. The key will be the injected Beam’s Qualifier value.

import . "github.com/BeamFoundry/rays/pkg/core"

type CommandHandler interface {
    HandleCommand()
}

type ServerNotification struct {
    Configuration
    Server `Name:"primary"`
}

func (s *ServerNotification) Email() CommandHandler {
    ...
}

func (s *ServerNotification) Sms() CommandHandler {
    ...
}

type NotificationDispatcher struct {
    Service

    // Injects all available handlers as a slice
    allHandlers []CommandHandler `@:"Inject"`

    // Injects handlers mapped by their qualifier name ("email", "sms")
    handlerMap map[string]CommandHandler `@:"Inject"`
}

func (d *NotificationDispatcher) Dispatch(handlerName string) {
    if handler, exists := d.handlerMap[handlerName]; exists {
        handler.HandleCommand()
    }
}

Map Injection with custom grouping

Of course you can group on different attributes, an a default value can be specified from beams not having the attribute defined.

import . "github.com/BeamFoundry/rays/pkg/core"

type PrimaryServerNotification struct {
    Configuration
    Server `Name:"primary"`
}

func (p *PrimaryServerNotification) Email() CommandHandler {
    ...
}

func (p *PrimaryServerNotification) Sms() CommandHandler {
    ...
}

type SecondaryServerNotification struct {
    Configuration
}

func (s *SecondaryServerNotification) Log() CommandHandler {
    ...
}

type NotificationDispatcher struct {
    Service

    // Inject all handlers by Server attribute, defaulting to secondary
    handlers map[string][]CommandHandler `@:"Inject,GroupBy=Server,Default=secondary"`
}

func (d *NotificationDispatcher) Dispatch (serverName string) {
  if handlers, exists := d.handlers[serverName]; exists {
    for _, handler := range handlers {
      handler.HandleCommand()
    }
  }
}