Provider Registry
A provider is the piece that knows how to talk to one particular database — there is a GORM provider, a Bun provider, a Redis provider. You set one up at startup, and from then on your code asks for it by name instead of passing a connection through every function.
The registry is where they are kept. Think of it as a labelled shelf: you
put the Postgres provider on it under the name "primary" and the Redis one
under "cache", and anywhere in your application you can take either back
down.
What makes it worth having is that you get the right type back, not a
Provider you have to assert:
gpa.Register[*gpagorm.Provider]("primary", gormDB)
primary := gpa.MustGet[*gpagorm.Provider]("primary") // already the right typeArchitecture
Providers are stored in a two-level map:
providers map[string]map[string]Provider
^ ^
| +-- instance name (e.g., "primary", "cache")
+-- provider type name (e.g., "gorm", "redis")Registration
Register a Provider
import (
"github.com/lemmego/gpa"
"github.com/lemmego/gpagorm"
"github.com/lemmego/gparedis"
)
// Register a GORM provider as "primary"
gpa.Register[*gpagorm.Provider]("primary", gormProvider)
// Register with registry defaults
gpa.RegisterDefault[*gpagorm.Provider](gormProvider)
// Register a Redis provider as "cache"
gpa.Register[*gparedis.Provider]("cache", redisClient)From an Application Provider
type DatabaseProvider struct{}
func (p *DatabaseProvider) Provide(a app.App) error {
cfg := a.Config()
provider, err := gpagorm.NewProvider(gpa.Config{
Host: cfg.String("database.host"),
Port: cfg.Int("database.port"),
Database: cfg.String("database.name"),
})
if err != nil {
return err
}
gpa.Register[*gpagorm.Provider]("default", provider)
return nil
}Retrieval
Type-Safe Retrieval
// With compile-time type inference
primary := gpa.MustGet[*gpagorm.Provider]("primary")
cache := gpa.MustGet[*gparedis.Provider]("cache")
// With error handling
provider, err := gpa.Get[*gpagorm.Provider]("primary")
if err != nil {
// handle error
}Default Instance
If no instance name is provided, the registry looks up "default":
provider := gpa.MustGet[*gpagorm.Provider]()Multiple Instances
// Get all instances of a type
gormProviders, err := gpa.GetByType[*gpagorm.Provider]()Lifecycle Management
Registration and lookup are package-level generic functions, but the
lifecycle operations are methods on the registry, reached through
gpa.Registry():
r := gpa.Registry()
// List registered types
types := r.ListTypes()
// List instances for a type
instances, err := r.ListInstances("gorm")
// Remove and close a provider
err := r.Remove("gorm", "primary")
// Remove and close all providers
err := r.RemoveAll()
// Health check
health := r.HealthCheck()
// Returns map[string]map[string]error — [type][instance]errorListInstances, Remove and RemoveAll all report an error; only
ListTypes and HealthCheck cannot fail.
Type Inference
The registry maps Go reflect.Type to provider type names. When Register[T] is called:
- The provider’s
ProviderInfo().Nameis used as the type key reflect.TypeOf(provider)is mapped to this name- On
Get[T](), the type is looked up in reverse to find the correct provider
This means resolution is fully type-safe at compile time — no string matching required.
Provider Interface
All providers implement:
type Provider interface {
Configure(config Config) error
Health() error
Close() error
SupportedFeatures() []Feature
ProviderInfo() ProviderInfo
}And optionally one of:
SQLProvider— for SQL databases (GORM, Bun)KeyValueProvider— for key-value stores (Redis)DocumentProvider— for document databases (MongoDB)GraphProvider— for graph databasesWideColumnProvider— for wide-column stores
Type Assertion Helpers
if sqlProvider, ok := gpa.AsSQLProvider(provider); ok {
db := sqlProvider.DB()
}
if gpa.IsSQLProvider(provider) {
// provider is a SQL provider
}