warren

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 2, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

Warren

A DDD-first application framework and CLI for Go backends.

⚠️ Pre-release, v0.1 in progress. Most of the framework is importable and works today — use cases, errors, domain, config, DI, lifecycle, the module system, the consumer chain, the transactional outbox, the persistence and transport ports, health, and validation. What is not built yet are the driver adapters that need third-party libraries: transport/http, transport/grpc, persistence/postgres, broker/kafka. Until they land a service serves requests by driving the transport.Table itself. The repository is being rebuilt spec-first: every package gets an approved SPEC.md before its first line of Go, retired once the package is implemented and reviewed. warren.md is the design; AGENT.md is the rules.


What Warren is

Four claims define it — everything in this repository exists to protect one of them:

  1. Transport-agnostic use cases. One app.Handler[Req, Res]; HTTP, gRPC, and message consumers are thin adapters over it. A handler imports no transport package — no net/http, no pgx, no kgo. That is the entire point.
  2. Real module encapsulation. A provider is private to its module unless exported, and imports are explicit — not one global container where everything sees everything.
  3. DDD as real types, not folder naming conventions — aggregates, events, and the transactional outbox as compiler-checked constructs.
  4. Architecture enforced in CI. warren lint arch fails the build when domain/ imports infrastructure/ — for your project and for Warren's own repository, same command.

Warren is not a web framework, an ORM, or a deployment platform. It composes existing routers and drivers behind stable ports, and its dependency budget is defensible: the kernel is standard library + dig, permanently.

TOOLING     warren/cli — templates · AST editor · analyzer     build-time only
ADAPTERS    transport/http · transport/grpc · broker/kafka     separate modules,
            persistence/postgres · observability · …           never import each other
CONTRACTS   app.Handler · broker.Publisher · Registrar · …     ports & shared types
KERNEL      warren · di · lifecycle · config · log · errors    stdlib + dig only

One line of main.go swaps Kafka for RabbitMQ. One handler serves three protocols. Every error the framework can detect surfaces at boot — never on request 1.


Status & roadmap

Progress is spec-first: ☑ means done and verified, not started.

Phase 0 — foundation
  • Package manifest written (warren.md) and repository reset to it
  • Rules rewritten (AGENT.md, CLAUDE.md)
  • All 32 packages scaffolded with a SPEC.md each
  • Every spec audited against the manifest — no invented API survived
  • Design contradictions found and catalogued (25 specs blocked on them)
  • Core decisions taken: config Source split, auth-code DLQ rows, Root[K] constraint, concrete registrars on Go 1.27
  • Remaining decisions folded into their specs and re-approved
  • Tooling rebuilt: Makefile, CI workflow, golangci config, module-rules check (scripts/invariants.sh)
  • Dependency audits run (dig first) — no library enters a go.mod without one
Phase 1 — kernel (buildable on Go 1.26, in dependency order)

All seven implemented packages were adversarially reviewed on 2026-08-01 (31 reproduced findings across two review rounds, all fixed with regression tests) and their specs retired — the code, tests, golden files, and warren.md entries are the contract now.

  • errors — the semantic vocabulary; load-bearing for everything (implemented; spec retired)
  • domainEntity, Root[K], AggregateRoot, Event; the §3.1 example compiles as a test (implemented; spec retired)
  • log — context-carried logger, Vendor mode, exported seeding surface (implemented; spec retired)
  • di — the container wrap; the golden diagnostic reproduces byte for byte; dig v1.19.0 audited (implemented; spec retired)
  • lifecycle — ordered start/stop, Ready() readiness gate (implemented; spec retired)
  • config (core) — Source-split loading: Load, Source, env, flags (implemented; spec retired — Module[T] lands with the root package)
  • config/yaml — the first file Source (needs its own spec + YAML library audit before the module exists)
  • warren (root) — module system, boot sequence, run loop (implemented with config.Module[T]; adversarially reviewed — 8 findings fixed — spec retired)
  • app core — Handler/HandlerFunc/Middleware/Chain (implemented; a five-middleware chain adds 0 allocs; §10 handler compiles verbatim)
  • app built-in middleware — Retrying/Traced/Metered/Authorized (implemented over the app-owned ports: RetryPolicy, AuthorizationPolicy, context-carried Telemetry)
  • app.Transactional — over the one-method app.UnitOfWork port (implemented; the app spec is retired)
  • broker port + consumer chain — envelope, Pipeline (Recover/Drain/ TraceExtract/Deduplicate/DeadLetter/Retry/ConcurrencyLimit), options (implemented; §2.6 disposition table one test per code)
  • inbox — dedupe-store port + stdlib memory store (implemented)
Phase 2 — transport
  • transport (port) — sealed Registrar, generic free functions, route table of pre-built closures (implemented on Go 1.26 — the "Fix A" shape; the 1.27 method form is a mechanical call-site rewrite)
  • Bump toolchain to Go 1.27; verify generic methods compile as designed (and that inference works — explicit type arguments are needed today)
  • transport/http — chi-backed adapter, the HTTP error column
  • transport/grpc — interceptors through the shared chain, the gRPC column
  • Fallback if 1.27 slips: generic free functions (compiles on 1.26; call sites change shape)
Phase 3 — messaging
  • Outbox/inbox ownership decisions (writer split, leader election, module map)
  • broker/memory — in-process driver, default in tests (implemented; passes the exported broker/brokertest contract suite)
  • outbox — writer port, relay, elector, memory store (implemented; the SQL store and advisory-lock elector land with postgres)
  • inbox — dedupe store, on by default (port + memory store shipped with the broker chain)
  • broker/kafka — franz-go driver (SASL type decision applied)
  • broker/rabbitmq, broker/nats — after their manifest entries are written
Phase 4 — persistence
  • persistence (port) — Repository, UnitOfWork, the Track/Collect enlistment seam, in-process driver + contract suite (implemented)
  • persistence/postgres — pool, UnitOfWork, outbox table, migrations
  • persistence/mongo, persistence/redis — after their manifest entries are written (mysql: deferred — exists only in a heading)
Phase 5 — cross-cutting
  • Core policy ports decided: RetryPolicy/AuthorizationPolicy/ Telemetry live in app, telemetry rides the context
  • observability — OTel wiring (spec approved)
  • validate — port in core, implementation in a submodule
  • health — check registry, liveness/readiness verdicts, root-scope binding (implemented; the routes land with the transport adapters)
  • warren/testing (warrentest) — boot a module with fakes, Invoke by type, AssertPublished, Golden (implemented; stdlib + core only)
  • auth, resilience, jobs, openapi
Phase 6 — the CLI *(the discovery engine: scaffolding real apps is how
weaknesses get found)*
  • warren new — a scaffold that compiles and tests against today's framework, with the CI gate that builds it (the anti-rot mechanism)
  • warren g module|entity|command|repository|consumer — golden-file tested, idempotent, stdlib AST editing (no dst: it has published no releases and sat untouched through Go 1.19–1.27)
  • warren lint arch --rules=layers|rings — subsumes two greps in scripts/invariants.sh; runs on Warren's own repo, same binary
  • warren version; tag core v0.1.0 so a scaffold's go.mod resolves
  • v0.2+: doctor, graph, explain di, templates eject
  • v0.3+: extract module, add <adapter>, migrate layout

Repository map

Where What
warren.md The package manifest — one entry per package, source of truth
AGENT.md Invariants, conventions, and process — canonical for humans and agents
<package>/SPEC.md The contract of a package not yet implemented; approved before any code, retired once the package ships
docs/assets/ Usage-flow diagrams for the approved specs

Contributing

Read AGENT.md first — the spec-first process, the dependency-audit rule, and the invariants apply to every change. No feature is implemented before its spec is approved, and no dependency is adopted without a written audit.

License

Apache-2.0 — see LICENSE.

Documentation

Overview

Package warren owns application bootstrap, the module system, and the run loop.

A module declaration is a value, not a side effect: NewModule returns an inert data structure and registers nothing. The bootstrapper walks the whole graph first, then materialises one DI scope per module, copies in only what each module's imports export, validates the whole graph, and only then instantiates anything — every error the framework can detect surfaces at boot, never on request 1.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is a bootstrapped application: the flattened module graph, its scoped containers, and its run loop.

func New

func New(modules ...Module) *App

New builds an App from the given module declarations. It does no fallible work — it collects the inert values and allocates the lifecycle; the boot sequence runs in Run or Start.

func (*App) Invoke

func (a *App) Invoke(module string, fn any) error

Invoke resolves fn's parameters from the named module's scope and calls fn — the seam tests and pre-transport mains reach the components the boot built, without constructing second instances. Module encapsulation holds: fn sees exactly what the module's own constructors see, own bindings and imported exports, nothing else. It is boot-time machinery (invariant 7 is about the request path); a transport adapter, once one exists, is the production caller of your handlers.

func (*App) Run

func (a *App) Run() error

Run boots the application and blocks until SIGINT or SIGTERM, then runs the shutdown sequence and returns. It returns the boot error if boot fails, otherwise whatever Stop returns. A second signal during shutdown cancels the drain — the force-exit short-circuit.

func (*App) Start

func (a *App) Start(ctx context.Context) error

Start runs boot steps 0–7 — flatten, scope, copy exports, validate, instantiate, hook up, open readiness — and returns once the application is serving. It exists so tests can drive boot without signals. Failure at any step is a startup failure; nothing is left half-started.

func (*App) Stop

func (a *App) Stop(ctx context.Context) error

Stop runs the shutdown sequence: readiness closes first, then hooks stop in reverse order, bounded by the force-exit deadline.

func (*App) Substitute

func (a *App) Substitute(subs ...Substitution) error

Substitute applies substitutions before boot: Substitute[T] replaces every provider of T, Bind[T] adds one in the root scope. It must be called before Start.

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module is an inert declaration of one module: its name, its imports, its providers, controllers, consumers, exports, and its lifecycle hooks. Constructing a Module registers nothing and performs no work.

Because Imports carries Module values, an import cycle is unrepresentable: closing one would be infinite recursion in the user's own constructors before New is ever called. Cycles between providers are detected by warren/di.

func NewModule

func NewModule(name string, opts ...ModuleOption) Module

NewModule returns an inert Module value named name, configured by opts. Nothing is registered and no container is touched. The call site is recorded: it is the "declared in module.go:14" line of the missing-provider diagnostic.

Declare each module ONCE. Modules are deduplicated by identity, so the natural `func Module() warren.Module` factory produces two distinct modules the moment two features import it, and two modules sharing a name is a boot error. The idiom that reads like a function and yields one identity:

var Module = sync.OnceValue(func() warren.Module {
    return warren.NewModule("platform", ...)
})

func (Module) Name

func (m Module) Name() string

Name reports the module's name — the scope App.Invoke addresses and the name diagnostics print.

type ModuleOption

type ModuleOption func(*Module)

ModuleOption configures a Module during NewModule.

func Consumers

func Consumers(consumers ...any) ModuleOption

Consumers declares the constructors of this module's message consumers. They are instantiated at boot; broker adapters register them at boot step 5. Like controllers, a consumer's constructor is also a provider — list it in one place only.

func Controllers

func Controllers(controllers ...any) ModuleOption

Controllers declares the constructors of this module's controllers. They are instantiated at boot; transport adapters register their routes at boot step 5. A controller's constructor is also a provider — list it here only, not in Providers too, or the duplicate registers as an ambiguous binding.

func Eager

func Eager[T any]() ModuleOption

Eager declares that T is materialised at boot even when nothing in the graph consumes it — for modules whose provider's construction IS the point. config.Module uses it so a bad config fails the boot even if no constructor injects the struct; without it, an unconsumed provider is simply never built.

func Exports

func Exports[T any]() ModuleOption

Exports makes T resolvable by modules that import this module. Anything not exported stays private. T must be a declared return type of one of the module's providers, controllers, or consumers — exporting anything else is a boot error, and a constructor returning a concrete type does not match an exported interface: declare the constructor's return type as the interface.

func Imports

func Imports(modules ...Module) ModuleOption

Imports declares the modules this module depends on. Only the imported modules' exported bindings become visible to it.

func OnStart

func OnStart(fn func(context.Context) error) ModuleOption

OnStart registers a startup hook for this module, run in dependency order at boot step 6. The hook is a plain closure fixed at declaration time and resolves nothing from the container; a hook that needs something built at boot — a consumer pipeline's drain func, a connection opened by a constructor — is registered the other way: the constructor injects lifecycle.Lifecycle (provided in the root scope) and appends its own lifecycle.Hook.

func OnStop

func OnStop(fn func(context.Context) error) ModuleOption

OnStop registers a shutdown hook for this module, run in reverse order at shutdown step 10. See OnStart for the boot-time-created alternative — the injected-Lifecycle pattern is how a consumer registers its drain.

func Providers

func Providers(constructors ...any) ModuleOption

Providers declares constructors owned by this module. A provider is private to its module unless its result type is also named in Exports.

Constructors wire; OnStart acquires. A constructor that opens a connection or starts a goroutine owns a resource the boot sequence cannot release if a later module fails to build — put acquisition in an OnStart hook, whose rollback the lifecycle guarantees.

type Substitution

type Substitution struct {
	// contains filtered or unexported fields
}

Substitution replaces or adds a binding before boot. It is the seam test harnesses use to inject fakes, and main can use it to provide a value it computed itself.

func Bind

func Bind[T any](v T) Substitution

Bind provides v as T in the root scope, where every module can see it.

If the graph already provides T, Bind REPLACES that provider rather than colliding with it: a harness binding a fake broker into an application whose platform module provides a real one is the normal case, and an ambiguous-binding failure there would be useless. Use Substitute when the replacement is required — it fails the boot if nothing matched.

func Substitute

func Substitute[T any](v T) Substitution

Substitute replaces every provider of T with v. An unmatched substitution is a boot error naming T — a typo'd fake is never silently ignored, which is the failure mode that makes test doubles untrustworthy.

Directories

Path Synopsis
app
Package app defines Warren's central abstraction: a transport-agnostic use case, and the core-ring middleware shape that decorates it.
Package app defines Warren's central abstraction: a transport-agnostic use case, and the core-ring middleware shape that decorates it.
internal/exampledomain
Package domain is the user-side domain of the §10 example, so the app tests can compile warren.md's handler verbatim.
Package domain is the user-side domain of the §10 example, so the app tests can compile warren.md's handler verbatim.
Package broker defines Warren's messaging ports: one driver-neutral message envelope, a publisher, a subscriber, and a message handler.
Package broker defines Warren's messaging ports: one driver-neutral message envelope, a publisher, a subscriber, and a message handler.
brokertest
Package brokertest is the contract suite every broker driver must pass: the in-process one, Kafka, RabbitMQ, NATS.
Package brokertest is the contract suite every broker driver must pass: the in-process one, Kafka, RabbitMQ, NATS.
memory
Package memory is the in-process broker: the default in tests, and the driver a modular monolith runs in production before its modules are extracted into services.
Package memory is the in-process broker: the default in tests, and the driver a modular monolith runs in production before its modules are extracted into services.
kafka module
cli module
Package config owns layered configuration resolution: struct defaults, then file sources, then environment variables, then command-line flags — later layers win, and the merged result is checked before boot continues.
Package config owns layered configuration resolution: struct defaults, then file sources, then environment variables, then command-line flags — later layers win, and the merged result is checked before boot continues.
di
Package di owns Warren's dependency-injection container: scoping, graph validation, and diagnostics.
Package di owns Warren's dependency-injection container: scoping, graph validation, and diagnostics.
internal/fixture/domain
Package domain is the contracts side of the §1.2 fixture graph the di tests run against.
Package domain is the contracts side of the §1.2 fixture graph the di tests run against.
internal/fixture/postgres
Package postgres is the providing module of the §1.2 fixture graph.
Package postgres is the providing module of the §1.2 fixture graph.
internal/fixture/user
Package user is the consuming module of the §1.2 fixture graph.
Package user is the consuming module of the §1.2 fixture graph.
Package domain provides the DDD building blocks Warren's other contracts are expressed in terms of: identity, aggregates, events, and specifications.
Package domain provides the DDD building blocks Warren's other contracts are expressed in terms of: identity, aggregates, events, and specifications.
Package errors defines Warren's semantic error vocabulary: a closed set of codes that describe what went wrong in terms a domain expert would use, with no reference to any transport.
Package errors defines Warren's semantic error vocabulary: a closed set of codes that describe what went wrong in terms a domain expert would use, with no reference to any transport.
Package health holds the registry of checks a service can be asked about and renders the two probe verdicts.
Package health holds the registry of checks a service can be asked about and renders the two probe verdicts.
Package inbox defines the dedupe-store port the consumer chain's Deduplicate stage records processed Message.IDs in, and ships the stdlib-only memory store that makes dedupe-by-default cost neither Docker nor a database.
Package inbox defines the dedupe-store port the consumer chain's Deduplicate stage records processed Message.IDs in, and ships the stdlib-only memory store that makes dedupe-by-default cost neither Docker nor a database.
Package lifecycle owns ordered startup and shutdown, readiness gating, and drain.
Package lifecycle owns ordered startup and shutdown, readiness gating, and drain.
Package log carries a *slog.Logger on the context and propagates the correlation ID that ties one request's records together.
Package log carries a *slog.Logger on the context and propagates the correlation ID that ties one request's records together.
openapi module
Package outbox implements the transactional outbox: the pattern that makes a state change and the events announcing it atomic without a distributed transaction.
Package outbox implements the transactional outbox: the pattern that makes a state change and the events announcing it atomic without a distributed transaction.
Package persistence is the port repositories and units of work are written against: load and store aggregates by identity, and make the state a handler wrote and the events its aggregates raised commit together or not at all.
Package persistence is the port repositories and units of work are written against: load and store aggregates by identity, and make the state a handler wrote and the events its aggregates raised commit together or not at all.
postgres module
Package warrentest boots a Warren module for a test with dependencies substituted, invokes handlers by request and response type, and asserts on what was published.
Package warrentest boots a Warren module for a test with dependencies substituted, invokes handlers by request and response type, and asserts on what was published.
Package transport is the port through which a controller exposes a use case over HTTP, gRPC, and events.
Package transport is the port through which a controller exposes a use case over HTTP, gRPC, and events.
http module
Package validate is the port transport adapters validate decoded requests through, plus the standard-library implementation of validate:"required".
Package validate is the port transport adapters validate decoded requests through, plus the standard-library implementation of validate:"required".
playground module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL