Contracts

> Minimal, stdlib-only interfaces that break circular dependencies between Velocity subsystems.

The contract package defines the narrow interfaces that let Velocity packages depend on each other’s behavior without importing each other directly. It is a leaf package: it declares interfaces (plus a small set of stdlib-only environment helpers) and carries no implementations of its own. Every signature uses only stdlib types, so the contract leaf never pulls a heavier package into your import graph.

Import path: github.com/velocitykode/velocity/contract

Cross-cutting interfaces

Two contracts are implemented across many subsystems rather than by a single type. Bootstrap wiring threads them through every subsystem that implements them.

// EventDispatcherAware is implemented by every subsystem that emits
// framework events (cache, queue, scheduler, router, ORM, auth, view,
// mail, crypto). Bootstrap calls SetEventDispatcher to thread a single
// dispatcher through them all.
type EventDispatcherAware interface {
    SetEventDispatcher(fn func(ctx context.Context, event any) error)
}

// ShutdownAware is implemented by types that hold background resources
// (goroutines, connections, file handles). The provider registry and
// App.Shutdown call Shutdown in reverse registration order. Implementations
// must honour the context deadline and be safe to call more than once.
type ShutdownAware interface {
    Shutdown(ctx context.Context) error
}

Subsystem interfaces

Each interface lists exactly the methods the consuming package needs.

// Implemented by *auth.Manager.
type AuthManager interface {
    GateAllows(r *http.Request, ability string, args ...interface{}) bool
    GateAuthorize(r *http.Request, ability string, args ...interface{}) error
}

// Implemented by *csrf.CSRF.
type CSRFProtector interface {
    Middleware(next http.Handler) http.Handler
}

// Implemented by *view.Engine.
type ViewEngine interface {
    Back(w http.ResponseWriter, r *http.Request)
}

// Logger mirrors the log package's Logger interface exactly, so
// *log.StackLogger, log.NullLogger, and the redacting logger all satisfy
// it with no adapter. app.Services types its Log field as this contract.
type Logger interface {
    Debug(msg string, kvs ...any)
    Info(msg string, kvs ...any)
    Warn(msg string, kvs ...any)
    Error(msg string, kvs ...any)
    Fatal(msg string, kvs ...any)
}

// Encryptor is the symmetric-encryption contract. Encrypt* return the
// base64 envelope string; Decrypt* take it back. The *WithAAD variants
// bind additional authenticated data to the ciphertext.
type Encryptor interface {
    Encrypt(plaintext string) (string, error)
    EncryptBytes(plaintext []byte) (string, error)
    Decrypt(payload string) (string, error)
    DecryptBytes(payload string) ([]byte, error)
    EncryptBytesWithAAD(plaintext, aad []byte) (string, error)
    DecryptBytesWithAAD(payload string, aad []byte) ([]byte, error)
    GenerateKey() (string, error)
}

The package declares many more contracts in the same spirit - one narrow interface per seam. The full set, with their implementers, is below.

Who implements them

InterfaceImplementation
EventDispatcherAwareevery event-emitting subsystem
ShutdownAwareevery subsystem holding background resources
AuthManager*auth.Manager
CSRFProtector*csrf.CSRF
CSRFTokenRotator*csrf.CSRF
ViewEngine*view.Engine
RedirectAllowlistthe router (Router.RedirectAllowedHosts)
Logger*log.StackLogger, log.NullLogger
Encryptor*crypto/drivers.AESDriver, crypto.Encryptor
Database*orm.Manager
LoginThrottlerauth.NoopLoginThrottler (default no-op)
ExceptionHandlerthe exceptions handler
Cache / CacheManagerthe cache manager and stores
MailManager / Mailer*mail.Manager
StorageManager / StorageDriverthe storage manager and drivers
Dispatcher / EventListenerthe events dispatcher
QueueDriver / QueueJobthe queue manager and jobs
Validatorthe validation engine
Notifier / NotificationChannelthe notification manager

Environment helpers

The contract leaf also owns the canonical APP_ENV vocabulary, because subsystems that cannot import the root app package (exceptions, auth, csrf, scheduler, …) still need to classify the environment. app re-exports these as app.Env, app.IsProductionEnv, and so on.

const EnvVar = "APP_ENV" // canonical env-var name

contract.GetEnv()                // normalised APP_ENV (lowercased, trimmed)
contract.NonProdEnvNames()       // []string of recognised non-prod names
contract.IsProductionEnv(env)    // "production"/"prod"/"staging"/unknown -> true
contract.IsTestingEnv(env)       // "test"/"testing" -> true
contract.IsDevelopmentEnv(env)   // "development"/"dev" -> true
contract.IsDevOrTestEnv(env)     // dev/test/local profiles -> true

Anything outside the recognised set is treated as production (fail-secure): a typo in APP_ENV never silently disables a production gate.

When you’d use it

  • Writing a custom auth manager, CSRF protector, or view engine - satisfy the interface and Velocity will accept your implementation wherever the concrete type is expected.
  • Testing against Velocity internals - use a fake that satisfies the contract instead of constructing the full type.

Most application code doesn’t import contract directly; it’s an internal seam that keeps package dependency graphs acyclic.