Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ name: CI

on:
push:
branches: [ main, development ]
branches: [main, development]
pull_request:
branches: [ main, development ]
branches: [main, development]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ '1.25' ]
go-version: ["1.26"]

steps:
- uses: actions/checkout@v4
Expand All @@ -32,7 +32,7 @@ jobs:

- name: Upload coverage
uses: codecov/codecov-action@v4
if: matrix.go-version == '1.25'
if: matrix.go-version == '1.26'
with:
files: coverage.out
fail_ci_if_error: false
Expand All @@ -45,7 +45,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
go-version: "1.26"

- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
Expand Down
87 changes: 62 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
# Needle

A modern, type-safe dependency injection framework for Go 1.25+.
A modern, type-safe dependency injection framework for Go.

[![Go Reference](https://pkg.go.dev/badge/github.com/danpasecinic/needle.svg)](https://pkg.go.dev/github.com/danpasecinic/needle)
[![Go Report Card](https://goreportcard.com/badge/github.com/danpasecinic/needle)](https://goreportcard.com/report/github.com/danpasecinic/needle)

## Features

- **Type-safe generics** - Compile-time type checking with `Provide[T]` and `Invoke[T]`
- **Auto-wiring** - Constructor injection and struct tag injection
- **Hot reload** - Replace services at runtime without restart
- **Zero dependencies** - Only Go standard library
- **Cycle detection** - Automatically detects circular dependencies
- **Multiple scopes** - Singleton, Transient, Request, Pooled
- **Lifecycle management** - OnStart/OnStop hooks with ordering
- **Lazy providers** - Defer instantiation until first use
- **Parallel startup** - Start independent services concurrently
- **Modules** - Group related providers
- **Interface binding** - Bind interfaces to implementations
- **Decorators** - Wrap services with cross-cutting concerns
- **Health checks** - Liveness and readiness probes
- **Optional dependencies** - Type-safe optional resolution
Needle uses Go generics for compile-time type safety (`Provide[T]`, `Invoke[T]`) and has zero external dependencies.

It supports constructor auto-wiring, struct tag injection, multiple scopes (singleton, transient, request, pooled), and lifecycle hooks that run in dependency order. Services can start in parallel, be lazily initialized, or be replaced at runtime without restarting the container.

You can group providers into modules, bind interfaces to implementations, wrap services with decorators, and resolve optional dependencies with a built-in `Optional[T]` type. Health and readiness checks are supported out of the box.

## Installation

Expand Down Expand Up @@ -56,6 +47,52 @@ See the [examples](examples/) directory:
- [optional](examples/optional/) - Optional dependencies with fallbacks
- [parallel](examples/parallel/) - Parallel startup/shutdown

## Choosing a Scope

| Scope | Lifetime | Use When |
|-------|----------|----------|
| **Singleton** (default) | One instance for the container lifetime | Stateful services: DB pools, config, caches, loggers |
| **Transient** | New instance every resolution | Stateless handlers, commands, lightweight value objects |
| **Request** | One instance per `WithRequestScope(ctx)` | Per-HTTP-request state: request loggers, auth context, transaction managers |
| **Pooled** | Reusable instances from a fixed-size pool | Expensive-to-create, stateless-between-uses resources: gRPC connections, worker objects |

```go
needle.Provide(c, NewService) // Singleton (default)
needle.Provide(c, NewHandler, needle.WithScope(needle.Transient))
needle.Provide(c, NewRequestLogger, needle.WithScope(needle.Request))
needle.Provide(c, NewWorker, needle.WithPoolSize(10)) // Pooled with 10 slots
```

Pooled services must be released by the caller via `c.Release(key, instance)`. If the pool is full, the instance is dropped and a warning is logged.

## Replacing Services

Replace services at runtime without restarting the container. Useful for feature flags, A/B testing, test doubles, or configuration updates.

```go
// Replace with a new value
needle.ReplaceValue(c, &Config{Port: 9090})

// Replace with a new provider
needle.Replace(c, func(ctx context.Context, r needle.Resolver) (*Server, error) {
return &Server{Config: needle.MustInvoke[*Config](c)}, nil
})

// Replace with auto-wired constructor
needle.ReplaceFunc[*Service](c, NewService)

// Replace with struct injection
needle.ReplaceStruct[*Service](c)

// Named variants
needle.ReplaceNamedValue(c, "primary", &Config{Port: 5432})
needle.ReplaceNamed(c, "primary", provider)
```

All Replace functions accept the same options as Provide (`WithScope`, `WithOnStart`, `WithOnStop`, `WithLazy`, `WithPoolSize`). If the service does not exist yet, Replace creates it. If it does exist, the old entry is removed from both the registry and the dependency graph before re-registering.

`Must` variants (`MustReplace`, `MustReplaceValue`, `MustReplaceFunc`, `MustReplaceStruct`) panic on error.

## Benchmarks

Needle wins benchmark categories against uber/fx, samber/do, and uber/dig.
Expand All @@ -64,21 +101,21 @@ Needle wins benchmark categories against uber/fx, samber/do, and uber/dig.

| Framework | Simple | Chain | Memory (Chain) |
|------------|--------|-------|----------------|
| **Needle** | 780ns | 1.6μs | 3KB |
| Do | 1.9μs | 5.0μs | 4KB |
| Dig | 13μs | 28μs | 28KB |
| Fx | 42μs | 85μs | 70KB |
| **Needle** | 698ns | 1.5μs | 3KB |
| Do | 1.8μs | 4.4μs | 4KB |
| Dig | 13μs | 26μs | 28KB |
| Fx | 39μs | 78μs | 70KB |

Needle is **50x faster** than Fx for provider registration.
Needle is **56x faster** than Fx for provider registration.

### Service Resolution

| Framework | Singleton | Chain |
|------------|-----------|-------|
| Fx | 0ns* | 0ns* |
| **Needle** | 17ns | 16ns |
| Do | 152ns | 159ns |
| Dig | 591ns | 586ns |
| **Needle** | 15ns | 17ns |
| Do | 150ns | 161ns |
| Dig | 614ns | 622ns |

*Fx resolves at startup, not on-demand.

Expand All @@ -88,8 +125,8 @@ When services have initialization work (database connections, HTTP clients, etc.

| Scenario | Sequential | Parallel | Speedup |
|-------------------|------------|----------|---------|
| 10 services × 1ms | 23ms | 2.4ms | **10x** |
| 50 services × 1ms | 116ms | 2.5ms | **45x** |
| 10 services × 1ms | 23ms | 2.3ms | **10x** |
| 50 services × 1ms | 113ms | 2.6ms | **44x** |

Run benchmarks: `cd benchmark && make run`

Expand Down
46 changes: 30 additions & 16 deletions autowire.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func InvokeStructCtx[T any](ctx context.Context, c *Container) (T, error) {
var zero T

t := reflectPkg.TypeOf(zero)
isPtr := t.Kind() == reflectPkg.Ptr
isPtr := t.Kind() == reflectPkg.Pointer
if isPtr {
t = t.Elem()
}
Expand Down Expand Up @@ -82,25 +82,25 @@ func InvokeStructCtx[T any](ctx context.Context, c *Container) (T, error) {
return structVal.Interface().(T), nil
}

func ProvideFunc[T any](c *Container, constructor any, opts ...ProviderOption) error {
func buildFuncProvider[T any](c *Container, constructor any) (Provider[T], []ProviderOption, error) {
params, returnType, err := reflect.FuncParams(constructor)
if err != nil {
return err
return nil, nil, err
}

if returnType == nil {
return fmt.Errorf("constructor must return at least one value")
return nil, nil, fmt.Errorf("constructor must return at least one value")
}

expectedType := reflectPkg.TypeOf((*T)(nil)).Elem()
if !returnType.AssignableTo(expectedType) {
return fmt.Errorf("constructor returns %s, expected %s", returnType, expectedType)
return nil, nil, fmt.Errorf("constructor returns %s, expected %s", returnType, expectedType)
}

fnVal := reflectPkg.ValueOf(constructor)
fnType := fnVal.Type()

hasError := fnType.NumOut() == 2 && fnType.Out(1).Implements(reflectPkg.TypeOf((*error)(nil)).Elem())
hasError := fnType.NumOut() == 2 && fnType.Out(1).Implements(reflectPkg.TypeFor[error]())

deps := make([]string, len(params))
for i, p := range params {
Expand Down Expand Up @@ -128,17 +128,10 @@ func ProvideFunc[T any](c *Container, constructor any, opts ...ProviderOption) e
return results[0].Interface().(T), nil
}

opts = append([]ProviderOption{WithDependencies(deps...)}, opts...)
return Provide(c, provider, opts...)
}

func MustProvideFunc[T any](c *Container, constructor any, opts ...ProviderOption) {
if err := ProvideFunc[T](c, constructor, opts...); err != nil {
panic(err)
}
return provider, []ProviderOption{WithDependencies(deps...)}, nil
}

func ProvideStruct[T any](c *Container, opts ...ProviderOption) error {
func buildStructProvider[T any](c *Container) (Provider[T], []ProviderOption) {
provider := func(ctx context.Context, r Resolver) (T, error) {
return InvokeStructCtx[T](ctx, c)
}
Expand All @@ -155,7 +148,28 @@ func ProvideStruct[T any](c *Container, opts ...ProviderOption) error {
}
}

opts = append([]ProviderOption{WithDependencies(deps...)}, opts...)
return provider, []ProviderOption{WithDependencies(deps...)}
}

func ProvideFunc[T any](c *Container, constructor any, opts ...ProviderOption) error {
provider, depOpts, err := buildFuncProvider[T](c, constructor)
if err != nil {
return err
}

opts = append(depOpts, opts...)
return Provide(c, provider, opts...)
}

func MustProvideFunc[T any](c *Container, constructor any, opts ...ProviderOption) {
if err := ProvideFunc[T](c, constructor, opts...); err != nil {
panic(err)
}
}

func ProvideStruct[T any](c *Container, opts ...ProviderOption) error {
provider, depOpts := buildStructProvider[T](c)
opts = append(depOpts, opts...)
return Provide(c, provider, opts...)
}

Expand Down
2 changes: 1 addition & 1 deletion benchmark/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module benchmark

go 1.25.4
go 1.26.0

replace github.com/danpasecinic/needle => ../

Expand Down
Loading
Loading