Show HN: Things-Kit – Bringing Spring Boot-Like DX to Go Microservices

1 month ago 4

A Modular, Opinionated Microservice Framework for Go, Built on Uber Fx.

Things-Kit is a microservice framework for Go designed to bring the productivity and developer experience of frameworks like Spring Boot to the Go ecosystem. It is built entirely on the principles of dependency injection, leveraging Uber's Fx library to manage the application lifecycle and dependency graph.

Core Implementation Complete - All foundational modules and infrastructure modules have been implemented and tested.

  • Modularity First: Every piece of infrastructure (gRPC, Kafka, Redis, etc.) is a self-contained, versionable Go module
  • Dependency Injection: Built on Uber Fx for clean, decoupled architecture
  • Interface-Based Design: Program to abstractions, not implementations
  • Convention over Configuration: Sensible defaults with full override capability
  • Lifecycle Aware: Graceful startup and shutdown for all components
  • Developer Experience: Minimal boilerplate through generic helpers

Check out our complete example projects:

  1. things-kit-example - Basic HTTP server

    • Demonstrates Gin framework integration
    • Service layer pattern
    • Dependency injection basics
  2. things-kit-example-db - HTTP + PostgreSQL

    • Complete CRUD REST API
    • Repository pattern
    • Database integration with sqlc
    • Integration tests with testcontainers ✅
    • All tests passing
# Clone an example git clone https://github.com/things-kit/things-kit-example.git cd things-kit-example # Configure and run cp config.example.yaml config.yaml go run ./cmd/server

Then test it:

curl http://localhost:8080/health curl http://localhost:8080/greet/World

This is a Go workspace containing multiple modules:

  • app/ - Core application runner with lifecycle management

Infrastructure Modules (in module/)

All framework-provided modules are organized under module/:

  • module/log/ - Logger interface abstraction
  • module/logging/ - Default Zap-based logger implementation ⭐
  • module/http/ - HTTP server interface abstraction (framework-agnostic)
  • module/httpgin/ - Default Gin-based HTTP server implementation ⭐
  • module/cache/ - Cache interface abstraction (key-value operations)
  • module/redis/ - Default Redis-based cache implementation ⭐
  • module/grpc/ - gRPC server with lifecycle management
  • module/sqlc/ - Database connection pool with lifecycle management
  • module/kafka/ - Kafka consumer implementing messaging interfaces
  • module/messaging/ - Message handling interface abstraction (Handler, Consumer, Producer)
  • module/viperconfig/ - Configuration management with Viper
  • module/testing/ - Testing utilities for integration tests

⭐ = Default implementation (interface + impl pattern)

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/viperconfig" ) func main() { app.New( viperconfig.Module, logging.Module, httpgin.Module, // Use Gin HTTP implementation // Add your handlers and services ).Run() }

2. Configure Your Service

Create config.yaml:

http: port: 8080 mode: release logging: level: info encoding: json
for dir in app module/*; do if [ -d "$dir" ] && [ -f "$dir/go.mod" ]; then (cd "$dir" && go build ./...) fi done
for dir in app module/*; do if [ -d "$dir" ] && [ -f "$dir/go.mod" ]; then (cd "$dir" && go test ./...) fi done
go work sync # Sync all module dependencies

Each module follows a consistent pattern:

  1. Config Struct - Define configuration with mapstructure tags
  2. NewConfig Function - Load config from Viper with sensible defaults
  3. Module Variable - Export fx.Module for easy consumption
  4. Lifecycle Integration - Use fx.Lifecycle hooks for startup/shutdown
  5. Helper Functions - Provide AsXxx helpers for easy service registration

See any module's implementation for examples.

Interface Abstraction Pattern

Things-Kit follows the principle "Program to an Interface, Not an Implementation". This means:

All framework-provided components are in module/:

  • Interface modules define the contract (e.g., module/log, module/http, module/cache)
  • Implementation modules provide default implementations (e.g., module/logging, module/httpgin, module/redis)
  • Alternative implementations would live at the root level (e.g., httpchi/, logging-logrus/, cachevalkey/)
  • Interface: module/log defines the Logger interface
  • Default Implementation: module/logging provides a Zap-based implementation
  • Your Choice: You can provide your own logger implementation (logrus, zerolog, etc.)
  • Interface: module/http defines Server, Handler, and Config interfaces
  • Default Implementation: module/httpgin provides a Gin-based implementation
  • Your Choice: You can use Chi, Echo, standard library, or any HTTP framework
  • Interface: module/cache defines the Cache interface (Get, Set, Delete, etc.)
  • Default Implementation: module/redis provides a Redis-based implementation
  • Your Choice: You can use Valkey, Memcached, in-memory cache, or any key-value store
  • Interfaces: module/messaging defines Handler, Consumer, and Producer interfaces
  • Default Implementation: module/kafka provides Kafka consumer implementation
  • Your Choice: You can use RabbitMQ, NATS, AWS SQS, or any message broker

This design allows you to:

  1. Start Fast: Use the default implementations (Zap logger, Gin HTTP server, Redis cache, Kafka messaging)
  2. Stay Flexible: Swap implementations without changing your application code
  3. Test Easily: Mock interfaces for unit testing
  4. Evolve Gracefully: Migrate to different frameworks as needs change
  5. Clear Organization: All framework modules in one place (module/)

Example: Custom HTTP Implementation

// Step 1: Implement the http.Server interface type MyCustomServer struct { // your implementation } func (s *MyCustomServer) Start(ctx context.Context) error { /* ... */ } func (s *MyCustomServer) Stop(ctx context.Context) error { /* ... */ } func (s *MyCustomServer) Addr() string { /* ... */ } // Step 2: Create an Fx module var CustomHttpModule = fx.Module("mycustomhttp", fx.Provide( NewCustomConfig, NewMyCustomServer, fx.Annotate( func(s *MyCustomServer) http.Server { return s }, fx.As(new(http.Server)), ), ), fx.Invoke(RunCustomHttpServer), ) // Step 3: Use it in your application func main() { app.New( viperconfig.Module, logging.Module, CustomHttpModule, // Your custom implementation // ... ).Run() }

See the httpgin module for a complete example of implementing the HTTP interface.

Example: Custom Cache Implementation

// Step 1: Implement the cache.Cache interface type MyMemcachedCache struct { client *memcache.Client } func (c *MyMemcachedCache) Get(ctx context.Context, key string) (string, error) { /* ... */ } func (c *MyMemcachedCache) Set(ctx context.Context, key, value string, exp time.Duration) error { /* ... */ } // ... implement all Cache methods // Step 2: Create an Fx module var MemcachedModule = fx.Module("memcached", fx.Provide( NewMemcachedConfig, NewMemcachedCache, fx.Annotate( func(c *MyMemcachedCache) cache.Cache { return c }, fx.As(new(cache.Cache)), ), ), ) // Step 3: Use it in your application func main() { app.New( viperconfig.Module, logging.Module, MemcachedModule, // Your custom cache implementation // ... ).Run() }

See the redis module and cache interface for complete examples.

What Makes This Different?

  • True Modularity: Each module is independently versionable
  • Pluggable Core: Swap out any component (logger, config, etc.)
  • Context-Aware: Embraces Go's context idiom for cancellation and tracing
  • Production Ready: Built-in graceful shutdown, structured logging, configuration management
  • Developer Friendly: Minimal boilerplate, clear patterns, comprehensive examples

MIT

Read Entire Article