Why I Built Things-Kit: A Spring Boot Alternative for Go

I built Things-Kit, a modular microservice framework for Go that brings Spring Boot’s developer experience while staying true to Go’s philosophy. It’s built on Uber Fx and provides composable modules for common infrastructure concerns.

My …


This content originally appeared on DEV Community and was authored by Avicienna Ulhaq

I built Things-Kit, a modular microservice framework for Go that brings Spring Boot's developer experience while staying true to Go's philosophy. It's built on Uber Fx and provides composable modules for common infrastructure concerns.

My Journey with Microservices

Like many developers, I've built my share of microservices. Started with monoliths, moved to microservices, learned the hard way about distributed systems, and eventually found patterns that work.

But here's the thing: every time I started a new Go microservice, I found myself in the same frustrating cycle.

The Groundhog Day Problem

Picture this: It's Monday morning. You're starting a new microservice. You know exactly what you need to build, but first...

You need to set up the HTTP server:

router := gin.Default()
router.Use(middleware.Recovery())
router.Use(middleware.Logger())
// ... 20 more lines of setup

Then wire up dependencies:

logger := zap.NewProduction()
defer logger.Sync()

db, err := sql.Open("postgres", connectionString)
if err != nil {
    logger.Fatal("failed to connect", zap.Error(err))
}
defer db.Close()

cache := redis.NewClient(&redis.Options{
    Addr: config.RedisAddr,
})
defer cache.Close()

// Create services
userService := service.NewUserService(logger, db, cache)
orderService := service.NewOrderService(logger, db, cache)

// Create handlers
userHandler := handler.NewUserHandler(userService)
orderHandler := handler.NewOrderHandler(orderService)

// Register routes
router.GET("/users/:id", userHandler.GetUser)
router.POST("/orders", orderHandler.CreateOrder)
// ... more routes

Don't forget configuration:

viper.SetConfigName("config")
viper.AddConfigPath(".")
if err := viper.ReadInConfig(); err != nil {
    log.Fatal(err)
}

type Config struct {
    HTTPPort  int
    DBHost    string
    DBPort    int
    RedisAddr string
    LogLevel  string
    // ... 20 more fields
}

var config Config
if err := viper.Unmarshal(&config); err != nil {
    log.Fatal(err)
}

And of course, graceful shutdown:

srv := &http.Server{
    Addr:    fmt.Sprintf(":%d", config.HTTPPort),
    Handler: router,
}

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        logger.Fatal("server failed", zap.Error(err))
    }
}()

<-quit
logger.Info("shutting down server...")

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := srv.Shutdown(ctx); err != nil {
    logger.Fatal("server forced to shutdown", zap.Error(err))
}

logger.Info("server exited")

By now, you've written 100+ lines of code and you haven't even started on your actual business logic!

Sound familiar? 😅

The Copy-Paste Trap

My first solution? "I'll just copy from my last project!"

But then:

  • Different projects used different patterns
  • Some had better error handling
  • Others had better testing setups
  • Configuration structures diverged
  • Updates to one project didn't propagate

I was maintaining the same infrastructure code in 15+ microservices, each slightly different. When I found a bug or wanted to improve something, I had to update it everywhere.

There had to be a better way.

Learning from Spring Boot

I come from a Java background (yes, I said it on a Go blog 😄). One thing Spring Boot does brilliantly is this:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

That's it. HTTP server? Check. Dependency injection? Check. Configuration? Check. Database connections? Check. Logging? Check.

But here's what I loved most: It wasn't magic. It was conventions over configuration with escape hatches everywhere. Need custom behavior? Implement an interface. Want a different database? Swap the dependency.

Could we bring this experience to Go without sacrificing Go's simplicity?

The Go Challenge

Go isn't Java. And that's great! Go's philosophy is different:

  • Simplicity over cleverness
  • Explicit over implicit
  • Composition over inheritance
  • Small, focused standard library

Any solution had to respect these principles. No magic. No reflection-heavy frameworks. No "enterprise" complexity.

But I still wanted:

  • ✅ Minimal boilerplate
  • ✅ Clear dependency injection
  • ✅ Testable code
  • ✅ Swappable components
  • ✅ Graceful lifecycle management

Enter Uber Fx

Then I discovered Uber Fx.

Fx is a dependency injection framework from Uber (yes, the ride-sharing company uses this in production). It's not magic - it's just a clever use of Go's type system and reflection to wire dependencies.

func main() {
    fx.New(
        fx.Provide(NewLogger),
        fx.Provide(NewDatabase),
        fx.Provide(NewUserService),
        fx.Invoke(RunServer),
    ).Run()
}

This felt right! But you still had to:

  • Write all the New* functions
  • Handle lifecycle hooks manually
  • Set up configuration loading
  • Manage graceful shutdown
  • Create server instances

Every. Single. Time.

The Solution: Things-Kit

What if we could package these common patterns into reusable modules?

Here's what the same microservice looks like with Things-Kit:

package main

import (
    "github.com/things-kit/app"
    "github.com/things-kit/module/httpgin"
    "github.com/things-kit/module/logging"
    "github.com/things-kit/module/sqlc"
    "github.com/things-kit/module/viperconfig"
    "myapp/internal/user"
)

func main() {
    app.New(
        viperconfig.Module,  // Configuration
        logging.Module,      // Logging
        sqlc.Module,         // Database
        httpgin.Module,      // HTTP server
        user.Module,         // Your business logic
    ).Run()
}

That's it.

No HTTP server setup. No manual wiring. No graceful shutdown code. No configuration loading boilerplate.

But unlike magic frameworks, you can see exactly what's happening. Each module is just an Fx module providing certain types:

// Inside the logging module
var Module = fx.Module("logging",
    fx.Provide(NewLogger),
)

func NewLogger(config Config) (log.Logger, error) {
    // Standard Zap logger setup
    return zap.NewProduction()
}

Core Principles

Building Things-Kit, I stuck to five principles:

1. Modularity First

Every component is an independent Go module. Need just logging? Import just logging. Your binary only includes what you actually use.

import "github.com/things-kit/module/logging"

Each module is versioned independently. No monolithic framework version hell.

2. Program to Interfaces

Your code depends on interfaces, not concrete implementations:

type UserHandler struct {
    logger log.Logger      // Not *zap.Logger
    db     *sql.DB         // Standard library
    cache  cache.Cache     // Not *redis.Client
}

Want to swap Zap for Zerolog? Implement the log.Logger interface. Want to use Valkey instead of Redis? Implement the cache.Cache interface.

3. Convention over Configuration

Everything works out of the box with sensible defaults:

# config.yaml
http:
  port: 8080  # That's it for basic HTTP

logging:
  level: info  # Sensible default

But every value is overridable when you need it.

4. No Magic

Every module is just regular Go code using Fx. You can read the source and understand exactly what's happening. No code generation. No build tags. No reflection tricks.

// You can always access the underlying types
fx.Invoke(func(logger log.Logger) {
    // This is the actual *zap.Logger if you need it
    if zapLogger, ok := logger.(*zap.Logger); ok {
        // Use Zap-specific features
    }
})

5. Lifecycle Aware

All modules hook into Fx's lifecycle for graceful startup and shutdown:

fx.Invoke(func(lc fx.Lifecycle, server Server) {
    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            // Start server
        },
        OnStop: func(ctx context.Context) error {
            // Graceful shutdown
        },
    })
})

No signal handling. No goroutine management. It just works.

Show Me Real Code

Let's build a user service with Things-Kit:

1. Your Business Logic (internal/user/service.go):

package user

import "github.com/things-kit/module/log"

type Service struct {
    logger log.Logger
    repo   *Repository
}

func NewService(logger log.Logger, repo *Repository) *Service {
    return &Service{logger: logger, repo: repo}
}

func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
    s.logger.InfoC(ctx, "fetching user", log.Field{Key: "id", Value: id})
    return s.repo.FindByID(ctx, id)
}

2. Your HTTP Handler (internal/user/handler.go):

package user

import (
    "github.com/gin-gonic/gin"
    "github.com/things-kit/module/http"
)

type Handler struct {
    service *Service
}

func NewHandler(service *Service) *Handler {
    return &Handler{service: service}
}

func (h *Handler) GetUser(c *gin.Context) {
    user, err := h.service.GetUser(c.Request.Context(), c.Param("id"))
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, user)
}

// Register routes
func (h *Handler) RegisterRoutes(router gin.IRouter) {
    router.GET("/users/:id", h.GetUser)
}

3. Wire It Together (internal/user/module.go):

package user

import (
    "go.uber.org/fx"
    "github.com/things-kit/module/httpgin"
)

var Module = fx.Module("user",
    fx.Provide(NewRepository),
    fx.Provide(NewService),
    fx.Provide(NewHandler),
    fx.Invoke(httpgin.AsHandler((*Handler).RegisterRoutes)),
)

4. Main (cmd/server/main.go):

package main

import (
    "github.com/things-kit/app"
    "github.com/things-kit/module/httpgin"
    "github.com/things-kit/module/logging"
    "github.com/things-kit/module/sqlc"
    "github.com/things-kit/module/viperconfig"
    "myapp/internal/user"
)

func main() {
    app.New(
        viperconfig.Module,
        logging.Module,
        sqlc.Module,
        httpgin.Module,
        user.Module,
    ).Run()
}

That's a complete, production-ready microservice!

What's Different?

Before Things-Kit:

  • ❌ 150+ lines of infrastructure code
  • ❌ Manual dependency wiring
  • ❌ Custom graceful shutdown
  • ❌ Configuration boilerplate
  • ❌ Repeated across every service

After Things-Kit:

  • ✅ 10 lines in main
  • ✅ Automatic dependency injection
  • ✅ Built-in lifecycle management
  • ✅ Convention-based configuration
  • ✅ Reusable across all services

Current Status

Things-Kit is feature-complete for the initial release:

  • ✅ Core app runner
  • ✅ Configuration (Viper)
  • ✅ Logging (Zap)
  • ✅ HTTP server (Gin)
  • ✅ gRPC server
  • ✅ Database (sqlc/PostgreSQL)
  • ✅ Cache (Redis)
  • ✅ Messaging (Kafka)
  • ✅ Testing utilities

I've built two complete examples:

  1. Basic HTTP service
  2. Full CRUD API with PostgreSQL + integration tests

Both are running in production-like environments.

What's Next?

In this series, I'll dive deep into:

  1. This Post: Why I built it (you are here! 👋)
  2. Next: How dependency injection works in Go with Uber Fx
  3. Tutorial: Building your first microservice step-by-step
  4. Deep Dive: The interface abstraction pattern
  5. Production: Taking it from example to production

Try It Yourself

Want to see it in action?

git clone https://github.com/things-kit/things-kit-example.git
cd things-kit-example
cp config.example.yaml config.yaml
go run ./cmd/server

# In another terminal:
curl http://localhost:8080/health
curl http://localhost:8080/greet/DevTo

I Want Your Feedback! 🙏

Before I release v1.0, I'm actively seeking feedback:

Questions for you:

  1. Does this approach resonate? Is it useful or overengineering?
  2. What modules would you want to see next?
  3. Any concerns with the interface abstraction pattern?
  4. How do you currently handle DI in your Go projects?

Links:

Drop a comment below! I read and respond to everything.

And if you found this interesting, ⭐ star the repo and follow along. Next week, I'll explain exactly how the dependency injection magic works under the hood.

Thank You! 🙌

Thanks for reading this far! Building Things-Kit has been a journey of learning about Go, microservices, and developer experience.

What's your biggest pain point when building Go microservices? Let me know in the comments!

Follow the series:

  • Part 1: Why I Built This (this post)
  • Part 2: Understanding Dependency Injection in Go (coming next week)
  • Part 3: Building Your First Microservice (tutorial)
  • Part 4: The Interface Abstraction Pattern
  • Part 5: From Example to Production

See you in Part 2! 👋

Built with ❤️ for the Go community

go #golang #microservices #opensource #webdev #programming #tutorial #showdev


This content originally appeared on DEV Community and was authored by Avicienna Ulhaq


Print Share Comment Cite Upload Translate Updates
APA

Avicienna Ulhaq | Sciencx (2025-10-06T00:59:55+00:00) Why I Built Things-Kit: A Spring Boot Alternative for Go. Retrieved from https://www.scien.cx/2025/10/06/why-i-built-things-kit-a-spring-boot-alternative-for-go/

MLA
" » Why I Built Things-Kit: A Spring Boot Alternative for Go." Avicienna Ulhaq | Sciencx - Monday October 6, 2025, https://www.scien.cx/2025/10/06/why-i-built-things-kit-a-spring-boot-alternative-for-go/
HARVARD
Avicienna Ulhaq | Sciencx Monday October 6, 2025 » Why I Built Things-Kit: A Spring Boot Alternative for Go., viewed ,<https://www.scien.cx/2025/10/06/why-i-built-things-kit-a-spring-boot-alternative-for-go/>
VANCOUVER
Avicienna Ulhaq | Sciencx - » Why I Built Things-Kit: A Spring Boot Alternative for Go. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/10/06/why-i-built-things-kit-a-spring-boot-alternative-for-go/
CHICAGO
" » Why I Built Things-Kit: A Spring Boot Alternative for Go." Avicienna Ulhaq | Sciencx - Accessed . https://www.scien.cx/2025/10/06/why-i-built-things-kit-a-spring-boot-alternative-for-go/
IEEE
" » Why I Built Things-Kit: A Spring Boot Alternative for Go." Avicienna Ulhaq | Sciencx [Online]. Available: https://www.scien.cx/2025/10/06/why-i-built-things-kit-a-spring-boot-alternative-for-go/. [Accessed: ]
rf:citation
» Why I Built Things-Kit: A Spring Boot Alternative for Go | Avicienna Ulhaq | Sciencx | https://www.scien.cx/2025/10/06/why-i-built-things-kit-a-spring-boot-alternative-for-go/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.