Skip to content

New service type

Mikros supports four built-ins (grpc, http, native, script).

When your runtime model doesn’t fit those shapes, you can add a custom service type that behaves like a first-class citizen: it participates in the same lifecycle, uses the same logging/error/env APIs, validates configuration early, and shuts down gracefully.

This page explains the process and provides a minimal, production-quality example.

What it means to add a service type

A custom service kind is a Go type that implements the plugin.Service interface and (optionally) plugin.ServiceSettings to load its own [services.<kind>] configuration from service.toml.

go
// Required
type Service interface {
    Name() string
    Info() []flogger.Attribute
    Initialize(ctx context.Context, opt *ServiceOptions) error
    Run(ctx context.Context, srv interface{}) error
    Stop(ctx context.Context) error
}

// Optional
type ServiceSettings interface {
    Definitions(path string) (definition.ExternalServiceEntry, error)
}

Mikros passes a fully-populated ServiceOptions to your implementation:

go
type ServiceOptions struct {
    Port           service.ServerPort         // If your kind binds a port (servers)
    Type           definition.ServiceType     // The declared type (e.g., "cronjob")
    Name           service.Name               // Logical service name (from service.toml)
    Product        string
    Logger         flogger.LoggerAPI          // Structured logger
    Errors         ferrors.ErrorAPI           // Standardized error API
    ServiceContext *mcontext.ServiceContext   // Shared runtime context
    Tags           map[string]string          // Runtime tags
    Service        options.ServiceOptions     // Framework-level knobs
    Definitions    *definition.Definitions    // Parsed service.toml
    Features       *FeatureSet                // Resolved features
    ServiceHandler interface{}                // Your handler/root object (if any)
    Env            fenv.EnvAPI                // Env loader/validator
}

The Run method receives an interface{} object which points to the service main structure, so the service implementation can deal with somehow, if needed.

Configuration shape

Declare your kind in types and place your settings under [services.<kind>]. (If your kind is not a server, omit the :port suffix; Go supports type:port for server kinds.)

toml
name     = "jobs"
language = "go"
product  = "SDS"
version  = "0.1.0"

types = ["cronjob"]

[services.cronjob]
frequency       = "weekly"
scheduled_times = ["02:00", "14:00"]
days            = ["monday"]

Internally, Mikros validates that your type is supported. If you’re adding a brand-new string (e.g., "cronjob"), make sure your bootstrap path adds it to supported types before validation (see your application entrypoint).

Logging, errors, and validation

  • Use opt.Logger for structured logs. Prefer attributes (key/value) over ad-hoc strings for better searchability.
  • Use opt.Errors to wrap unexpected failures into framework errors (Internal, NotFound, etc.) with attributes.
  • Validate vigorously in Definitions.Validate() and Initialize; fail fast rather than letting bad config reach the hot path.

Health & readiness

  • If your kind binds sockets or depends on downstreams, only report readiness after the dependency graph is up.
  • Native kinds that do background work should expose internal health signals (counters, last success timestamp) via your observability features.
  • Scripts don’t need liveness; exit codes suffice.

Common patterns

  • Server kinds (websocket, custom TCP): respect ServiceOptions.Port. Bind listeners in Initialize or Run, and block Run while serving; Stop should close listeners and drain.
  • Background kinds (schedulers, consumers): keep Run non-blocking with a cancelable loop; Stop should cancel and drain quickly.
  • Hybrid: if your service also has http in types, put shared resources in Initialize and coordinate shutdown via the provided context.

Example: cronjob service type

A small service type that runs scheduled tasks at configured times. Here we demonstrate:

  • Implementing plugin.Service
  • Loading typed config with plugin.ServiceSettings
  • Graceful start/stop using context.Context
  • Logging and error APIs

Define typed settings

Implement definition.ExternalServiceEntry so Mikros can validate your block.

go
package cronjob

import (
	"fmt"
)

// Config under: [services.cronjob]
type Defs struct {
	Frequency      string   `toml:"frequency"`
	ScheduledTimes []string `toml:"scheduled_times"` // "HH:MM" (24h)
	Days           []string `toml:"days"`            // ["monday", ...]
}

func (d *Defs) Name() string {
	return "cronjob"
}

// Validate is called by Mikros after decoding.
func (d *Defs) Validate() error {
	if d.Frequency == "" {
		return fmt.Errorf("frequency is required")
	}

	if len(d.ScheduledTimes) == 0 {
		return fmt.Errorf("at least one scheduled time is required")
	}

	if len(d.Days) == 0 {
		return fmt.Errorf("at least one day is required")
	}

	return nil
}

Implement the service type

go
package cronjob

import (
    "context"
    "time"
	"strings"

    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"
)

// An options structure example to be used the service is being created.
type Options struct {}

func (o *Options) Kind() definition.ServiceType {
	t := definition.CreateServiceType("cronjob")
	return t
}

type Service struct {
    defs  Defs
    log   logger_api.LoggerAPI
    errs  merrors.ErrorAPI

    // internal state for shutdown
    cancel context.CancelFunc
}

// Ensure interface compliance.
var _ plugin.Service = (*Service)(nil)
var _ plugin.ServiceSettings = (*Service)(nil)

func New() *Service {
	return &Service{}
}

func (s *Service) Name() string {
	return "cronjob"
}

func (s *Service) Info() []logger_api.Attribute {
    return []logger_api.Attribute{
        logger.String("service.kind", s.Name()),
        logger.String("frequency", s.defs.Frequency),
    }
}

// Load [services.cronjob] into s.defs as typed configuration.
func (s *Service) Definitions(path string) (definition.ExternalServiceEntry, error) {
	type definitions struct {
		Services struct {
			Config Defs `toml:"cronjob"`
		} `toml:"services"`
	}

	var defs definitions
	if err := definition.ParseExternalDefinitions(path, &defs); err != nil {
		return nil, err
	}

	s.defs = defs.Services.Config
    return &s.defs, nil
}

func (s *Service) Initialize(ctx context.Context, opt *plugin.ServiceOptions) error {
    s.log = opt.Logger
    s.errs = opt.Errors

    s.log.Info(ctx, "cronjob initialize",
        logger.String("frequency", s.defs.Frequency),
        logger.Any("times", len(s.defs.ScheduledTimes)),
        logger.Any("days", len(s.defs.Days)),
    )

    return nil
}

func (s *Service) Run(ctx context.Context, _ interface{}) error {
    // Create a child context we can cancel from Stop()
    runCtx, cancel := context.WithCancel(ctx)
    s.cancel = cancel

    // Start scheduler loop (non-blocking example; adjust as needed)
    go s.scheduler(runCtx)

    // Block until parent context is done (Mikros handles signals)
    <-runCtx.Done()
    return nil
}

func (s *Service) Stop(ctx context.Context) error {
    s.log.Info(ctx, "cronjob stopping")
    if s.cancel != nil {
        s.cancel()
    }

	// Drain/cleanup here if you hold resources
    return nil
}

// --- internals ---

func (s *Service) scheduler(ctx context.Context) {
    // A minimal loop. In real code, compute next fire time precisely.
    ticker := time.NewTicker(1 * time.Minute)
    defer ticker.Stop()

    s.log.Info(ctx, "cronjob scheduler started")

    for {
        select {
        case <-ctx.Done():
            s.log.Info(ctx, "cronjob scheduler stopped")
            return
        case <-ticker.C:
            // Decide if "now" matches one of the configured times/days, then run the job.
            if s.shouldRun(time.Now()) {
                s.runJob(ctx)
            }
        }
    }
}

func (s *Service) shouldRun(now time.Time) bool {
    // Simplified: check weekday and HH:MM matches any configured time.
    // Implement exact matching and time zone handling for production.
    var (
		weekday = strings.ToLower(now.Weekday().String()) // e.g., "monday"
	    hhmm 	= now.Format("15:04")
    	dayOK, timeOK bool
	)

	for _, d := range s.defs.Days {
        if d == weekday {
            dayOK = true
            break
        }
    }

	for _, t := range s.defs.ScheduledTimes {
        if t == hhmm {
            timeOK = true
            break
        }
    }

	return dayOK && timeOK
}

func (s *Service) runJob(ctx context.Context) {
    // Your job logic here. Always honor ctx and bound latencies.
    s.log.Info(ctx, "cronjob tick fired")

	// On unexpected errors, wrap with the framework error API:
    // err := doWork(ctx)
    // if err != nil {
    //     _ = s.errs.Internal(err).Submit(ctx)
    // }
}

// Register a set containing the custom service type to be registered inside
// mikros.
func Register() *plugin.ServiceSet {
	services := plugin.NewServiceSet()
	services.Register(New())
	return services
}

Notes

  • Run may block (servers) or run non-blocking and return immediately while workers run in the background. Choose the model that makes sense for your kind; always honor ctx.Done().
  • Keep Stop idempotent and lightweight—cancel, then let loops finish quickly. Use timeouts where appropriate.

Using the new service type

Using Mikros’ extension injection, add the new service type to your service:

go
package myservice

import (
    "context"

	"github.com/mikros-dev/mikros"
	"github.com/mikros-dev/mikros/components/options"

    "github.com/your-org/your-repo/cronjob" // import your service package
)

type Service struct {}

func main() {
    svc := mikros.NewService(&options.NewServiceOptions{
		Service: map[string]options.ServiceOptions{
			"cronjob": &cronjob.Options{},
		},
    }).WithExternalServices(cronjob.Register()) // Register the new service

    svc.Start(&service{})
}

Mikros is MIT/MPL-2.0 licensed