Skip to main content
Kratos promotes clean architecture through clear separation of concerns and dependency injection. While the framework doesn’t mandate a specific DI tool, it’s commonly used with Wire for compile-time dependency injection.

Core Concepts

Dependency injection in Kratos follows these principles:

Constructor Injection

Dependencies are passed through constructor functions, making them explicit and testable.

Interface Contracts

Components depend on interfaces rather than concrete types, enabling flexibility and testing.

Functional Options

The framework uses the functional options pattern for configurable components.

No Global State

Avoid global variables; pass dependencies explicitly through the dependency graph.

The Options Pattern

Kratos extensively uses the functional options pattern for configuration:

How It Works

The pattern is implemented using:
  1. An unexported options struct holding configuration
  2. An Option function type that modifies options
  3. Constructor functions that accept variadic options
options.go:14-39
type Option func(o *options)

type options struct {
    id        string
    name      string
    version   string
    metadata  map[string]string
    endpoints []*url.URL

    ctx  context.Context
    sigs []os.Signal

    logger           log.Logger
    registrar        registry.Registrar
    registrarTimeout time.Duration
    stopTimeout      time.Duration
    servers          []transport.Server

    // Before and After funcs
    beforeStart []func(context.Context) error
    beforeStop  []func(context.Context) error
    afterStart  []func(context.Context) error
    afterStop   []func(context.Context) error
}

Example Option Functions

options.go:41-98
// ID with service id.
func ID(id string) Option {
    return func(o *options) { o.id = id }
}

// Name with service name.
func Name(name string) Option {
    return func(o *options) { o.name = name }
}

// Server with transport servers.
func Server(srv ...transport.Server) Option {
    return func(o *options) { o.servers = srv }
}

// Registrar with service registry.
func Registrar(r registry.Registrar) Option {
    return func(o *options) { o.registrar = r }
}

Benefits

New options can be added without breaking existing code.
Options not provided use default values.
Each option clearly states what it configures.
Options can be stored, passed around, and composed.

Dependency Injection Without Wire

You can manually wire dependencies without any DI framework:

Basic Manual Wiring

Manual Dependency Injection
package main

import (
    "github.com/go-kratos/kratos/v2"
    "github.com/go-kratos/kratos/v2/transport/http"
    "github.com/go-kratos/kratos/v2/transport/grpc"
)

// Data layer
type UserRepo struct {
    db *sql.DB
}

func NewUserRepo(db *sql.DB) *UserRepo {
    return &UserRepo{db: db}
}

// Business layer
type UserService struct {
    repo *UserRepo
}

func NewUserService(repo *UserRepo) *UserService {
    return &UserService{repo: repo}
}

// API layer
type UserHandler struct {
    service *UserService
}

func NewUserHandler(service *UserService) *UserHandler {
    return &UserHandler{service: service}
}

// Wire it all together
func main() {
    // Initialize dependencies
    db := initDatabase()
    repo := NewUserRepo(db)
    service := NewUserService(repo)
    handler := NewUserHandler(service)
    
    // Create HTTP server with handler
    httpSrv := http.NewServer(
        http.Address(":8000"),
    )
    registerHTTPHandlers(httpSrv, handler)
    
    // Create application
    app := kratos.New(
        kratos.Name("user-service"),
        kratos.Server(httpSrv),
    )
    
    if err := app.Run(); err != nil {
        panic(err)
    }
}

Using Interfaces

Depend on interfaces for better testability:
Interface-Based DI
// Define interfaces in business layer
type UserRepository interface {
    GetUser(ctx context.Context, id int) (*User, error)
    SaveUser(ctx context.Context, user *User) error
}

// Business logic depends on interface
type UserService struct {
    repo UserRepository // Interface, not concrete type
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

// Data layer implements interface
type MySQLUserRepo struct {
    db *sql.DB
}

func NewMySQLUserRepo(db *sql.DB) UserRepository {
    return &MySQLUserRepo{db: db}
}

func (r *MySQLUserRepo) GetUser(ctx context.Context, id int) (*User, error) {
    // Implementation
}

func (r *MySQLUserRepo) SaveUser(ctx context.Context, user *User) error {
    // Implementation
}
This makes testing easier with mock implementations:
Testing with Mocks
type MockUserRepo struct {}

func (m *MockUserRepo) GetUser(ctx context.Context, id int) (*User, error) {
    return &User{ID: id, Name: "Test User"}, nil
}

func (m *MockUserRepo) SaveUser(ctx context.Context, user *User) error {
    return nil
}

func TestUserService(t *testing.T) {
    mockRepo := &MockUserRepo{}
    service := NewUserService(mockRepo)
    
    // Test service with mock repository
    user, err := service.GetUser(context.Background(), 1)
    assert.NoError(t, err)
    assert.Equal(t, "Test User", user.Name)
}

Using Wire for DI

Kratos projects typically use Google’s Wire for compile-time dependency injection:

Wire Basics

Wire uses code generation to create the wiring code at compile time:
wire.go
//go:build wireinject

package main

import (
    "github.com/google/wire"
)

// wireApp creates the application with all dependencies wired
func wireApp(*conf.Config) (*kratos.App, func(), error) {
    panic(wire.Build(
        dataProvider,
        bizProvider,
        serviceProvider,
        serverProvider,
        newApp,
    ))
}

Provider Sets

Group related providers into sets:
data.go
package data

import "github.com/google/wire"

// ProviderSet is data providers.
var ProviderSet = wire.NewSet(
    NewDB,
    NewUserRepo,
    NewPostRepo,
)
biz.go
package biz

import "github.com/google/wire"

// ProviderSet is business providers.
var ProviderSet = wire.NewSet(
    NewUserUseCase,
    NewPostUseCase,
)
service.go
package service

import "github.com/google/wire"

// ProviderSet is service providers.
var ProviderSet = wire.NewSet(
    NewUserService,
    NewPostService,
)

Complete Wire Example

1

Define Providers

Create constructor functions for each component:
// data/data.go
func NewDB(cfg *conf.Data) *sql.DB {
    db, err := sql.Open("mysql", cfg.Database.Source)
    if err != nil {
        panic(err)
    }
    return db
}

func NewUserRepo(db *sql.DB) biz.UserRepo {
    return &userRepo{db: db}
}
2

Create Provider Sets

Group providers by layer:
// data/data.go
var ProviderSet = wire.NewSet(
    NewDB,
    NewUserRepo,
)

// biz/biz.go
var ProviderSet = wire.NewSet(
    NewUserUseCase,
)
3

Write Wire Injector

Define what you want Wire to build:
//go:build wireinject

package main

import (
    "github.com/google/wire"
    "your-app/internal/biz"
    "your-app/internal/data"
    "your-app/internal/service"
    "your-app/internal/server"
)

func wireApp(*conf.Server, *conf.Data) (*kratos.App, func(), error) {
    panic(wire.Build(
        data.ProviderSet,
        biz.ProviderSet,
        service.ProviderSet,
        server.ProviderSet,
        newApp,
    ))
}
4

Generate Wire Code

Run Wire to generate the wiring:
wire gen ./...
This creates wire_gen.go with actual wiring code.
5

Use in Main

Call the generated function:
func main() {
    // Load config
    cfg := loadConfig()
    
    // Wire creates everything
    app, cleanup, err := wireApp(&cfg.Server, &cfg.Data)
    if err != nil {
        panic(err)
    }
    defer cleanup()
    
    // Run the application
    if err := app.Run(); err != nil {
        panic(err)
    }
}

Wire with Cleanup

Wire supports cleanup functions for resource disposal:
Cleanup Pattern
func NewDB(cfg *conf.Data) (*sql.DB, func(), error) {
    db, err := sql.Open("mysql", cfg.Database.Source)
    if err != nil {
        return nil, nil, err
    }
    
    cleanup := func() {
        db.Close()
    }
    
    return db, cleanup, nil
}
Wire automatically chains cleanup functions:
app, cleanup, err := wireApp(cfg)
defer cleanup() // Calls all cleanup functions in reverse order

Server Registration Pattern

Kratos servers are registered using the options pattern:
Server Registration
// Create servers
func NewHTTPServer(handler *service.UserService) *http.Server {
    srv := http.NewServer(
        http.Address(":8000"),
    )
    // Register handlers
    v1.RegisterUserServiceHTTPServer(srv, handler)
    return srv
}

func NewGRPCServer(handler *service.UserService) *grpc.Server {
    srv := grpc.NewServer(
        grpc.Address(":9000"),
    )
    // Register services
    v1.RegisterUserServiceServer(srv, handler)
    return srv
}

// Provide to Wire
var ProviderSet = wire.NewSet(
    NewHTTPServer,
    NewGRPCServer,
)
Pass servers to the App:
func newApp(hs *http.Server, gs *grpc.Server) *kratos.App {
    return kratos.New(
        kratos.Name("user-service"),
        kratos.Server(hs, gs),
    )
}

Layered Architecture with DI

Kratos encourages a layered architecture:

Dependency Rules

The Dependency Rule: Dependencies point inward. Outer layers depend on inner layers, never the reverse.
  • Transport layer depends on Service layer
  • Service layer depends on Business layer
  • Business layer depends on Data interfaces (but not implementations)
  • Data layer implements Business layer interfaces

Example Layer Structure

// Define interfaces
type UserRepo interface {
    GetUser(context.Context, int64) (*User, error)
}

// Business logic
type UserUseCase struct {
    repo UserRepo
}

func NewUserUseCase(repo UserRepo) *UserUseCase {
    return &UserUseCase{repo: repo}
}

Best Practices

Constructors should only assign dependencies. Complex initialization logic should be in separate Init methods or lifecycle hooks.
Constructor functions should return interfaces when possible to reduce coupling.
If you have circular dependencies, your architecture needs refactoring. Consider introducing an interface or intermediate layer.
Instead of doing work in constructors, use BeforeStart hooks for complex initialization:
app := kratos.New(
    kratos.BeforeStart(func(ctx context.Context) error {
        return cache.Warmup(ctx)
    }),
)
Avoid hidden dependencies (like reading environment variables in constructors). Pass all configuration through parameters.

Testing with DI

Dependency injection makes testing straightforward:
Testing Example
func TestUserUseCase(t *testing.T) {
    // Create mock repository
    mockRepo := &MockUserRepo{
        users: map[int64]*biz.User{
            1: {ID: 1, Name: "Test User"},
        },
    }
    
    // Inject mock into use case
    uc := biz.NewUserUseCase(mockRepo)
    
    // Test business logic
    user, err := uc.GetUser(context.Background(), 1)
    assert.NoError(t, err)
    assert.Equal(t, "Test User", user.Name)
}
Use interface-based design to make your components testable without needing the actual infrastructure.

Next Steps

Architecture Overview

Learn about Kratos framework architecture

Application Lifecycle

Understand the App lifecycle management

Build docs developers (and LLMs) love