Go DI container vs. service template generator
Service templates are fine on day one. DI containers age better in Go services.

- A Service Template (generator) bootstraps new microservices from a shared starting point. Common examples:
- Dependency injection
(DI)1 means code receives ready components instead of creating them
itself.2
- DI container automates that wiring. It isn’t the most common Go bootstrap path, but it keeps the shared boot code typed.
# The problem: service drift
The goal is boring: ship services faster without making each service a one-off snowflake. That means:
- Pre-built components: no new logging, metrics, config, DB, or broker code in every service.
- Consistent service structure: less context switching between services.
- Unified interaction interfaces: same config, logs, metrics, and deploy hooks for ops.
# Where templates start to drift
The drift starts when the second, fifth, and tenth service appear. The template keeps moving, but the generated services don’t move with it.
When I inspect services, I usually find the same split. Whatever the layered architecture is called: Onion, Clean, or Hexagonal3) the outer layers repeat. Business logic changes.4 Observability, database connections, message broker clients, config, and metrics can be shared. That’s the part worth typing and testing once.
# Straightforward implementation
A common approach is a shared library plus a service template. The template creates a new service with the library and prepared boilerplate.
Pros:
- This approach works.
Cons:
- The template fixes initial setup, then drifts away from the services it created.
- The template itself is harder to test and analyze than normal Go code.
# DI container: typed shared bootstrapping
Instead of relying on templates, I keep the shared library and put the shared components inside a container. The service adds business logic around it:
import (
"github.com/irr123/di"
...
"internal.lib/bootstrap"
)
func main() {
c := di.New()
bootstrap.PutItAllTogether(c)
di.Set(c, di.OptMiddleware(func(e *echo.Echo) (*echo.Echo, error) {
// echo srv here is already fully configured and only needs to attach handlers
e.GET("/", func(c echo.Context) error {
return c.JSON(http.StatusOK, "")
})
}))
di.Get[*echo.Echo](c).Start(":8080")
}
This costs less to maintain because it’s typed Go code, not a text template. Tests, static analysis, and normal compatibility rules apply.
# What templates still cover
Templates still cover more on day one: project layout, deployment scripts, CI/CD, VCS hooks, dashboards, alerts. That’s useful. The limit is enforcement. Templates provide those pieces at creation time. Containers keep shared wiring consistent while the service evolves.
Use both if needed. Template the folder. Put the living bootstrapping code in a container.