Auto Configuration

Introduction

Hiboot auto-configuration automatically configures your application based on the packages you import. When you import a starter package, Hiboot detects it and configures the necessary components for dependency injection.

This approach is inspired by Spring Boot’s auto-configuration mechanism, adapted for Go’s package system.

How Auto-Configuration Works

  1. Import a starter package in your application
  2. The starter’s init() function registers its configuration
  3. Hiboot scans registered configurations at startup
  4. Configuration methods are called to create injectable components
  5. Components become available for dependency injection

Built-in Starters

Hiboot provides several built-in starters:

Starter Package Description
Actuator github.com/hidevopsio/hiboot/pkg/starter/actuator Health checks and metrics
Logging github.com/hidevopsio/hiboot/pkg/starter/logging Structured logging
JWT github.com/hidevopsio/hiboot/pkg/starter/jwt JWT authentication
Locale github.com/hidevopsio/hiboot/pkg/starter/locale Internationalization

Using a Starter

package main

import (
	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/app/web"
	"github.com/hidevopsio/hiboot/pkg/starter/actuator"
	"github.com/hidevopsio/hiboot/pkg/starter/logging"
)

func main() {
	web.NewApplication().
		SetProperty(app.ProfilesInclude, actuator.Profile, logging.Profile).
		Run()
}

Creating Your Own Starter

A starter typically contains:

  • Configuration struct - Embeds app.Configuration and defines factory methods
  • Properties struct - Holds configuration values from YAML files
  • Service structs - The actual components to be injected

Project Structure

starter/
└── myservice/
    ├── autoconfigure.go    # Configuration and registration
    ├── properties.go       # Properties struct
    ├── service.go          # Service implementation
    └── doc.go              # Package documentation

Step 1: Define Properties

Properties hold configuration values that can be customized in application.yml:

package myservice

// Properties holds the configuration for MyService
type Properties struct {
	Enabled  bool   `json:"enabled" default:"true"`
	Endpoint string `json:"endpoint" default:"http://localhost:8080"`
	Timeout  int    `json:"timeout" default:"30"`
	MaxRetry int    `json:"max_retry" default:"3"`
}

Step 2: Define the Service

package myservice

import "fmt"

// Service is the interface for MyService
type Service interface {
	DoSomething(data string) (string, error)
}

// serviceImpl is the implementation
type serviceImpl struct {
	endpoint string
	timeout  int
	maxRetry int
}

func newService(endpoint string, timeout, maxRetry int) Service {
	return &serviceImpl{
		endpoint: endpoint,
		timeout:  timeout,
		maxRetry: maxRetry,
	}
}

func (s *serviceImpl) DoSomething(data string) (string, error) {
	return fmt.Sprintf("Processed: %s", data), nil
}

Step 3: Create the Configuration

package myservice

import "github.com/hidevopsio/hiboot/pkg/app"

const (
	// Profile is the profile name for this starter
	Profile = "myservice"
)

// configuration is the auto-configuration struct
type configuration struct {
	app.Configuration

	// Properties will be populated from application.yml
	// The field name should match the mapstructure tag
	MyServiceProperties Properties `mapstructure:"myservice"`
}

func init() {
	// Register the configuration
	app.Register(newConfiguration)
}

func newConfiguration() *configuration {
	return &configuration{}
}

// Service creates and returns the MyService instance
// This method is called by Hiboot during startup
func (c *configuration) Service() Service {
	return newService(
		c.MyServiceProperties.Endpoint,
		c.MyServiceProperties.Timeout,
		c.MyServiceProperties.MaxRetry,
	)
}

Step 4: Configure in application.yml

app:
  profiles:
    include:
    - myservice

myservice:
  enabled: true
  endpoint: "https://api.example.com"
  timeout: 60
  max_retry: 5

Step 5: Use in Your Application

package controller

import (
	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/at"
	"github.com/hidevopsio/hiboot/pkg/model"

	"myapp/starter/myservice"
)

type myController struct {
	at.RestController
	at.RequestMapping `value:"/api"`

	service myservice.Service
}

func init() {
	app.Register(newMyController)
}

func newMyController(service myservice.Service) *myController {
	return &myController{
		service: service,
	}
}

func (c *myController) Post(request *DataRequest) (model.Response, error) {
	result, err := c.service.DoSomething(request.Data)
	response := new(model.BaseResponse)
	response.SetData(result)
	return response, err
}

Advanced Configuration

Conditional Configuration

Use struct tags to control when configuration is applied:

type configuration struct {
	app.Configuration `after:"databaseConfiguration"`

	// This configuration loads after databaseConfiguration
}

Available tags:

Tag Description
after:"name" Load after the specified configuration
missing:"name" Only load if the specified configuration is missing

Multiple Bean Methods

A configuration can provide multiple injectable components:

type configuration struct {
	app.Configuration

	Props Properties `mapstructure:"myservice"`
}

// Primary service
func (c *configuration) Service() Service {
	return newService(c.Props)
}

// Client for external API
func (c *configuration) Client() *Client {
	return newClient(c.Props.Endpoint)
}

// Repository with database connection
func (c *configuration) Repository(db *Database) Repository {
	return newRepository(db)
}

Injecting Dependencies in Configuration

Configuration methods can have parameters that will be injected:

// Repository depends on Database which is provided by another starter
func (c *configuration) Repository(db *gorm.DB) Repository {
	return &gormRepository{db: db}
}

// Service depends on Repository
func (c *configuration) Service(repo Repository) Service {
	return &serviceImpl{repository: repo}
}

Best Practices

  1. Use interfaces: Define service interfaces for better testability
  2. Provide defaults: Use default:"" tags for sensible defaults
  3. Document properties: Clearly document what each property does
  4. Keep it focused: Each starter should provide one cohesive feature
  5. Handle errors gracefully: Return meaningful errors during initialization

What’s Next?