Tools That Help in Understanding Code Structure in jframe
The jframe architecture relies on three primary tools to help in understanding code structure: the Engine that manages module lifecycle, the Hub that provides dependency injection, and the standardized Module interface that ensures consistent plugin architecture.
Navigating a modular framework requires clear architectural boundaries and intuitive exploration utilities. In jframe, a lightweight Go framework maintained by juanjitech, specific design patterns and core components serve as tools that help in understanding code structure. These utilities provide a clear map of how modules interact, how dependencies flow through the system, and where configuration lives, making the codebase straightforward to explore and extend.
Core Architectural Components
The architecture centers on a minimal kernel that glues together independent modules through well-defined interfaces located in the core/kernel directory.
The Engine
The Engine serves as the central orchestrator and primary entry point for the application. Defined in core/kernel/kernel.go at lines 15-24, the Engine struct holds the global context, the dependency injection container, and a map of registered modules:
type Engine struct {
// ... modules map[string]Module ...
}
The Engine provides the RegMod method (lines 28-40) for adding modules to the global registry while checking for name collisions, and StartModule (lines 55-85) for initializing module configurations and executing lifecycle hooks.
The Hub and Dependency Injection
The Hub acts as the bridge between the Engine and individual modules, providing scoped dependency injection capabilities. Located in core/kernel/module.go at lines 10-13, the Hub struct wraps the injector and carries a scoped logger:
type Hub struct {
inject.Injector
Log *zap.SugaredLogger
}
The framework uses github.com/juanjiTech/inject/v2 for dependency management. Modules register values using Hub.Map and retrieve them through Hub.Value, Hub.Invoke, or Hub.Load. A concrete example appears in mod/example/mod.go at lines 38-45, demonstrating how modules consume dependencies without direct imports, maintaining loose coupling.
The Module Interface Contract
The Module interface standardizes how plugins integrate with the kernel, defined in core/kernel/module.go at lines 15-24. This contract requires implementations to provide:
Name() string– unique module identifierConfig() any– optional configuration structure- Lifecycle hooks:
PreInit,Init,PostInit,PreStart,Start,PreStop,Stop
To reduce boilerplate, the framework provides UnimplementedModule (lines 42-81), a default implementation with no-op methods. Developers embed this struct and override only the hooks they need, making module creation predictable and the codebase easier to scan for functionality.
Module Lifecycle and Registration Tools
Understanding how modules enter and exit the system provides additional clarity when exploring the codebase.
Registration and Discovery
Modules enter the system through explicit registration. The cmd/server/modList/list.go file (lines 15-24) contains the ModList variable, an explicit slice of built-in modules that ship with the framework:
var ModList = []kernel.Module{ ... }
This centralized list serves as a manifest, making it immediately obvious which modules are active in a given build. Custom modules register via engine.RegMod(&mymod.Mod{}) before initialization, as shown in the Engine's registration logic at core/kernel/kernel.go lines 28-40.
Lifecycle Hooks
The Engine orchestrates module initialization through a predictable sequence defined in core/kernel/kernel.go lines 55-85. During StartModule, the Engine:
- Unmarshals module-specific configuration using Viper into the struct returned by
Module.Config() - Invokes
PreInit,Init, andPostInithooks sequentially - Launches
Startin a goroutine afterPreStart
This standardized lifecycle means developers can predict exactly where initialization logic resides in any module file, typically looking for Init or Start methods when tracing execution flow.
Configuration Management
Configuration loading relies on Viper to unmarshal module-specific blocks. The Engine uses reflection to build a temporary configuration struct during StartModule (lines 55-85), then passes it to viper.Unmarshal. Modules define their configuration requirements through the Config() method, returning a pointer to a struct with mapstructure tags, ensuring type-safe configuration without global state.
Practical Code Examples
These patterns come together in concrete implementations that demonstrate the framework's navigability.
Creating a Custom Module
A minimal module implements only the required hooks by embedding UnimplementedModule. This example shows the standard structure found throughout the repository:
package mymod
import (
"github.com/juanjiTech/jframe/core/kernel"
"sync"
)
type Mod struct{ kernel.UnimplementedModule }
func (m *Mod) Name() string { return "mymod" }
// optional: expose custom configuration
func (m *Mod) Config() any { return &struct{ Port int `mapstructure:"port"` }{} }
func (m *Mod) Init(h *kernel.Hub) error {
// expose a value to other modules
h.Map("hello from mymod")
return nil
}
func (m *Mod) Stop(wg *sync.WaitGroup, _ context.Context) error {
defer wg.Done()
return nil
}
This pattern appears consistently in mod/example/mod.go and other module directories, making it easy to recognize module boundaries when browsing the codebase.
Registering and Consuming Dependencies
Modules interact through the Hub's dependency injection rather than direct imports. Registration occurs during initialization:
engine := kernel.New()
engine.RegMod(&mymod.Mod{}) // registration performed once at startup
engine.Init()
engine.StartModule()
engine.Serve()
Consumption uses several equivalent patterns, as demonstrated in mod/example/mod.go lines 38-45:
func (c *SomeMod) Load(h *kernel.Hub) error {
var greeting string
// three equivalent ways to fetch the value registered by `mymod`
if err := h.Load(&greeting); err != nil { return err }
// or
greeting = h.Value(reflect.TypeOf("")).String()
// or
_, _ = h.Invoke(func(s string) { fmt.Println(s) })
fmt.Println("Got:", greeting) // → "Got: hello from mymod"
return nil
}
This indirection ensures modules remain loosely coupled, and developers can trace data flow by searching for Hub.Map calls and Hub.Load invocations across the repository.
Summary
The jframe framework provides several architectural tools that help in understanding code structure:
- Engine: Central orchestrator in
core/kernel/kernel.gothat manages module registration and lifecycle - Hub: Dependency injection wrapper in
core/kernel/module.gothat enables loose coupling between modules - Module Interface: Standardized contract in
core/kernel/module.gothat makes plugin architecture predictable - UnimplementedModule: Default implementation that reduces boilerplate and clarifies which methods are overridden
- Explicit Module List: Centralized registry in
cmd/server/modList/list.gothat documents active components
These components create a layered architecture where the bootstrap sequence, dependency flow, and module boundaries are explicitly defined, making the codebase straightforward to navigate and extend.
Frequently Asked Questions
What is the role of the Engine in jframe?
The Engine acts as the central orchestrator for the entire application. Defined in core/kernel/kernel.go, it maintains the global context, dependency injection container, and a map of registered modules. It handles module registration via RegMod and orchestrates the initialization sequence through StartModule, making it the primary entry point for understanding how the application boots and coordinates its components.
How does the Hub facilitate dependency injection between modules?
The Hub provides a scoped dependency injection mechanism that keeps modules loosely coupled. Located in core/kernel/module.go, the Hub wraps github.com/juanjiTech/inject/v2 and provides methods like Map for registering values and Load, Value, or Invoke for retrieving them. This allows modules to share data without direct imports, making dependencies traceable by searching for Hub method calls rather than import statements.
What makes the Module interface essential for code navigation?
The Module interface standardizes how plugins integrate with the kernel, making the codebase predictable and easy to scan. Defined in core/kernel/module.go, it requires implementations to provide Name(), Config(), and lifecycle hooks like Init and Start. By embedding UnimplementedModule, developers only override relevant methods, creating consistent patterns across the codebase. This standardization means you can quickly identify module boundaries by looking for structs embedding UnimplementedModule or implementing the Module interface.
Where can I find the list of active modules in a jframe application?
Active modules are explicitly listed in cmd/server/modList/list.go within the ModList variable. This slice contains all built-in modules that ship with the framework, serving as a centralized manifest. When exploring the codebase, this file acts as an index to determine which components are loaded by default, and custom modules are typically added to this list or registered via engine.RegMod in the bootstrap sequence.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →