Vormia Go Core
Foundation layer for the Vormia ecosystem — dependency injection, config, logging, events, lifecycle, context, and errors.
Status: pre-release v0.0.1 (versioned by git tags; not a stable 1.x API yet).
Module: github.com/vormialabs/vormia-go-core · Go: 1.21+ · Deps: standard library only.
Deep dive: aiguide/GUIDE.md · AI / contributor rules: aiguide/CURSOR_CODEX_MCP_GUIDE.md
What it provides
| Package |
Purpose |
container |
DI: singleton + transient services, ServiceProvider |
config |
Key/value store from .env files and OS environment |
logger |
Leveled logging (stdout, writer, or file) |
events |
Sync + async publish/subscribe |
lifecycle |
App with ordered boot/shutdown hooks and signal handling |
context |
Typed context keys + mutable request ValueBag |
errors |
Wrap / Exception / pluggable Handler |
No HTTP, database, or cache here — those belong in higher Vormia layers.
Installation
go get github.com/vormialabs/vormia-go-core@v0.0.1
Requires the tag to be published on the remote. Check available versions:
git ls-remote --tags https://github.com/vormialabs/vormia-go-core.git
go list -m -versions github.com/vormialabs/vormia-go-core
Quick start
Application shell (recommended)
Thin main: create App, load config, register hooks, boot, wait for signal.
package main
import (
"os"
"github.com/vormialabs/vormia-go-core/lifecycle"
"github.com/vormialabs/vormia-go-core/logger"
)
func main() {
app := lifecycle.NewApp()
// File first, then OS env (OS wins).
_ = app.Config().Load(".env") // missing file is OK
app.Config().LoadEnv()
app.Logger().SetLevel(logger.ParseLevel(
app.Config().GetString("LOG_LEVEL", "info"),
))
app.OnBoot(func() error {
return app.Config().Require("APP_NAME")
})
app.OnShutdown(func() error {
// close pools, flush loggers, etc.
return nil
})
if err := app.Boot(); err != nil {
app.Logger().Error("boot failed", err, nil)
os.Exit(1)
}
// Blocks on SIGINT/SIGTERM, then runs Shutdown in reverse order.
_ = app.WaitForSignal()
}
Dependency injection
c := container.New()
// Singleton — factory runs once.
_ = c.Register("userRepo", func() interface{} {
return &UserRepository{}
})
// Transient — fresh instance every Resolve.
_ = c.RegisterTransient("requestID", func() interface{} {
return newID()
})
repo, err := c.Resolve("userRepo")
if err != nil {
// match *container.ErrNotFound / *container.ErrCyclicDependency with errors.As
panic(err)
}
userRepo := repo.(*UserRepository)
Wire modules with ServiceProvider (Register then Boot via RegisterProviders).
Configuration
cfg := config.New()
_ = cfg.Load(".env")
cfg.LoadEnv()
port := cfg.GetInt("PORT", 8080)
debug := cfg.GetBool("APP_DEBUG", false)
name := cfg.GetString("APP_NAME", "vormia")
err := cfg.Require("DB_HOST", "DB_USER") // one error listing all missing keys
Typed getters never fail — they return the default when missing or unparsable.
Logging
log := logger.NewStdoutLogger()
log.SetLevel(logger.LevelDebug)
log.Info("started", map[string]interface{}{"version": "0.0.1"})
log.Error("failed", err, map[string]interface{}{"user_id": 123})
// Production: append to file and mirror to stdout.
fileLog, _ := logger.NewFileLogger("logs/app.log")
defer fileLog.Close()
Depend on the logger.Logger interface, not a concrete type. Also available: NewWriterLogger(w) for tests.
Events
d := events.NewDispatcher()
d.OnPanic(func(event string, recovered interface{}) {
// required if you use DispatchAsync — otherwise panics are silent
})
d.Listen("user.created", func(payload interface{}) {
// sync handlers run in registration order
})
d.Dispatch("user.created", user)
w := d.DispatchAsync("user.created", user)
w.Wait() // optional
Context values
import vctx "github.com/vormialabs/vormia-go-core/context"
const UserIDKey = vctx.Key("user_id")
ctx := vctx.WithValue(vctx.Background(), UserIDKey, "u-42")
bag := vctx.NewValueBag()
ctx = vctx.WithBag(ctx, bag)
if b := vctx.BagFrom(ctx); b != nil {
b.Set("trace_id", "abc")
}
Errors
err := errors.Wrap(cause, "loading user")
ex := errors.NewException("payment failed", "PAY_DECLINED").
WithCause(gatewayErr).
With("order_id", 1234)
handler := errors.NewLoggingHandler(app.Logger())
handler.Handle(ex)
Core contracts
type ServiceContainer interface {
Register(name string, factory func() interface{}) error
Resolve(name string) (interface{}, error)
Get(name string, defaultValue interface{}) interface{}
}
type Logger interface {
Debug(message string, context map[string]interface{})
Info(message string, context map[string]interface{})
Warn(message string, context map[string]interface{})
Error(message string, err error, context map[string]interface{})
SetLevel(level Level)
}
type EventDispatcher interface {
Listen(event string, callback func(payload interface{}))
Dispatch(event string, payload interface{})
}
Also notable:
- container:
RegisterTransient, Has, ServiceProvider, RegisterProviders
- config:
Load / LoadEnv, typed getters, Require, All, Has
- logger:
ParseLevel, NewFileLogger, NewWriterLogger
- events:
OnPanic, ListenTo, DispatchAsync, Forget, HasListeners
- lifecycle:
OnBoot / OnShutdown (+ priority variants), WaitForSignal
Design notes
- Thread-safe by default — container, config, dispatcher, loggers, value bag, hooks.
- Fail fast on boot — first boot-hook error aborts; shutdown runs every hook and joins errors.
- Config load order —
Load(".env") then LoadEnv() so OS wins.
- No magic — string-keyed services and explicit factories (no reflection autowiring).
Package layout
vormia-go-core/
├── go.mod
├── README.md
├── aiguide/ # GUIDE.md + AI contributor rules
├── container/
├── config/
├── logger/
├── events/
├── lifecycle/
├── context/
└── errors/
Development
go build ./...
go vet ./...
gofmt -l . # must print nothing
go test ./...
go test -race -count=2 ./...
License
See LICENSE.