Features
Mikros features are modular, reusable units of cross-cutting behavior—logging, error handling, env validation and more. They integrate cleanly into the service lifecycle without bloating your business code.
Built-in Features
Mikros ships with several default features designed to cover common concerns. They are already ready to use, i.e., don't need to be enabled in service.toml
- logger – Structured logging with leveled messages and attributes. Available via
LoggerAPI
and accessed in code withs.Logger().Info(...)
. - errors – Rich, standardized error handling. Provides error constructors such as
Internal()
,NotFound()
, andPermissionDenied()
, supporting structured logging and custom codes. - env – Environment variable loading and validation.
- http – Provides methods to interact with the current HTTP response within a request handler.
- definition – Provides access to service metadata loaded from the
service.toml
file.
Why Use Features
- Decoupled concerns — Enable only what you need, keeping your service lean.
- Safe defaults — Disabled by default; features only activate when explicitly opted in.
- Lifecycle-aware — Features attach at the right phase: during Build, wrap Start/Run, and clean up during Shutdown.
- Uniform APIs — Logger and Error APIs are consistent across all services, improving onboarding and maintenance.
Using Features in your Service
You can inject the feature into your Service
struct using the mikros:"feature"
tag:
type MyService struct {
Logger logger_api.LoggerAPI `mikros:"feature"`
Errors errors_api.ErrorAPI `mikros:"feature"`
}
Then, within your service methods, call:
s.Logger.Info(ctx, "Starting operation", logger.String("kind", "startup"))
return s.Errors.Internal(err).WithAttributes(logger.String("op", "Init")).Submit(ctx)
This ensures structured logs and consistent error wrapping.
Hooking into the lifecycle
Features follow the Mikros lifecycle:
Phase | What happens, examples |
---|---|
Build | Feature checks config via CanBeInitialized(...) , then runs Initialize(...) to create resources. |
Run | Features may wrap handlers or schedule background tasks |
Shutdown | Features run Cleanup(...) to release resources and stop loops |
If disabled, features are no-ops and incur minimal overhead.
Creating a new feature
Custom features allow you to add cross-cutting capabilities that are specific to your services or organization.
To be supported by Mikros, a feature must implement the Feature
interface from the plugin
package.
Required interface
Every feature must implement:
type Feature interface {
// Checks if the feature should be used in this service.
CanBeInitialized(options *CanBeInitializedOptions) bool
// Initializes resources needed by the feature.
Initialize(ctx context.Context, options *InitializeOptions) error
// Informative fields to be logged at startup.
Fields() []logger.Attribute
// Provides name, enabled state, and internal info.
FeatureEntry
}
The embedded FeatureEntry requires:
type FeatureEntry interface {
UpdateInfo(info UpdateInfoEntry)
IsEnabled() bool
Name() string
}
Mikros provides a ready-to-use plugin.Entry struct that implements FeatureEntry. Embedding it into your feature struct is the recommended way to get logging, error helpers, and state management "for free."
All custom feature must be registered inside mikros at runtime to be available for usage. To do so, the API WithExternalFeatures
from Service
must be called with the proper set of custom features to register.
Example service.toml
[features.myfeature]
enabled = true
max_connections = 50
endpoint = "https://example.com"
Initialization options
The InitializeOptions
struct gives your feature everything it needs:
Logger
,Errors
,Env
– core Mikros APIs.Definitions
– parsed service.toml.ServiceContext
– shared runtime context.Dependencies
– other features your feature depends on.RunTimeFeatures
– hooks into runtime wiring.
Use these to configure your feature consistently with the rest of the service.
Optional interfaces
Mikros features can opt into extra behaviors by implementing the following interfaces. Use them only if your feature actually needs the capability—keep features small and composable.
FeatureController
Run tasks alongside the service and clean them up on shutdown.
- Why: background loops, caches, periodic jobs, or wiring into the service's root object.
- Lifecycle: Mikros calls
Start(ctx, srv)
after service initialization;Cleanup(ctx)
during shutdown. - Do: honor context cancellation; make
Cleanup
idempotent; bound goroutines with backoff/timeouts. - Don't: block
Start
indefinitely; spin unbounded workers.
type FeatureController interface {
Start(ctx context.Context, srv interface{}) error
Cleanup(ctx context.Context) error
}
FeatureSettings
Load feature-specific configuration from service.toml
.
- Why: typed config under [features.<name>] with validation and defaults.
- Lifecycle: called before Initialize; Mikros decodes into your struct.
- Config shape:toml
[features.myfeature] enabled = true # ... your keys here
- Tip: validate eagerly; fail fast on bad config.
type FeatureSettings interface {
Definitions(path string) (definition.ExternalFeatureEntry, error)
}
FeatureExternalAPI
Expose a typed API for services to consume (e.g., GreeterAPI
, CacheAPI
).
- Why: make feature capabilities available to handlers without leaking implementation.
- Lifecycle: Mikros calls
ServiceAPI()
afterInitialize
; injected into service fields annotated withmikros:"feature"
.
type FeatureExternalAPI interface {
ServiceAPI() interface{}
}
- Tip: keep APIs small and stable; prefer interfaces over concrete structs, implement it over the feature object itself.
The API returned by
ServiceAPI()
must always point to a valid value (non nil) so mikros can properly initialize the feature.
FeatureInternalAPI
Expose an API for framework or extension internals (not for services).
- Why: allow other Mikros internals or feature-to-feature integration.
- Audience: framework/extension code only—not your service handlers.
type FeatureInternalAPI interface {
FrameworkAPI() interface{}
}
FeatureTester
Integrate with Mikros’ test harness.
- Why: flip internal knobs for tests, set up fakes/mocks, and run feature-specific checks.
- Lifecycle in tests:
Setup(ctx, t)
→ test runs →Teardown(ctx, t)
. UseDoTest
for feature-owned assertions. - Tip: keep test state isolated; reset everything in Teardown.
type FeatureTester interface {
Setup(ctx context.Context, t *testing.Testing)
Teardown(ctx context.Context, t *testing.Testing)
DoTest(ctx context.Context, t *testing.Testing, serviceName service.Name) error
}
Choosing what to implement
- Only configuration →
FeatureSettings
- Only public API to services →
FeatureExternalAPI
- Needs background jobs →
FeatureController
- Needs framework integration →
FeatureInternalAPI
- Needs first-class testing →
FeatureTester
Use the smallest set of interfaces that does the job.
Example
This example loads config from service.toml
, exposes a typed API to services, logs during init, and cleans up on shutdown.
A tiny feature that greets users and demonstrates the recommended structure.
Feature definitions
[features.hello]
enabled = true
prefix = "👋 " # optional; defaults to "Hello, "
uppercase = false # optional
Implementation
package hello
import (
"context"
"strings"
"github.com/mikros-dev/mikros/apis/features/env"
merrors "github.com/mikros-dev/mikros/apis/features/errors"
logger_api "github.com/mikros-dev/mikros/apis/features/logger"
"github.com/mikros-dev/mikros/components/definition"
"github.com/mikros-dev/mikros/components/logger"
"github.com/mikros-dev/mikros/components/plugin"
)
// GreeterAPI is the public, typed API that services will use.
type GreeterAPI interface {
Greet(ctx context.Context, name string) string
}
// cfg holds the feature settings loaded from [features.hello] in service.toml.
type cfg struct {
IsEnabled bool `toml:"enabled"`
Prefix string `toml:"prefix"`
Uppercase bool `toml:"uppercase"`
}
func (c *cfg) Enabled() bool {
return c.IsEnabled
}
func (c *cfg) Validate() error {
return nil
}
func (c *cfg) defaults() {
if c.Prefix == "" {
c.Prefix = "Hello, "
}
}
// HelloFeature implements plugin.Feature and exposes GreeterAPI.
type HelloFeature struct {
plugin.Entry // embeds FeatureEntry: Name/IsEnabled/Error helpers
log logger_api.LoggerAPI
errs merrors.ErrorAPI
env env.EnvAPI
conf cfg
}
// Ensure interface compliance.
var _ plugin.Feature = (*HelloFeature)(nil)
var _ plugin.FeatureSettings = (*HelloFeature)(nil)
var _ plugin.FeatureExternalAPI = (*HelloFeature)(nil)
// New returns a new HelloFeature (disabled until UpdateInfo runs).
func New() *HelloFeature {
return &HelloFeature{}
}
// --- plugin.Feature ---
func (h *HelloFeature) CanBeInitialized(_ *plugin.CanBeInitializedOptions) bool {
// If disabled in service.toml, Mikros will not Initialize() it.
return h.conf.Enabled()
}
func (h *HelloFeature) Initialize(ctx context.Context, opt *plugin.InitializeOptions) error {
h.log = opt.Logger
h.errs = opt.Errors
h.env = opt.Env
h.conf.defaults()
h.log.Info(ctx, "hello feature initialized",
logger.String("feature", h.Name()),
logger.String("prefix", h.conf.Prefix),
logger.Any("uppercase", h.conf.Uppercase),
)
return nil
}
func (h *HelloFeature) Fields() []logger_api.Attribute {
return []logger_api.Attribute{
logger.String("feature", "hello"),
logger.Any("enabled", h.IsEnabled()),
}
}
// --- plugin.FeatureSettings ---
// Load [features.hello] into our cfg struct. Mikros will call this before
// Initialize().
func (h *HelloFeature) Definitions(path string) (definition.ExternalFeatureEntry, error) {
// The framework will decode [features.hello] into this struct.
type definitions struct {
Features struct {
Config cfg `toml:"hello"`
} `toml:"features"`
}
var defs definitions
if err := definition.ParseExternalDefinitions(path, &defs); err != nil {
return nil, err
}
h.conf = defs.Features.Config
return &h.conf, nil
}
// --- plugin.FeatureExternalAPI ---
// Expose a typed API that services can consume via feature injection.
func (h *HelloFeature) ServiceAPI() interface{} {
return h
}
// implements GreeterAPI.
func (h *HelloFeature) Greet(_ context.Context, name string) string {
if !h.IsEnabled() {
return ""
}
msg := h.conf.Prefix + name
if h.conf.Uppercase {
msg = strings.ToUpper(msg)
}
return msg
}
// Feature returns a set containing the custom feature to be registered inside
// mikros.
func Feature() *plugin.FeatureSet {
features := plugin.NewFeatureSet()
features.Register("hello", New())
return features
}
Using the feature in a service
Using Mikros’ feature injection, add the API field to your service:
package myservice
import (
"context"
"github.com/mikros-dev/mikros/apis/features/logger"
"github.com/your-org/your-repo/hello" // import your feature package
)
type Service struct {
Logger logger.LoggerAPI `mikros:"feature"`
Greeter hello.GreeterAPI `mikros:"feature"` // resolved from HelloFeature.ServiceAPI()
}
func (s *Service) Handle(ctx context.Context, user string) {
s.Logger.Info(ctx, "handling request")
msg := s.Greeter.Greet(ctx, user)
s.Logger.Info(ctx, "greeted user", logger.String("message", msg))
}
func main() {
svc := mikros.NewService(&options.NewServiceOptions{
// Do service initialization here...
}).WithExternalFeatures(hello.Feature()) // Register our feature
svc.Start(&service{})
}
What Mikros wires for you
- Reads
[features.hello]
fromservice.toml
and decodes intocfg
. - Calls
UpdateInfo
on your embeddedplugin.Entry
. - Runs
CanBeInitialized
→Initialize
. - Injects your
GreeterAPI
into the service (viaServiceAPI()
). - On shutdown, Mikros calls feature cleanup paths.