ore
CategoryMiscellaneous
SubcategoryDependency Injection
Stars28
Lightweight, generic & simple dependency injection (DI) container
About ore
This large README is shown as a plain-text preview. Read the full README at the source
# Ore — Dependency Injection for Go
[](https://pkg.go.dev/github.com/firasdarwish/ore)
[](https://goreportcard.com/report/github.com/firasdarwish/ore)
[](https://github.com/avelino/awesome-go)
[](https://codecov.io/gh/firasdarwish/ore)
**Ore** is a lightweight, type-safe dependency injection (DI) container for Go. Inspired by ASP.NET's DI model, it gives you clean lifetime management, lazy initialization, runtime value injection, and modular containers — without magic or reflection soup.
---
## Table of Contents
1. [Why Ore?](#why-ore)
2. [Installation](#installation)
3. [Core Concepts](#core-concepts)
4. [Lifetimes](#lifetimes)
- [Singleton](#singleton)
- [Scoped](#scoped)
- [Transient](#transient)
5. [Registering Services](#registering-services)
- [Eager Singleton](#eager-singleton)
- [Anonymous Functions](#anonymous-functions-registerfunc)
- [Creator\[T\] Interface](#creatort-interface-registercreator)
6. [Resolving Services](#resolving-services)
- [Get](#get)
- [GetList](#getlist)
7. [Keyed Services](#keyed-services)
8. [Aliases](#aliases)
9. [Placeholder Services](#placeholder-services)
10. [Isolated Containers](#isolated-containers)
11. [Validation](#validation)
12. [Graceful Termination](#graceful-termination)
13. [Recommended Startup Pattern](#recommended-startup-pattern)
14. [Real-World Usage Patterns](#real-world-usage-patterns)
15. [API Reference](#api-reference)
---
## Why Ore?
Go encourages simplicity, and many projects wire dependencies by hand. That works — until it doesn't. As applications grow, manual wiring becomes error-prone, lifetime bugs sneak in, and test setup becomes tedious.
Ore solves this without going overboard:
- **Type-safe** — powered by Go generics, no `interface{}` casting
- **Lifetime-aware** — Singleton, Scoped, and Transient, correctly enforced
- **Context-native** — scoped instances live and die with `context.Context`
- **Validates your graph** — catches circular dependencies, missing registrations, and lifetime mismatches at startup
- **Modular** — isolated containers per module for clean monolith architecture
- **Non-invasive** — your structs don't need to embed anything from Ore
---
## Installation
```bash
go get -u github.com/firasdarwish/ore
```
```go
import "github.com/firasdarwish/ore"
```
---
## Core Concepts
Ore works in two phases:
**1. Registration** — at startup, you tell Ore how to build each service and what lifetime it should have.
**2. Resolution** — at runtime, you ask Ore for a service. It constructs it (or returns a cached instance, depending on lifetime), injects its dependencies, and returns it to you.
The key insight: **you never call `new(MyService)` scattered throughout your app**. Ore centralizes construction, so lifetimes are predictable and dependencies are explicit.
---
## Lifetimes
Every service registered with Ore has a **lifetime** that controls when instances are created and how long they live.
### Singleton
A singleton is created **once** and reused for the entire application lifetime. Use this for stateless services, configuration, database connection pools, loggers, and anything that's safe to share globally.
```
App starts → instance created → reused forever → app shuts down
```
There are two flavors:
**Eager singleton** — created immediately at registration time. Best for critical services where you want startup failures to surface early.
**Lazy singleton** — created the first time it's resolved. Best for services that may never be used, or that are expensive to initialize.
### Scoped
A scoped service is created **once per context** (`context.Context`). Every call to `Get` with the same context returns the same instance. A new context gets a fresh instance.
```
Request A context → instance A (shared within request A)
Request B context → instance B (shared within request B)
```
This is ideal for HTTP request handlers, database transactions, and anything that should be consistent within a single unit of work but isolated from other units.
### Transient
A transient service is created **fresh on every resolution**. No caching, no sharing.
```
Get() → new instance
Get() → another new instance
```
Use transients for lightweight, stateful objects where sharing would cause bugs.
---
## Registering Services
### Eager Singleton
Pass an already-constructed instance directly. Ore stores it and returns it on every `Get`.
```go
type Logger interface {
Log(msg string)
}
type zapLogger struct{}
func (z *zapLogger) Log(msg string) { /* ... */ }
// Registered immediately — no construction function needed
ore.RegisterSingleton[Logger](&zapLogger{})
```
### Anonymous Functions (`RegisterFunc`)
Pass a constructor function. Ore calls it when the service is first needed (or on every `Get` for transients). The function receives a `context.Context` and returns the service plus the (potentially updated) context.
```go
ore.RegisterFunc[Logger](ore.Singleton, func(ctx context.Context) (Logger, context.Context) {
return &zapLogger{}, ctx
})
```
**Injecting dependencies inside a constructor:**
When one service depends on another, call `ore.Get` inside the constructor and pass the context through. This is what threads scoped state correctly across a dependency chain.
```go
type UserService interface {
GetUser(id string) (*User, error)
}
type userServiceImpl struct {
db DB
}
ore.RegisterFunc[UserService](ore.Scoped, func(ctx context.Context) (UserService, context.Context) {
// Resolve DB — passes ctx through so scoped instances are shared
db, ctx := ore.Get[DB](ctx)
return &userServiceImpl{db: db}, ctx
})
```
> **Always pass `ctx` through the dependency chain.** Scoped services store their instances in the context. If you don't thread the returned `ctx` forward, scoped dependencies won't be shared correctly within the same scope.
### `Creator[T]` Interface (`RegisterCreator`)
An alternative to anonymous functions. Implement the `Creator[T]` interface on your struct, and Ore calls `New` to construct it.
```go
type Creator[T any] interface {
New(ctx context.Context) (T, context.Context)
}
```
This is useful when you want the struct itself to own its construction logic, keeping it colocated with the type definition.
```go
type simpleCounter struct {
count int
}
// Implementing Creator[Counter]
func (c *simpleCounter) New(ctx context.Context) (Counter, context.Context) {
return &simpleCounter{count: 0}, ctx
}
func (c *simpleCounter) AddOne() { c.count++ }
func (c *simpleCounter) GetCount() int { return c.count }
// Register using RegisterCreator
ore.RegisterCreator[Counter](ore.Scoped, &simpleCounter{})
```
**Anonymous func vs Creator[T] — when to use which:**
| | `RegisterFunc` | `RegisterCreator` |
|---|---|---|
| Constructor location | Inline at registration site | On the struct itself |
| Good for | External types, simple wiring | Types that own their construction |
| Coupling to Ore | None | Struct knows about `context.Context` |
| Verbosity | Slightly more boilerplate | Cleaner registration call |
---
## Resolving Services
### `Get`
Resolves a single service. Returns the service and an updated context (important for scoped services).
```go
ctx := context.Background()
logger, ctx := ore.Get[Logger](ctx)
logger.Log("hello")
```
Always use the returned `ctx` for subsequent resolutions if any services in the chain are scoped.
### `GetList`
Resolves **all registered implementations** of a type. Returns a slice.
```go
// If you registered three different Counter implementations:
counters, ctx := ore.GetList[Counter](ctx)
for _, c := range counters {
fmt.Println(c.GetCount())
}
```
`GetList` is useful for plugin-style architectures or when you genuinely need all implementations (e.g., firing all event handlers, running all validators).
`GetList` never panics if nothing is registered — it returns an empty slice.
---
## Keyed Services
Keys let you register **multiple implementations of the same type** and select among them by name at resolution time.
```go
type Greeter interface {
Greet() string
}
type FriendlyGreeter struct{}
func (g *FriendlyGreeter) Greet() string { return "Hey! Great to see you!" }
type FormalGreeter struct{}
func (g *FormalGreeter) Greet() string { return "Good day. How may I assist you?" }
// Register with keys
ore.RegisterKeyedFunc[Greeter](ore.Scoped, func(ctx context.Context) (Greeter, context.Context) {
return &FriendlyGreeter{}, ctx
}, "friendly")
ore.RegisterKeyedFunc[Greeter](ore.Transient, func(ctx context.Context) (Greeter, context.Context) {
return &FormalGreeter{}, ctx
}, "formal")
// Resolve by key
friendly, ctx := ore.GetKeyed[Greeter](ctx, "friendly")
fmt.Println(friendly.Greet()) // Hey! Great to see you!
formal, ctx := ore.GetKeyed[Greeter](ctx, "formal")
fmt.Println(formal.Greet()) // Good day. How may I assist you?
```
**Get all implementations under a key:**
```go
greeters, ctx := ore.GetKeyedList[Greeter](ctx, "friendly")
```
**Common use cases for keyed services:**
- Multiple payment providers (`"stripe"`, `"paypal"`)
- Multiple notification channels (`"email"`, `"sms"`, `"push"`)
- Module-specific service overrides
- Feature flags driving different implementations at runtime
---
## Aliases
Aliases let you link a **concrete type** (struct pointer) to an **interface**, without registering the interface directly. This is powerful when you want to resolve by a broad interface but your implementations are registered under their concrete types.
```go
type IPerson interface {
GetName() string
}
type Broker struct{ Name string }
func (b *Broker) GetName() string { return b.Name }
type Trader struct{ Name string }
func (t *Trader) GetName() string { return t.Name }
// Register concrete types
ore.RegisterFunc[*Broker](ore.Scoped, func(ctx context.Context) (*Broker, context.Context) {
return &Broker{Name: "Alice"}, ctx
})
ore.RegisterFunc[*Trader](ore.Scoped, func(ctx context.Context) (*Trader, context.Context) {
return &Trader{Name: "Bob"}, ctx
})
// Link them to IPerson
ore.RegisterAlias[IPerson, *Broker]()
ore.RegisterAlias[IPerson, *Trader]() // last-linked takes precedence for Get
// Resolve — returns Trader (last linked)
person, ctx := ore.Get[IPerson](ctx)
fmt.Println(person.GetName()) // Bob
// Resolve all — returns both
people, ctx := ore.GetList[IPerson](ctx)
fmt.Println(len(people)) // 2
```
**Precedence rules:**
- `Get` returns the **most recently linked** alias, unless a direct resolver for the interface exists — then the direct resolver always wins.
- `GetList` returns **all** linked implementations plus any direct resolvers.
- Alias-of-alias is not supported and will panic.
- Ore validates at registration that the concrete type actually implements the interface.
---
## Placeholder Services
Placeholders let you declare a dependency that **doesn't exist at registration time** but will be provided at runtime. This is the right pattern for request-scoped values like authenticated users, tenant IDs, or feature flag evaluations.
### How it works
1. **Declare** a placeholder at startup — this tells Ore "something of this type will be provided later."
2. **Provide** the value at request time by injecting it into the context.
3. **Resolve** it normally — any service that depends on the placeholder resolves correctly once the value is set.
```go
// 1. At startup: declare that a *User will be injected per-request
ore.RegisterPlaceholder[*User]()
// 2. Register a service that depends on *User
ore.RegisterFunc[UserDashboard](ore.Scoped, func(ctx context.Context) (UserDashboard, context.Context) {
user, ctx := ore.Get[*User](ctx) // depends on the placeholder
return &userDashboardImpl{user: user}, ctx
})
// 3. At request time: inject the actual user
func handleRequest(w http.ResponseWriter, r *http.Request) {
user := getUserFromJWT(r)
ctx := ore.ProvideScopedValue[*User](r.Context(), user)
dashboard, ctx := ore.Get[UserDashboard](ctx)
// dashboard.user is set correctly
}
```
**What happens if you forget to provide the value?** Ore panics when trying to resolve a service that depends on an unfulfilled placeholder. This is intentional — a silent nil would be far worse.
**Keyed placeholders** follow the same pattern using `RegisterKeyedPlaceholder` and `ProvideKeyedScopedValue`.
**Placeholders and real resolvers coexist:**
If you later register a real resolver for the same type+key, it takes precedence over the placeholder for `Get`. Both appear in `GetList`. This lets you gradually migrate from runtime injection to proper construction logic.
---
## Isolated Containers
By default, all registrations go into Ore's **default container**. For larger applications, you can create **isolated containers** — each with its own independent dependency graph.
This is the foundation for **modular monolith architecture**: each module owns its container, preventing accidental cross-module coupling.
```go
// Create isolated containers per module
brokerContainer := ore.NewContainer()
traderContainer := ore.NewContainer()
// Register into specific containers
ore.RegisterFuncToContainer(brokerContainer, ore.Singleton, func(ctx context.Context) (BrokerService, context.Context) {
return &brokerServiceImpl{}, ctx
})
ore.RegisterFuncToContainer(traderContainer, ore.Scoped, func(ctx context.Context) (TraderService, context.Context) {
return &traderServiceImpl{}, ctx
})
// Resolve from specific containers
broker, ctx := ore.GetFromContainer[BrokerService](brokerContainer, ctx)
trader, ctx := ore.GetFromContainer[TraderService](traderContainer, ctx)
```
Services registered in one container are completely invisible to another. There is no accidental leakage between modules.
**Using containers in tests:**
Isolated containers are excellent for unit tests. Each test gets its own clean container with mocked dependencies — no shared global state, no test order dependencies.
```go
func TestUserService(t *testing.T) {
c := ore.NewContainer()
ore.RegisterSingletonToContainer[DB](c, &mockDB{})
ore.RegisterFuncToContainer(c, ore.Scoped, func(ctx context.Context) (UserService, context.Context) {
db, ctx := ore.GetFromContainer[DB](c, ctx)
return &userServiceImpl{db: db}, ctx
})
svc, _ := ore.GetFromContainer[UserService](c, context.Background())
// test svc with mockDB
}
```
All container-scoped registration functions follow the naming convention `XxxToContainer` (e.g., `RegisterFuncToContainer`, `RegisterSingletonToContainer`, `RegisterPlaceholderToContainer`).
---
## Validation
Ore validates your dependency graph to catch bugs at startup rather than in production.
### What Ore validates
**Missing dependencies** — a service depends on a type that was never registered.
**Circular dependencies** — Service A depends on B, B depends on A. Would cause infinite recursion.
**Lifetime misalignment** — a long-lived service depends on a shorter-lived one. For example, a Singleton depending on a Scoped service is a bug: the Singleton gets created once, captures a scoped instance, and that instance outlives its intended scope. Ore catches this.
### How to use validation
By default, Ore validates on every `Get` call. For production, this per-call overhead can be eliminated by disabling it after a one-time startup check:
```go
func main() {
// Register all services...
ore.RegisterSingleton[DB](&dbImpl{})
ore.RegisterFunc[UserService](ore.Scoped, NewUserService)
// ...
// Lock the container — no new registrations allowed after this
ore.Seal()
// Validate the full graph once at startup
// This resolves everything, checks all dependencies, then clears instances
ore.Validate()
// Disable per-call validation in production for best performance
ore.DisableValidation = true
// Start your server...
}
```
`ore.Seal()` causes Ore to panic if any code tries to register a new service after the fact — useful for preventing accidental late registrations in large codebases.
`ore.Validate()` tries to resolve all registered services (including their full dependency chains), verifies correctness, then clears the instances so the app starts fresh.
> **Constructor purity matters.** Since `Validate()` actually runs your constructors, they should be deterministic and side-effect-free. Don't make network calls, open files, or start goroutines inside constructors.
---
## Graceful Termination
When your application shuts down, resources held by Singleton services (DB connections, open files, background workers) need to be cleaned up. Ore helps coordinate this without prescribing a specific interface.
### Application shutdown (Singletons)
Define whatever cleanup interface fits your app:
```go
type Shutdowner interface {
Shutdown() error
}
type Closer interface {
Close() error
}
```
At shutdown, ask Ore for all resolved Singletons that implement your interface. Ore returns them in **reverse resolution order** — dependencies are cleaned up before their dependents.
```go
// In your shutdown handler
shutdownables := ore.GetResolvedSingletons[Shutdowner]()
for _, s := range shutdownables {
if err := s.Shutdown(); err != nil {
log.Printf("shutdown error: %v", err)
}
}
```
Only Singletons that were **actually resolved** during the app's lifetime are returned. Lazily registered singletons that were never used are excluded.
### Request/context shutdown (Scoped)
For scoped services, cleanup happens when the context ends:
```go
type Disposer interface {
Dispose(ctx context.Context) error
}
// In your request handler or middleware
ctx, cancel := context.WithCancel(r.Context())
defer func() {
cancel()
// Clean up scoped instances that implemented Disposer
disposables := ore.GetResolvedScopedInstances[Disposer](ctx)
for _, d := range disposables {
_ = d.Dispose(ctx)
}
}()
// Handle request using ctx...
```
Full example showing both patterns together:
```go
func main() {
ore.RegisterSingleton[*GlobalRepo](&GlobalRepo{})
ore.RegisterCreator(ore.Scoped, &ScopedRepo{})
ore.Seal()
ore.Validate()
// Simulate a request
ctx, cancel := context.WithCancel(context.Background())
_, ctx = ore.Get[*ScopedRepo](ctx)
// End request — clean up scoped resources
cancel()
disposables := ore.GetResolvedScopedInstances[Disposer](ctx)
for _, d := range disposables {
_ = d.Dispose(ctx)
}
// Shut down app — clean up singletons
shutdownables := ore.GetResolvedSingletons[Shutdowner]()
for _, s := range shutdownables {
_ = s.Shutdown()
}
}
```
---
## Recommended Startup Pattern
Here is the battle-tested pattern for setting up Ore in a production Go application:
```go
package main
import (
"context"
"github.com/firasdarwish/ore"
)
func registerServices() {
// Eager singletons for critical infrastructure
ore.RegisterSingleton[Config](loadConfig())
// Lazy singletons for app-wide services
ore.RegisterFunc[DB](ore.Singleton, func(ctx context.Context) (DB, context.Context) {
cfg, ctx := ore.Get[Config](ctx)
return connectDB(cfg.DatabaseURL), ctx
})
// Scoped services for per-request work
ore.RegisterFunc[UserRepository](ore.Scoped, func(ctx context.Context) (UserRepository, context.Context) {
db, ctx := ore.Get[DB](ctx)
return &userRepo{db: db}, ctx
})
ore.RegisterFunc[UserService](ore.Scoped, func(ctx context.Context) (UserService, context.Context) {Frequently Asked Questions
What is ore?
ore is a Miscellaneous library for the Go programming language. Lightweight, generic & simple dependency injection (DI) container
How do I install ore?
Install ore with the Go module system using `go get firasdarwish/ore`. Check the repository for the current installation instructions.
What category does ore belong to?
ore is listed under Miscellaneous, specifically Dependency Injection.