#1281·fx

Feature Request: Support for Partial Struct Initialization Pattern

Author: scr-oathCreated Nov 10, 2025Updated Nov 10, 2025

Problem Statement

When building services with fx, there's a common pattern that's difficult to express cleanly:

  • Injected dependencies (config, logger, database connections)
  • Computed/derived fields (caches, connection pools)
  • Internal state (mutexes, channels, maps)

Currently, fx's design constraints force verbose boilerplate for this pattern. Specifically:

The fx.In Constraints

  1. fx.In must be directly in params - Can't use embedded base structs that fx would need to flatten
  2. Return types cannot embed fx.In - fx/dig explicitly rejects: "cannot provide parameter objects: embeds a dig.In"
  3. Anonymous embedding fails - Can't wrap params struct in return type

These constraints lead to tedious field-by-field copying:

go
type ServiceParams struct {
    fx.In
    Config *Config
    Logger *Logger
    DB     *sql.DB
}

type Service struct {
    config *Config
    logger *Logger
    db     *sql.DB
    cache  *Cache      // computed field
    mu     sync.Mutex  // internal state
}

func NewService(p ServiceParams) *Service {
    // Manual field-by-field copying
    return &Service{
        config: p.Config,
        logger: p.Logger,
        db:     p.DB,
        cache:  NewCache(p.Config.CacheSize),
    }
}

This becomes painful with 5+ dependencies and forces duplicate field definitions.


Two Key "Aha Moments"

While exploring solutions, two constraints stood out as potential improvement opportunities:

1. fx.In Cannot Appear in Return Types

This is a fundamental dig constraint that prevents the cleanest solution:

go
// ❌ This doesn't work - fx rejects: "cannot provide parameter objects: embeds a dig.In"
type ServiceParams struct {
    fx.In
    Config *Config
    Logger *Logger
}

func NewService(p ServiceParams) *ServiceParams {
    // Would love to just return the params with computed fields added
    return &p
}

Question: Could fx support this with an annotation or tag? E.g., fx.In with fx:"allow-in-output" or a new fx.Partial marker?

2. Anonymous Embedding of Base Structs Fails

The most Go-idiomatic pattern would be:

go
// ❌ This doesn't work - fx can't flatten the embedded base
type ServiceBase struct {
    fx.In  // OR could be separate params struct
    Config *Config
    Logger *Logger
}

type Service struct {
    ServiceBase  // anonymous embedding for clean field access
    cache *Cache
    mu    sync.Mutex
}

func NewService(base ServiceBase) *Service {
    return &Service{
        ServiceBase: base,
        cache:       NewCache(base.Config.Size),
    }
}

Question: Could fx support recognizing anonymous embedded structs (with or without explicit tags) as "inject these fields"?


Use Case: Universal Pattern

While this came up during google/wireuber/fx migration (wire's wire.Struct handled this elegantly), this is a general Go DI pattern:

Wire Context (for reference)

In wire, you could mark fields for partial initialization:

go
type Service struct {
    Config *Config
    Logger *Logger
    Cache  *Cache  // manually initialized
}

// wire.Struct(new(Service), "Config", "Logger")
// Wire injects Config/Logger, user provides Cache

This pattern is common across many Go codebases:

  • Services with lifecycle-managed resources
  • Services with derived state from config
  • Large microservice codebases with 10+ dependencies per service
  • Library/module authors who want to provide fx.Module that consumers can include

What's Been Tried: Reflection-Based Solution

Through experimentation, I've developed a production-ready pattern using Go's reflection capabilities. This works beautifully but highlights opportunities for native fx support.

AutoConstructor Pattern (★ Recommended Approach)

The most idiomatic and composable solution:

go
// Define base with injected dependencies
type ServiceBase struct {
    Config *Config
    Logger *Logger
    DB     *sql.DB
}

// Define real struct with embedded base + internal state
type Service struct {
    ServiceBase
    cache map[string]any
    mu    sync.Mutex
}

// Auto-generate provider for ServiceBase
fx.Provide(AutoConstructor[ServiceBase]())

// Write normal constructor
func NewService(base ServiceBase) *Service {
    return &Service{
        ServiceBase: base,
        cache:       make(map[string]any),
    }
}

fx.Provide(NewService)

With lifecycle hooks:

go
func NewService(base ServiceBase, lc fx.Lifecycle) *Service {
    s := &Service{
        ServiceBase: base,
        cache:       make(map[string]any),
    }
    lc.Append(fx.Hook{
        OnStart: s.start,
        OnStop:  s.stop,
    })
    return s
}

In an fx.Module (library use case):

go
// Library code in github.com/myorg/mylib
var Module = fx.Module(
    "MyLibrary",
    fx.Provide(
        AutoConstructor[ServerBase](),
        NewServer,
    ),
)

// Consumer code
fx.New(
    mylib.Module,  // Just works!
    // ...
)

Why this is the best pattern:

No fx.In pollution - Base struct has no fx markers
No fx:"-" tags - Clear separation: dependencies in Base, state in Real
Normal constructors - Write regular Go functions, not reflection wrappers
Composable - Multiple constructors can share the same Base
Works in fx.Module - Can be packaged in libraries for reuse
Natural lifecycle - fx.Lifecycle, fx.Shutdowner work normally in constructors
Wire-like ergonomics - Mirrors wire.Struct conceptually


Alternative Explored: AutoProvidePartial + fx.Decorate

An alternative pattern uses fx:"-" tags and fx.Decorate:

go
type Service struct {
    Config *Config              // Injected by fx
    Logger *Logger              // Injected by fx
    Cache  *Cache   `fx:"-"`    // Manual initialization
    State  *State   `fx:"-"`    // Manual initialization
}

fx.Provide(AutoProvidePartial[Service]())

fx.Decorate(
    func(s *Service, lc fx.Lifecycle) *Service {
        s.Cache = NewCache(s.Config.CacheSize)
        s.State = &State{Ready: false}
        
        lc.Append(fx.Hook{
            OnStart: func(ctx context.Context) error {
                s.State.Ready = true
                return nil
            },
        })
        
        return s
    },
)

Why this is less ideal:

Cannot be packaged in fx.Module - fx.Decorate is scoped to the module it's in
Library authors can't use it - You can provide AutoProvidePartial[Service]() in a module, but consumers would need to call fx.Decorate separately
Not composable - Can't ship a complete library module that "just works"

The fundamental issue: fx.Decorate doesn't work across module boundaries. A library can't do:

go
// ❌ This doesn't work - Decorate is scoped to this module only
var Module = fx.Module(
    "MyLibrary",
    fx.Provide(AutoProvidePartial[Service]()),
    fx.Decorate(func(s *Service) *Service {
        s.Cache = NewCache(s.Config.Size)
        return s
    }),
)

The decorator only sees types provided within the same module, making this pattern unsuitable for library/module authors who want to ship complete, self-contained functionality.

This is why AutoConstructor is the superior pattern - it produces a complete, initialized object without requiring decorators.


Implementation Details: How Reflection Makes This Work

The core technique uses three reflection primitives to dynamically generate fx-compatible constructors:

go
func AutoConstructor[T any]() interface{} {
    targetType := reflect.TypeOf((*T)(nil)).Elem()
    
    // 1. reflect.StructOf - Build params struct with fx.In + exported fields
    fields := []reflect.StructField{
        {
            Name:      "In",
            Type:      reflect.TypeOf((*fx.In)(nil)).Elem(),
            Anonymous: true,
        },
    }
    
    numFields := targetType.NumField()
    for i := 0; i < numFields; i++ {
        field := targetType.Field(i)
        if !field.IsExported() {
            continue  // Skip unexported fields
        }
        fields = append(fields, field)
    }
    
    paramsType := reflect.StructOf(fields)
    
    // 2. reflect.FuncOf - Create function signature fx expects
    returnType := targetType
    if targetType.Kind() != reflect.Ptr {
        returnType = reflect.TypeOf((*T)(nil))
    }
    
    funcType := reflect.FuncOf(
        []reflect.Type{paramsType},
        []reflect.Type{returnType},
        false,
    )
    
    // 3. reflect.MakeFunc - Implement the function
    fn := reflect.MakeFunc(funcType, func(args []reflect.Value) []reflect.Value {
        paramsVal := args[0]
        
        // Create target instance
        var targetVal reflect.Value
        if targetType.Kind() == reflect.Ptr {
            targetVal = reflect.New(targetType.Elem())
        } else {
            targetVal = reflect.New(targetType).Elem()
        }
        
        // Copy fields from params to target
        for i := 0; i < numFields; i++ {
            field := targetType.Field(i)
            if !field.IsExported() {
                continue
            }
            
            // Find matching field in params (skip fx.In)
            paramsField := paramsVal.FieldByName(field.Name)
            targetField := targetVal
            if targetType.Kind() == reflect.Ptr {
                targetField = targetField.Elem()
            }
            targetField = targetField.FieldByName(field.Name)
            
            targetField.Set(paramsField)
        }
        
        return []reflect.Value{targetVal}
    })
    
    return fn.Interface()
}

Key insight: fx's reflection can inspect dynamically-created types (from reflect.StructOf) as if they were hand-written structs. This lets us generate the params struct at runtime while keeping the target struct clean.

What this generates (conceptually):

go
// AutoConstructor[ServiceBase]() generates equivalent of:
type generatedParams struct {
    fx.In
    Config *Config
    Logger *Logger
    DB     *sql.DB
}

func generatedConstructor(p generatedParams) ServiceBase {
    return ServiceBase{
        Config: p.Config,
        Logger: p.Logger,
        DB:     p.DB,
    }
}

Features that work:

  • ✅ Standard fx struct tags: `name:"..."`, `optional:"true"`, `group:"..."`
  • ✅ Error returns in constructors
  • ✅ fx.Lifecycle, fx.Shutdowner injection
  • ✅ Works in fx.Module
  • ✅ Minimal runtime overhead (reflection only at startup)

Comparison: Current Workarounds

Pattern Field Access Boilerplate Reflection Module-Safe Lifecycle Limitations
Manual copying Direct High None Duplicate definitions, verbose
Named field (.P.) service.P.Config Low None Awkward accessor
AutoConstructor Direct Minimal Startup only Needs external package, debug traces
AutoProvidePartial + Decorate Direct Minimal Startup only Cannot work in fx.Module

All patterns work in production, but they require:

  1. External utility package
  2. Understanding of reflection internals
  3. Accepting `reflect.makeFuncStub` in stack traces

Potential Solutions (Open to fx Team's Design)

Several approaches could make this cleaner natively in fx:

Option A: Allow fx.In in Return Types (with annotation)

go
type Service struct {
    fx.In `fx:"partial"` // or fx.Partial
    Config *Config
    Logger *Logger
    cache  *Cache `fx:"-"`  // not injected
}

func NewService(s Service) *Service {
    s.cache = NewCache(s.Config.Size)
    return &s
}

Option B: Support Anonymous Base Struct Embedding

go
type ServiceBase struct {
    fx.In  // or could be implicit
    Config *Config
    Logger *Logger
}

type Service struct {
    ServiceBase  // fx recognizes and injects this
    cache *Cache
}

func NewService(base ServiceBase) *Service {
    return &Service{
        ServiceBase: base,
        cache:       NewCache(base.Config.Size),
    }
}

Option C: Native fx:"-" Tag Support

go
type ServiceParams struct {
    fx.In
    Config *Config
    Logger *Logger
    Cache  *Cache  \`fx:"-"\`  // user provides this
}

type Service struct {
    ServiceParams
}

func NewService(p ServiceParams) *Service {
    p.Cache = NewCache(p.Config.Size)
    return &Service{ServiceParams: p}
}

Option D: Official Reflection-Based Utility

Adopt the `AutoConstructor` pattern as an official fx utility:

go
import "go.uber.org/fx/fxutil"

fx.Provide(
    fxutil.AutoConstructor[ServiceBase](),
    NewService,
)

Or even simpler, a new marker type:

go
type ServiceBase struct {
    fx.Struct  // or fx.Base, fx.Injectable
    Config *Config
    Logger *Logger
}

// fx recognizes fx.Struct and auto-generates the constructor

Benefits of Native Support

  1. Better debugging - No `reflect.makeFuncStub` in stack traces
  2. Discoverability - Official documentation and examples
  3. Compile-time validation - Could provide better error messages
  4. No external dependencies - Pure fx solution
  5. Consistency - One blessed pattern instead of multiple workarounds
  6. Library/module authors - Can ship complete, self-contained modules

Evidence This Pattern is Needed

From my exploration:

  • 3,000+ lines of production-ready code developed to solve this
  • Multiple patterns explored (7+ variations)
  • Comprehensive tests with lifecycle integration, error handling, edge cases
  • Performance benchmarks showing minimal overhead
  • Real-world usage in migration from wire
  • Complete example module demonstrating library packaging

The AutoConstructor pattern has emerged as clearly superior because:

  • Works in fx.Module (critical for library authors)
  • Most idiomatic Go (normal constructors)
  • No special tags or markers needed
  • Composable across multiple services

Additional Context

  • All struct tag features work on dynamically-created types (`name:"..."`, `optional:"true"`, `group:"..."`)
  • Patterns are composable (multiple constructors can share same base)
  • Works seamlessly with fx.Lifecycle, fx.Shutdowner, fx.Module
  • Reflection overhead is startup-only (during fx.New), zero runtime cost

Happy to provide more details or examples if helpful!


What would you like to see? I'd love feedback on:

  1. Whether native support for this pattern makes sense for fx
  2. Which approach (A/B/C/D above) best fits fx's design philosophy
  3. Whether the reflection-based implementation could be adopted officially

Thank you for considering this enhancement!