Documentation
¶
Overview ¶
Package modulex provides deterministic lifecycle orchestration for modular Go applications.
A Manager coordinates feature modules through an explicit lifecycle (configuring → initializing → initialized → starting → running → stopping → stopped), validates the dependency graph, rolls back partial failures, and supervises background tasks. Modules receive a Registry they can use to register services, access the router, tracer, logger, event bus, config, and start lifecycle-owned tasks.
Typed service registration helpers (Key, Provide, Resolve) are also provided.
EventBus adapter sub-packages such as github.com/mediusfy/modulex/nats, github.com/mediusfy/modulex/rabbitmq, and github.com/mediusfy/modulex/watermill keep framework dependencies out of the core package.
github.com/mediusfy/modulex/app provides an opinionated Run helper that owns the construct-manager/register-modules/signal-context/Init-Start-wait-Stop bootstrap skeleton every service entrypoint otherwise repeats.
Index ¶
- Variables
- func Provide[T any](reg Registry, key Key[T], svc T) error
- func Resolve[T any](reg Registry, key Key[T]) (T, error)
- type AckDecision
- type ConfigProvider
- type Diagnostics
- type DurableConsumer
- type DurableHandler
- type DurableMessage
- type DurableSubscribeOption
- type DurableSubscribeOptions
- type EventBus
- type EventBusProvider
- type EventHandler
- type HealthCheckProvider
- type HealthCheckRegisterer
- type Key
- type LifecycleState
- type LifecycleTimings
- type LoggerProvider
- type Manager
- func (m *Manager) Diagnostics() Diagnostics
- func (m *Manager) EventBus() EventBus
- func (m *Manager) ExportDAG() string
- func (m *Manager) GetConfig(target interface{}) error
- func (m *Manager) Go(ctx context.Context, taskName string, fn func(ctx context.Context) error) (*TaskHandle, error)
- func (m *Manager) HealthChecks() map[string]func(context.Context) error
- func (m *Manager) InitModules(ctx context.Context) error
- func (m *Manager) Logger() *slog.Logger
- func (m *Manager) ModuleContract() ModuleContract
- func (m *Manager) ReadinessChecks() map[string]func(context.Context) error
- func (m *Manager) RegisterHealthCheck(name string, check func(context.Context) error) error
- func (m *Manager) RegisterModule(mod Module) error
- func (m *Manager) RegisterReadinessCheck(name string, check func(context.Context) error) error
- func (m *Manager) RegisterService(name string, svc interface{}) error
- func (m *Manager) ResolveService(name string) (interface{}, error)
- func (m *Manager) StartModules(ctx context.Context) error
- func (m *Manager) State() LifecycleState
- func (m *Manager) StopModules(ctx context.Context) error
- type ManagerOption
- func WithConfigLoader(loader func(target interface{}) error) ManagerOption
- func WithEventBus(eb EventBus) ManagerOption
- func WithLogger(logger *slog.Logger) ManagerOption
- func WithPanicPolicy(policy PanicPolicy) ManagerOption
- func WithTracer(tracer Tracer) ManagerOption
- func WithTypedConfig[T any](cfg T) ManagerOption
- type Module
- type ModuleContract
- type ModuleContractEntry
- type ModuleTiming
- type PanicPolicy
- type Publisher
- type ReadinessProvider
- type ReadinessRegisterer
- type Registry
- type ReplayPolicy
- type ServiceRegisterer
- type ServiceRegistry
- type ServiceResolver
- type Span
- type SpanContext
- type Starter
- type Stopper
- type Subscriber
- type TaskDiagnostic
- type TaskHandle
- type TaskSpawner
- type Tracer
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrCircularDependency is returned when registered modules contain a circular dependency. ErrCircularDependency = errors.New("circular dependency detected") // ErrServiceNotFound is returned when a requested service is not registered in the locator. ErrServiceNotFound = errors.New("service not found") // ErrRegistryLocked is returned when a module attempts to register a service after registry initialization has completed. ErrRegistryLocked = errors.New("registry is locked: cannot register services after initialization") // ErrModuleNil is returned when a nil module is passed to RegisterModule. ErrModuleNil = errors.New("module must not be nil") // ErrInvalidModuleName is returned when a module name is empty or whitespace-only. ErrInvalidModuleName = errors.New("module name must not be empty") // ErrDuplicateModule is returned when RegisterModule is called with a module name that is already registered. ErrDuplicateModule = errors.New("module already registered") // ErrDuplicateService is returned when RegisterService is called with a service key that is already registered. ErrDuplicateService = errors.New("service already registered") // ErrDuplicateTask is returned when Go is called with a task name that is already in use. ErrDuplicateTask = errors.New("task already exists") // ErrInvalidTaskName is returned when Go is called with an empty or whitespace-only task name. ErrInvalidTaskName = errors.New("task name must not be empty") // ErrDependencyNotFound is returned when a module depends on a module that has not been registered. ErrDependencyNotFound = errors.New("module dependency not found") // ErrSelfDependency is returned when a module declares itself as a dependency. ErrSelfDependency = errors.New("module cannot depend on itself") // ErrInvalidDependencyName is returned when a module declares a dependency with an empty or whitespace-only name. ErrInvalidDependencyName = errors.New("module dependency name must not be empty") // ErrInvalidServiceName is returned when a service is registered with an empty or whitespace-only key. ErrInvalidServiceName = errors.New("service name must not be empty") // ErrServiceTypeMismatch is returned when a resolved service cannot be type-asserted to the requested type. ErrServiceTypeMismatch = errors.New("service type mismatch") // ErrInvalidLifecycleState is returned when a lifecycle operation is requested while the manager is in an incompatible state. ErrInvalidLifecycleState = errors.New("invalid lifecycle state") // ErrNoConfigLoader is returned by GetConfig when no config loader was // configured at construction time or via WithConfigLoader. ErrNoConfigLoader = errors.New("no config loader configured") // ErrInvalidPanicPolicy is returned by NewManager when WithPanicPolicy is // given a value outside the defined PanicPolicy enum. ErrInvalidPanicPolicy = errors.New("invalid panic policy") // ErrInvalidHealthCheckName is returned when a health check name is empty. ErrInvalidHealthCheckName = errors.New("health check name must not be empty") // ErrInvalidReadinessCheckName is returned when a readiness check name is empty. ErrInvalidReadinessCheckName = errors.New("readiness check name must not be empty") // ErrHealthCheckNil is returned when RegisterHealthCheck is given a nil // check function. A registered nil check would panic if any caller // invoked it directly rather than defensively nil-checking first (as // the httpx package does); rejecting it at registration means a nil // check function can never reach that map in the first place. ErrHealthCheckNil = errors.New("health check function must not be nil") // ErrReadinessCheckNil is returned when RegisterReadinessCheck is given // a nil check function. See ErrHealthCheckNil for why this is rejected // at registration rather than left to each caller to guard against. ErrReadinessCheckNil = errors.New("readiness check function must not be nil") )
var ErrConfigTypeMismatch = errors.New("config target type mismatch")
ErrConfigTypeMismatch is returned by the config loader installed via WithTypedConfig when GetConfig's target is not a pointer to the configured type.
Functions ¶
func Provide ¶
Provide registers a typed service implementation in the registry. It is a type-safe wrapper around Registry.RegisterService.
Example ¶
package main
import (
"fmt"
"io"
"log/slog"
"github.com/mediusfy/modulex"
)
type greetingService struct {
greeting string
}
func (s greetingService) Greet(name string) string {
return fmt.Sprintf("%s, %s!", s.greeting, name)
}
func main() {
manager := newExampleManager()
key := modulex.NewKey[greetingService]("example.GreetingService")
if err := modulex.Provide(manager, key, greetingService{greeting: "Hello"}); err != nil {
panic(err)
}
svc, err := modulex.Resolve(manager, key)
if err != nil {
panic(err)
}
fmt.Println(svc.Greet("Modulex"))
}
func newExampleManager() *modulex.Manager {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
mgr, err := modulex.NewManager(modulex.WithLogger(logger))
if err != nil {
panic(err)
}
return mgr
}
Output: Hello, Modulex!
Types ¶
type AckDecision ¶ added in v0.6.0
type AckDecision int
AckDecision is the disposition a DurableHandler assigns to a message it was given, replacing the bare "error or nil" signal EventHandler uses. EventHandler's plain error return cannot distinguish "please retry this message" from "give up on this message without retrying" from "this message can never succeed, route it to a dead letter" — a durable consumer with real ack/nack/dead-letter semantics needs to express all three, so DurableHandler returns AckDecision instead of error.
const ( // Ack acknowledges the message as successfully processed. A conforming // DurableConsumer will not redeliver it. Ack AckDecision = iota // Nack indicates processing failed but should be retried. A conforming // DurableConsumer redelivers the message, subject to whatever // retry/backoff/max-attempts policy it documents. Nack // DeadLetter indicates processing failed terminally: the message must // not be redelivered again. A conforming DurableConsumer routes it to // whatever dead-letter mechanism it documents (a separate subject or // stream, a broker-native DLQ, or simply marking it permanently failed) // instead of retrying it. DeadLetter )
func (AckDecision) String ¶ added in v0.6.0
func (d AckDecision) String() string
String returns a lower_snake_case name for d, or "unknown" for an out-of-range value.
type ConfigProvider ¶
type ConfigProvider interface {
// GetConfig unmarshals configuration values into the target structure.
// This abstract config retrieval prevents features from directly reading global configurations.
GetConfig(target interface{}) (err error)
}
ConfigProvider is the capability to unmarshal configuration values into a target structure.
type Diagnostics ¶ added in v0.6.0
type Diagnostics struct {
State string `json:"state"`
Modules ModuleContract `json:"modules"`
Services []string `json:"services"`
Tasks []TaskDiagnostic `json:"tasks"`
HealthChecks []string `json:"health_checks"`
ReadinessChecks []string `json:"readiness_checks"`
Timings LifecycleTimings `json:"timings,omitempty"`
}
Diagnostics is a point-in-time, machine-readable snapshot of the manager's internal state: lifecycle state, the module dependency graph, registered service names, supervised task status, health/readiness check names, and lifecycle timings.
Diagnostics is safe to log, export, or attach to a support ticket: it deliberately never includes registered service values (only their sorted names), health/readiness check function bodies (only their sorted names), task closures, or any other internal implementation detail such as mutexes or the internal task cancellation context. Only names, booleans, error strings, and durations are exposed.
type DurableConsumer ¶ added in v0.6.0
type DurableConsumer interface {
// SubscribeDurable registers handler as the durable consumer for topic
// under the identity and options given. opts must include
// WithConsumerName; an adapter rejects a call without one.
SubscribeDurable(ctx context.Context, topic string, handler DurableHandler, opts ...DurableSubscribeOption) (err error)
}
DurableConsumer is the capability of consuming a topic with the stronger guarantees Subscriber deliberately does not promise: explicit acknowledgement, redelivery, replay, consumer identity, and dead-letter routing. Not every EventBus adapter implements DurableConsumer — callers that need these guarantees should type-assert for it rather than assuming any EventBus or Subscriber provides them.
DurableConsumer deliberately does not embed Subscriber. A durable handler needs to express more than "error or nil" (see AckDecision), so it uses the distinct DurableHandler signature rather than EventHandler; the two therefore cannot share one Subscribe method identity. An adapter is free to implement both Subscriber and DurableConsumer (e.g. by also embedding a plain EventBus), but the capabilities are independent and are checked independently via type assertion.
DurableConsumer documents the six semantics named by MOD-54, three as the SubscribeDurable method contract and three as documented properties a correct implementation must uphold:
- Acknowledgement (method contract): DurableHandler returns an AckDecision — Ack, Nack, or DeadLetter — for every message it is given. The adapter is responsible for translating that decision into its broker's native ack mechanism.
- Consumer identity (method contract, via DurableSubscribeOptions): ConsumerName names a durable consumer/consumer-group. Reusing the same ConsumerName resumes from its last acknowledged position rather than starting over, including across process restarts. Multiple concurrent subscriptions sharing one ConsumerName load-balance messages across them (a competing-consumers group) rather than each receiving every message.
- Replay (method contract, via DurableSubscribeOptions): ReplayPolicy selects where a brand-new ConsumerName starts reading from — from the oldest retained message (ReplayAll) or only new ones (ReplayNew).
- Retry (documented property): on Nack, the adapter redelivers the message subject to its own documented retry/backoff/max-attempts policy. This is adapter-defined rather than a method because retry configuration (backoff curves, max attempts, poison-message thresholds) varies too much across brokers to usefully standardize at this interface's level; see the implementing adapter's doc comment for its specific policy.
- Ordering (documented property): within a single SubscribeDurable call (one ConsumerName, one subscription), an implementation must process and resolve (ack/nack/dead-letter) messages in the order the broker delivered them before fetching the next one, so relative order is preserved for that subscription. Ordering across multiple concurrent subscriptions sharing one ConsumerName (a competing-consumers group) is NOT guaranteed, since the broker may deliver to whichever subscription is next available.
- Dead-letter (documented property): on DeadLetter, the adapter must never redeliver the message again through the normal retry path. How it is routed instead (a separate subject/stream, a broker-native DLQ, or simply discarded after being marked permanently failed) is adapter-defined; see the implementing adapter's doc comment.
type DurableHandler ¶ added in v0.6.0
type DurableHandler func(ctx context.Context, msg DurableMessage) (decision AckDecision)
DurableHandler processes one message delivered by a DurableConsumer and returns the AckDecision it should receive. See AckDecision for why this differs from EventHandler's bare error return.
type DurableMessage ¶ added in v0.6.0
type DurableMessage struct {
// Payload is the message body.
Payload []byte
// Redelivered reports whether this delivery attempt is a retry of a
// message previously delivered (to this consumer or an earlier attempt
// by the same durable consumer identity).
Redelivered bool
// DeliveryCount is the number of times this message has been delivered
// to this durable consumer identity, starting at 1 for the first
// delivery. An adapter that cannot track delivery count reports 0.
DeliveryCount int
}
DurableMessage carries the payload and delivery metadata for one message given to a DurableHandler.
type DurableSubscribeOption ¶ added in v0.6.0
type DurableSubscribeOption func(*DurableSubscribeOptions)
DurableSubscribeOption configures a DurableSubscribeOptions value.
func WithConsumerName ¶ added in v0.6.0
func WithConsumerName(name string) DurableSubscribeOption
WithConsumerName sets the durable consumer/consumer-group identity for a DurableConsumer subscription. See the "Consumer identity" semantic on DurableConsumer.
func WithReplayPolicy ¶ added in v0.6.0
func WithReplayPolicy(p ReplayPolicy) DurableSubscribeOption
WithReplayPolicy sets where a brand-new consumer identity starts reading from. See ReplayPolicy and the "Replay" semantic on DurableConsumer.
type DurableSubscribeOptions ¶ added in v0.6.0
type DurableSubscribeOptions struct {
// ConsumerName identifies the durable consumer/consumer-group identity
// (see the "Consumer identity" semantic on DurableConsumer). Required;
// an adapter rejects an empty ConsumerName.
ConsumerName string
// Replay selects where a brand-new ConsumerName starts reading from.
// See ReplayPolicy.
Replay ReplayPolicy
}
DurableSubscribeOptions configures a DurableConsumer subscription. Use the With* option functions below to set fields; the zero value is not a valid configuration (ConsumerName is required).
type EventBus ¶
type EventBus interface {
// Publish sends a payload to a specific topic/subject.
Publish(ctx context.Context, topic string, payload []byte) (err error)
// Subscribe listens to a topic and invokes the handler when an event is
// received. The adapter determines how handler errors affect
// acknowledgment, retry, and redelivery; see the adapter documentation for
// its policy.
Subscribe(ctx context.Context, topic string, handler EventHandler) (err error)
// Close gracefully disconnects from the broker, shutting down active subscribers.
Close(ctx context.Context) (err error)
}
EventBus abstracts the underlying message broker (NATS, Kafka, RabbitMQ, etc.).
type EventBusProvider ¶
type EventBusProvider interface {
EventBus() (eb EventBus)
}
EventBusProvider is the capability to access the pluggable event bus.
type EventHandler ¶
EventHandler processes an event delivered by an EventBus. The meaning of a returned error depends on the concrete adapter: some adapters ack/nack based on the error, others only log it. Callers coding against the EventBus abstraction should not assume specific retry or redelivery semantics.
type HealthCheckProvider ¶ added in v0.3.0
type HealthCheckProvider interface {
HealthChecks() (checks map[string]func(context.Context) error)
}
HealthCheckProvider exposes the registered health (liveness) checks.
type HealthCheckRegisterer ¶ added in v0.6.0
type HealthCheckRegisterer interface {
// RegisterHealthCheck registers a health (liveness) check function under a
// unique name.
RegisterHealthCheck(name string, check func(context.Context) error) (err error)
}
HealthCheckRegisterer is the capability to register a module health (liveness) check.
A health check answers "is this process functioning correctly?" A failing health check means the process is broken and should be restarted (e.g. by an orchestrator's liveness probe). Contrast this with ReadinessRegisterer, which answers "should this process currently receive traffic?" — a failing readiness check means the instance should be pulled from load balancing, not restarted.
type Key ¶
type Key[T any] struct { // contains filtered or unexported fields }
Key is a typed service locator key. It couples a string identifier with a compile-time type so that Provide and Resolve can be type-safe.
type LifecycleState ¶
type LifecycleState int
LifecycleState represents the current phase of the Manager's lifecycle.
const ( StateConfiguring LifecycleState = iota StateInitializing StateInitialized StateStarting StateRunning StateStopping StateStopped )
func (LifecycleState) String ¶
func (s LifecycleState) String() string
type LifecycleTimings ¶ added in v0.6.0
type LifecycleTimings struct {
InitModules time.Duration `json:"init_modules_ns,omitempty"`
StartModules time.Duration `json:"start_modules_ns,omitempty"`
ModuleInit []ModuleTiming `json:"module_init,omitempty"`
ModuleStart []ModuleTiming `json:"module_start,omitempty"`
}
LifecycleTimings captures how long the InitModules and StartModules phases took, in total and (when available) per module. Durations are time.Duration values, which marshal to JSON as an integer count of nanoseconds. All fields are zero-valued/omitted until the corresponding phase has run at least once; they are never fabricated.
type LoggerProvider ¶
LoggerProvider is the capability to access the system logger.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager implements the Registry interface and orchestrates the module lifecycles.
func NewManager ¶
func NewManager(opts ...ManagerOption) (*Manager, error)
NewManager creates a new instance of Manager.
eb may be nil, in which case a no-op EventBus is used. logger may be nil, in which case slog.Default() is used. NewManager returns ErrInvalidPanicPolicy if WithPanicPolicy is given a value outside the defined PanicPolicy enum.
func (*Manager) Diagnostics ¶ added in v0.6.0
func (m *Manager) Diagnostics() Diagnostics
Diagnostics returns a snapshot of the manager's current state. See the Diagnostics type doc for the safety guarantees this method provides.
func (*Manager) ExportDAG ¶ added in v0.3.0
ExportDAG returns a Mermaid-compatible DAG visualization of the registered modules.
func (*Manager) Go ¶
func (m *Manager) Go(ctx context.Context, taskName string, fn func(ctx context.Context) error) (*TaskHandle, error)
Go implements Registry. It spawns a supervised background routine while preserving trace ancestry when a Tracer is configured, and returns a handle for awaiting completion.
func (*Manager) HealthChecks ¶ added in v0.3.0
HealthChecks returns all registered health (liveness) checks.
func (*Manager) InitModules ¶
InitModules sorts the modules topologically based on dependencies, then initializes them sequentially in dependency order inside trace spans.
If a module fails to initialize, all previously initialized modules are stopped in reverse order and the manager moves to the stopped state.
Example ¶
package main
import (
"context"
"fmt"
"io"
"log/slog"
"github.com/mediusfy/modulex"
)
func main() {
manager := newExampleManager()
mod := &exampleModule{}
if err := manager.RegisterModule(mod); err != nil {
panic(err)
}
if err := manager.InitModules(context.Background()); err != nil {
panic(err)
}
fmt.Println("initialized")
}
type exampleModule struct{}
func (m *exampleModule) Name() string { return "example" }
func (m *exampleModule) DependsOn() []string { return nil }
func (m *exampleModule) Init(context.Context, modulex.Registry) error { return nil }
func newExampleManager() *modulex.Manager {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
mgr, err := modulex.NewManager(modulex.WithLogger(logger))
if err != nil {
panic(err)
}
return mgr
}
Output: initialized
func (*Manager) ModuleContract ¶ added in v0.6.0
func (m *Manager) ModuleContract() ModuleContract
ModuleContract returns a deterministic, JSON-marshalable description of the registered modules and their declared dependencies. Modules are sorted alphabetically by name, and each module's DependsOn list is sorted alphabetically as well, so that two calls against the same manager state produce byte-identical JSON.
func (*Manager) ReadinessChecks ¶ added in v0.4.1
ReadinessChecks returns all registered readiness checks.
func (*Manager) RegisterHealthCheck ¶ added in v0.3.0
RegisterHealthCheck registers a health (liveness) check function under a unique name. check must not be nil.
func (*Manager) RegisterModule ¶
RegisterModule registers a feature module in the manager. Modules should be registered before calling InitModules.
Registration is rejected if the module is nil, its name is empty, another module with the same name is already registered, or initialization has already started. Independent modules preserve their registration order as the deterministic tie-break during topological sorting.
func (*Manager) RegisterReadinessCheck ¶ added in v0.4.1
RegisterReadinessCheck registers a readiness check function under a unique name. check must not be nil.
func (*Manager) RegisterService ¶
RegisterService implements Registry. It registers a service instance to the service locator. Registration is only permitted before InitModules has completed.
func (*Manager) ResolveService ¶
ResolveService implements Registry. It retrieves a registered service by its identifier.
func (*Manager) StartModules ¶
StartModules starts all registered modules in topological dependency order inside trace spans.
If a module fails to start, all previously started modules are stopped in reverse order and the manager moves to the stopped state.
Example ¶
package main
import (
"context"
"fmt"
"io"
"log/slog"
"github.com/mediusfy/modulex"
)
func main() {
manager := newExampleManager()
mod := &exampleModule{}
if err := manager.RegisterModule(mod); err != nil {
panic(err)
}
if err := manager.InitModules(context.Background()); err != nil {
panic(err)
}
if err := manager.StartModules(context.Background()); err != nil {
panic(err)
}
fmt.Println("started")
}
type exampleModule struct{}
func (m *exampleModule) Name() string { return "example" }
func (m *exampleModule) DependsOn() []string { return nil }
func (m *exampleModule) Init(context.Context, modulex.Registry) error { return nil }
func newExampleManager() *modulex.Manager {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
mgr, err := modulex.NewManager(modulex.WithLogger(logger))
if err != nil {
panic(err)
}
return mgr
}
Output: started
func (*Manager) State ¶
func (m *Manager) State() LifecycleState
State returns the current lifecycle state of the manager.
func (*Manager) StopModules ¶
StopModules cancels supervised tasks, stops registered modules in reverse topological order when they were running, and closes the EventBus.
StopModules is idempotent: calling it multiple times returns nil without re-executing shutdown logic. It is context-aware and joins all shutdown errors so that no failure is silently dropped.
StopModules returns ErrInvalidLifecycleState if called while InitModules or StartModules is concurrently in progress on another goroutine (i.e. the manager is in StateInitializing or StateStarting). Those phases iterate modules without holding the manager's state lock, so racing a concurrent StopModules against them cannot be done safely: it would let StopModules tear down tasks and the event bus while a module's Init/Start is still running, and could have the in-progress phase overwrite StateStopped with StateInitialized/StateRunning once it completes, silently breaking the idempotency guarantee above. To cancel an in-flight InitModules or StartModules call, cancel the context passed to it instead; call StopModules once it returns.
Example ¶
package main
import (
"context"
"fmt"
"io"
"log/slog"
"github.com/mediusfy/modulex"
)
func main() {
manager := newExampleManager()
mod := &exampleModule{}
if err := manager.RegisterModule(mod); err != nil {
panic(err)
}
if err := manager.InitModules(context.Background()); err != nil {
panic(err)
}
if err := manager.StartModules(context.Background()); err != nil {
panic(err)
}
if err := manager.StopModules(context.Background()); err != nil {
panic(err)
}
fmt.Println("stopped")
}
type exampleModule struct{}
func (m *exampleModule) Name() string { return "example" }
func (m *exampleModule) DependsOn() []string { return nil }
func (m *exampleModule) Init(context.Context, modulex.Registry) error { return nil }
func newExampleManager() *modulex.Manager {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
mgr, err := modulex.NewManager(modulex.WithLogger(logger))
if err != nil {
panic(err)
}
return mgr
}
Output: stopped
type ManagerOption ¶
type ManagerOption func(*Manager)
ManagerOption configures a Manager during construction.
func WithConfigLoader ¶
func WithConfigLoader(loader func(target interface{}) error) ManagerOption
WithConfigLoader configures the config loader after construction. This is useful when using the options pattern and keeps the positional configLoader argument nil-safe.
func WithEventBus ¶ added in v0.3.0
func WithEventBus(eb EventBus) ManagerOption
WithEventBus configures the pluggable event bus. If nil, a no-op event bus is used so event publishing remains optional.
func WithLogger ¶ added in v0.3.0
func WithLogger(logger *slog.Logger) ManagerOption
WithLogger configures the logger used by the manager and its modules. If nil, slog.Default() is used.
func WithPanicPolicy ¶
func WithPanicPolicy(policy PanicPolicy) ManagerOption
WithPanicPolicy sets the panic policy for supervised background tasks.
func WithTracer ¶
func WithTracer(tracer Tracer) ManagerOption
WithTracer injects a Tracer implementation into the Manager. If nil, a no-op tracer is used so tracing remains optional for core consumers.
func WithTypedConfig ¶ added in v0.5.1
func WithTypedConfig[T any](cfg T) ManagerOption
Example:
modulex.NewManager(modulex.WithTypedConfig(cfg))
type Module ¶
type Module interface {
// Name returns the unique, kebab-case name of the feature module.
Name() (name string)
// DependsOn returns the names of other modules that this module depends on.
// The Manager uses this list to sort the modules topologically before initialization.
DependsOn() (deps []string)
// Init initializes the module with the registry.
// This is where modules register their services, register routes, and resolve dependencies.
Init(ctx context.Context, reg Registry) (err error)
}
Module represents a self-contained feature module that complies with Hexagonal Architecture. It acts as the composition root of the feature, instantiating services and adapters, and wiring them through the central registry.
Start and Stop are optional lifecycle capabilities. A module that needs to run background work during startup implements Starter; a module that owns resources that must be released implements Stopper. The manager skips modules that do not implement these interfaces, so simple modules do not require no-op methods.
type ModuleContract ¶ added in v0.6.0
type ModuleContract struct {
Modules []ModuleContractEntry `json:"modules"`
}
ModuleContract is a machine-readable description of the registered modules and their declared dependency edges, independent of the Mermaid-oriented ExportDAG. It is intended for diffing between two versions of a running application's module topology (e.g. in CI, or between deployments), so its JSON encoding is fully deterministic: two calls to Manager.ModuleContract against the same manager state always marshal to byte-identical JSON.
type ModuleContractEntry ¶ added in v0.6.0
type ModuleContractEntry struct {
Name string `json:"name"`
DependsOn []string `json:"depends_on"`
}
ModuleContractEntry describes a single registered module and the sorted names of the modules it declares as dependencies.
type ModuleTiming ¶ added in v0.6.0
type ModuleTiming struct {
Name string `json:"name"`
DurationNs time.Duration `json:"duration_ns"`
}
ModuleTiming records how long a single module took during an InitModules or StartModules phase.
type PanicPolicy ¶
type PanicPolicy int
PanicPolicy controls how the manager reacts to a panic in a supervised task.
const ( // PanicPolicyLog recovers from the panic, records it as a task error, and logs it. PanicPolicyLog PanicPolicy = iota // PanicPolicyPropagate allows the panic to crash the application. PanicPolicyPropagate )
type Publisher ¶ added in v0.6.0
Publisher is the narrow capability of publishing a payload to a topic. It makes no promise about delivery durability beyond what the concrete adapter documents in its own doc comments: a Publisher may be backed by an at-most-once fire-and-forget transport (core NATS) or an acknowledged, durable one (JetStream) — the interface itself does not distinguish them.
Every EventBus implementation already satisfies Publisher for free, since Go interfaces are structural and EventBus.Publish has this exact signature. Publisher exists so code that only needs to publish can depend on the narrower capability instead of the full EventBus.
type ReadinessProvider ¶ added in v0.4.1
type ReadinessProvider interface {
ReadinessChecks() (checks map[string]func(context.Context) error)
}
ReadinessProvider exposes the registered readiness checks.
type ReadinessRegisterer ¶ added in v0.6.0
type ReadinessRegisterer interface {
// RegisterReadinessCheck registers a readiness check function under a
// unique name.
RegisterReadinessCheck(name string, check func(context.Context) error) (err error)
}
ReadinessRegisterer is the capability to register a module readiness check.
A readiness check answers "should this process currently receive traffic?" A failing readiness check means the instance is temporarily unable to serve requests (e.g. its database pool isn't warm yet, a dependency is unreachable, a cache hasn't primed) and should be pulled from the load balancer — the process itself is otherwise healthy and should not be restarted. Contrast this with HealthCheckRegisterer, whose checks answer "is this process functioning correctly?" and whose failures indicate the process should be restarted.
The consumer defines what "ready" means for their service by registering named check functions; Modulex only abstracts registration, aggregation, and HTTP exposure (see modulex/httpx).
type Registry ¶
type Registry interface {
ServiceRegistry
EventBusProvider
ConfigProvider
LoggerProvider
TaskSpawner
HealthCheckRegisterer
HealthCheckProvider
ReadinessRegisterer
ReadinessProvider
}
Registry manages the collection of features and cross-cutting platform components. It acts as a service locator and event bus hub, preventing features from importing each other directly or coupling to specific messaging architectures.
Registry is a composite of smaller capability interfaces. Modules that only need a subset of these capabilities can depend on the narrower interfaces (ServiceRegistry, EventBusProvider, ConfigProvider, LoggerProvider, or TaskSpawner) instead of the full Registry.
type ReplayPolicy ¶ added in v0.6.0
type ReplayPolicy int
ReplayPolicy selects where a brand-new DurableConsumer subscription starts reading from a topic. It only affects the first time a given ConsumerName (see DurableSubscribeOptions) is used; a durable consumer identity with prior acknowledged progress resumes from that position instead of replaying, regardless of ReplayPolicy.
const ( // ReplayAll starts from the oldest message the adapter has retained. ReplayAll ReplayPolicy = iota // ReplayNew delivers only messages published after the subscription is // established; nothing previously retained is replayed. ReplayNew )
func (ReplayPolicy) String ¶ added in v0.6.0
func (p ReplayPolicy) String() string
String returns a lower_snake_case name for p, or "unknown" for an out-of-range value.
type ServiceRegisterer ¶ added in v0.6.0
type ServiceRegisterer interface {
// RegisterService registers a service implementation under a unique key (e.g. "incidents.Service").
// Returns ErrRegistryLocked if the registry has already finished initialization.
RegisterService(name string, svc interface{}) (err error)
}
ServiceRegisterer is the capability to register a service instance under a unique name. Registrations are only permitted before the registry has finished initialization.
type ServiceRegistry ¶
type ServiceRegistry interface {
ServiceRegisterer
ServiceResolver
}
ServiceRegistry combines service registration and resolution.
type ServiceResolver ¶
type ServiceResolver interface {
// ResolveService resolves a registered service implementation by name.
// If the service is not found, it returns ErrServiceNotFound.
ResolveService(name string) (svc interface{}, err error)
}
ServiceResolver is the capability to resolve a previously registered service by name.
type Span ¶
type Span interface {
// End completes the span.
End()
// RecordError attaches an error to the span.
RecordError(err error)
// SetAttributes attaches key-value pairs to the span.
SetAttributes(attrs map[string]any)
}
Span is a minimal lifecycle span created by a Tracer.
type SpanContext ¶
SpanContext is an opaque span context used for trace propagation.
type Starter ¶ added in v0.6.0
Starter is an optional lifecycle capability for modules that begin background work or listeners during startup. The manager calls Start after all modules have been initialized successfully.
type Stopper ¶ added in v0.6.0
Stopper is an optional lifecycle capability for modules that release resources during shutdown. The manager calls Stop in reverse topological order when stopping the application or rolling back a failed init/start.
type Subscriber ¶ added in v0.6.0
type Subscriber interface {
Subscribe(ctx context.Context, topic string, handler EventHandler) (err error)
}
Subscriber is the narrow capability of registering a fire-and-forget handler for a topic. Subscriber makes NO durability guarantee: whether a delivered message is retried, redelivered, or silently dropped on handler error is entirely adapter-defined (see each adapter's Subscribe doc comment for its specific policy). A caller that needs at-least-once delivery, explicit acknowledgement, replay, or dead-letter semantics must use DurableConsumer instead — Subscriber alone never implies any of that.
Every EventBus implementation already satisfies Subscriber for free, since Go interfaces are structural and EventBus.Subscribe has this exact signature.
type TaskDiagnostic ¶ added in v0.6.0
type TaskDiagnostic struct {
Name string `json:"name"`
Done bool `json:"done"`
Err string `json:"error,omitempty"`
}
TaskDiagnostic is a safe, name-only snapshot of a supervised task's completion status. It surfaces exactly the information TaskHandle.Wait already exposes to callers today (a name, whether the task has finished, and its final error, if any) so including it in Diagnostics does not leak anything new.
type TaskHandle ¶
type TaskHandle struct {
// contains filtered or unexported fields
}
TaskHandle identifies a supervised background task started by Manager.Go. Callers can use it to wait for completion or inspect the final error.
func (*TaskHandle) Done ¶ added in v0.6.0
func (h *TaskHandle) Done() bool
Done reports whether the task has finished, without blocking. It is safe to call concurrently with Wait and from diagnostics code that must not block on a long-running task.
func (*TaskHandle) Err ¶ added in v0.6.0
func (h *TaskHandle) Err() error
Err returns the task's final error. It only reflects a meaningful value once Done reports true; a task that is still running always reports a nil error here, regardless of what it eventually returns.
func (*TaskHandle) Name ¶
func (h *TaskHandle) Name() string
func (*TaskHandle) Wait ¶
func (h *TaskHandle) Wait() error
Wait blocks until the task finishes and returns its final error.
type TaskSpawner ¶
type TaskSpawner interface {
// Go spawns a supervised goroutine to execute background work. It creates a
// child span when a Tracer is configured, recovers from panics according to
// the manager's panic policy, and returns a handle that can be used to wait
// for the task to finish. Tasks are cancelled and awaited during manager
// shutdown. Returns ErrRegistryLocked if the manager is stopping or stopped,
// or ErrDuplicateTask if a task with the same name already exists.
Go(ctx context.Context, taskName string, fn func(ctx context.Context) error) (handle *TaskHandle, err error)
}
TaskSpawner is the capability to start a supervised background task. Tasks receive a lifecycle-owned context and are cancelled during shutdown.
type Tracer ¶
type Tracer interface {
// Start creates a new span and returns a context that carries it.
Start(ctx context.Context, spanName string, attrs map[string]any) (context.Context, Span)
// SpanContextFromContext extracts the current span context from ctx.
SpanContextFromContext(ctx context.Context) SpanContext
// ContextWithSpanContext returns a new context carrying the provided span
// context. It is used to propagate trace ancestry to manager-owned task
// goroutines that run on a different base context.
ContextWithSpanContext(ctx context.Context, sc SpanContext) context.Context
}
Tracer abstracts span creation so the core package does not depend on a concrete OpenTelemetry implementation. A nil Tracer defaults to a no-op implementation.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package agentdocs renders a contract.Contract into provider-specific agent instruction documents, per ADR-0032 ("Agent-First Development Experience", docs/adr/adr-0032-agent-first-development-experience.md), P1: "Generate portable agent instruction files and repository templates" (Jira MOD-67).
|
Package agentdocs renders a contract.Contract into provider-specific agent instruction documents, per ADR-0032 ("Agent-First Development Experience", docs/adr/adr-0032-agent-first-development-experience.md), P1: "Generate portable agent instruction files and repository templates" (Jira MOD-67). |
|
Package contract defines a versioned, YAML-marshalable schema for a repository's agent contract (`modulex.agent.yaml`), per ADR-0032 ("Agent-First Development Experience"), P0: "Define and validate the Modulex agent repository contract" (Jira MOD-62).
|
Package contract defines a versioned, YAML-marshalable schema for a repository's agent contract (`modulex.agent.yaml`), per ADR-0032 ("Agent-First Development Experience"), P0: "Define and validate the Modulex agent repository contract" (Jira MOD-62). |
|
Package discovery scans a repository directory and reports what an AI coding agent needs to select a project and begin useful work, per ADR-0032 ("Agent-First Development Experience"), P0: "Add Modulex agent project discovery and command classification" (Jira MOD-64), step 1 of the ADR's "Standard agent workflow": `modulex agent discover` identifies the repository root, projects, modules, composition roots, instruction files, Make targets, CI workflows, and available indexes.
|
Package discovery scans a repository directory and reports what an AI coding agent needs to select a project and begin useful work, per ADR-0032 ("Agent-First Development Experience"), P0: "Add Modulex agent project discovery and command classification" (Jira MOD-64), step 1 of the ADR's "Standard agent workflow": `modulex agent discover` identifies the repository root, projects, modules, composition roots, instruction files, Make targets, CI workflows, and available indexes. |
|
examples
|
|
|
bootstrap
command
Package main demonstrates modulex/app's Run helper together with modulex.WithTypedConfig.
|
Package main demonstrates modulex/app's Run helper together with modulex.WithTypedConfig. |
|
deployment/consumer
Package consumer is a dependent feature module that consumes the notification service.
|
Package consumer is a dependent feature module that consumes the notification service. |
|
deployment/monolith
command
Package main demonstrates a monolithic Modulex deployment: every feature module is registered in-process and dependencies are wired directly to local implementations.
|
Package main demonstrates a monolithic Modulex deployment: every feature module is registered in-process and dependencies are wired directly to local implementations. |
|
deployment/notification
Package notification is the composition root for the notification feature.
|
Package notification is the composition root for the notification feature. |
|
deployment/notification/adapters
Package adapters provides infrastructure adapters for the notification feature.
|
Package adapters provides infrastructure adapters for the notification feature. |
|
deployment/notification/ports
Package ports defines the inbound and outbound contracts for the notification feature.
|
Package ports defines the inbound and outbound contracts for the notification feature. |
|
deployment/notification/service
Package service contains the core business logic for the notification feature.
|
Package service contains the core business logic for the notification feature. |
|
deployment/remote/consumer
command
Package main runs the consumer process in a remote deployment.
|
Package main runs the consumer process in a remote deployment. |
|
deployment/remote/grpc-consumer
command
Package main runs the consumer process against a remote gRPC notification service.
|
Package main runs the consumer process against a remote gRPC notification service. |
|
deployment/remote/notification-grpc-server
command
Package main runs the notification service as a standalone gRPC process.
|
Package main runs the notification service as a standalone gRPC process. |
|
deployment/remote/notification-server
command
Package main runs the notification service as a standalone process.
|
Package main runs the notification service as a standalone process. |
|
hexagonal
command
|
|
|
quickstart
command
Package main is a minimal runnable example of Modulex lifecycle orchestration.
|
Package main is a minimal runnable example of Modulex lifecycle orchestration. |
|
scaffolded-sample
Package scaffoldedsample was generated by tools/scaffold.
|
Package scaffoldedsample was generated by tools/scaffold. |
|
Package grpc provides an optional gRPC topology adapter for Modulex: a Modulex-managed server lifecycle, OpenTelemetry context propagation interceptors, a consistent domain-error-to-status mapping layer, and a health integration that reports a modulex.Manager's real registered health/readiness checks over the standard gRPC health-checking protocol.
|
Package grpc provides an optional gRPC topology adapter for Modulex: a Modulex-managed server lifecycle, OpenTelemetry context propagation interceptors, a consistent domain-error-to-status mapping layer, and a health integration that reports a modulex.Manager's real registered health/readiness checks over the standard gRPC health-checking protocol. |
|
internal
|
|
|
eventbustest
Package eventbustest provides shared test helpers for modulex.EventBus adapter implementations.
|
Package eventbustest provides shared test helpers for modulex.EventBus adapter implementations. |
|
Package modtest provides reusable, composable test helpers that verify a modulex.Module (or a small group of them) against Modulex's lifecycle contract: Init/Start ordering, rollback on failure, cancellation and deadline handling, health/readiness registration, and resource ownership.
|
Package modtest provides reusable, composable test helpers that verify a modulex.Module (or a small group of them) against Modulex's lifecycle contract: Init/Start ordering, rollback on failure, cancellation and deadline handling, health/readiness registration, and resource ownership. |
|
Package nats provides a Modulex EventBus adapter backed by NATS.
|
Package nats provides a Modulex EventBus adapter backed by NATS. |
|
Package patchapply implements atomic, content-based file mutation with rollback journaling for a single target directory, per ADR-0032 ("Agent-First Development Experience"), P2: "Add atomic patch application and rollback journaling" (Jira MOD-70).
|
Package patchapply implements atomic, content-based file mutation with rollback journaling for a single target directory, per ADR-0032 ("Agent-First Development Experience"), P2: "Add atomic patch application and rollback journaling" (Jira MOD-70). |
|
Package provenance defines a versioned, JSON-marshalable schema for recording what an AI coding agent did to a repository and why, per ADR-0032 ("Agent-First Development Experience"), P1: "Add provenance and handoff JSON".
|
Package provenance defines a versioned, JSON-marshalable schema for recording what an AI coding agent did to a repository and why, per ADR-0032 ("Agent-First Development Experience"), P1: "Add provenance and handoff JSON". |
|
Package review implements "agent diff review": checking a changeset for boundary violations, secret-shaped values, API compatibility breaks, protected-path edits, and missing changelog obligations, per ADR-0032 (Jira MOD-65).
|
Package review implements "agent diff review": checking a changeset for boundary violations, secret-shaped values, API compatibility breaks, protected-path edits, and missing changelog obligations, per ADR-0032 (Jira MOD-65). |
|
Package semindex diagnoses whether a semantic code index (CodeGraph, TokenSave, or any similar tool that builds an offline index of a repository) actually belongs to the git worktree an agent is currently working in, per ADR-0032 ("Agent-First Development Experience"), P2: "Add CodeGraph/TokenSave index-root validation and diagnostics" (Jira MOD-71).
|
Package semindex diagnoses whether a semantic code index (CodeGraph, TokenSave, or any similar tool that builds an offline index of a repository) actually belongs to the git worktree an agent is currently working in, per ADR-0032 ("Agent-First Development Experience"), P2: "Add CodeGraph/TokenSave index-root validation and diagnostics" (Jira MOD-71). |
|
tools
|
|
|
modboundary
module
|
|
|
Package verify maps a set of changed repository paths to focused verification checks, and pairs them with the repository's always-required full gates, per ADR-0032 ("Agent-First Development Experience"), P0: "Add focused agent verification with explicit skipped statuses" (Jira MOD-63), step 5 of the ADR's "Standard agent workflow":
|
Package verify maps a set of changed repository paths to focused verification checks, and pairs them with the repository's always-required full gates, per ADR-0032 ("Agent-First Development Experience"), P0: "Add focused agent verification with explicit skipped statuses" (Jira MOD-63), step 5 of the ADR's "Standard agent workflow": |
|
Package workerpool provides bounded, lifecycle-aware execution for work submitted by message adapters and other optional capabilities.
|
Package workerpool provides bounded, lifecycle-aware execution for work submitted by message adapters and other optional capabilities. |