Global State vs. Dependency Injection: Trade-offs and Best Practices
Introduction**β
When structuring backend applications, one of the key decisions is whether to use global state or dependency injection (DI) for managing shared resources like database connections.
We are using a go backend as our example here, but of course this applies to any application.
Understanding Global State in Go**β
What It Isβ
-
Defining variables at the package level:
package db
import (
"database/sql"
_ "github.com/lib/pq"
)
var DB *sql.DB
var Queries *Queries -
These variables are initialized once and shared across the application.
Pros of Using Global Stateβ
β
Simplifies function signatures β No need to pass dependencies everywhere.
β
Cleaner code in small projects β Reduces boilerplate.
β
Good for truly global, single-instance resources like a database connection.
Cons of Using Global Stateβ
β Harder to test β You can't easily swap dependencies in tests.
- That said, I personally use
β Implicit dependencies β Functions rely on hidden state instead of explicit parameters.
β Risk of unintended side effects β If reinitialized, everything depending on it changes.
Understanding Dependency Injection in Go**β
What It Isβ
-
Instead of using package-level variables, dependencies are explicitly created and passed around:
package db
func NewDB() (*sql.DB, *Queries, error) {
db, err := sql.Open("postgres", "dsn_here")
if err != nil {
return nil, nil, err
}
queries := New(db)
return db, queries, nil
}package main
import "myapp/db"
func main() {
dbConn, queries, err := db.NewDB()
if err != nil {
log.Fatal(err)
}
startServer(dbConn, queries)
}func startServer(db *sql.DB, queries *Queries) {
handler := NewHandler(db, queries)
http.ListenAndServe(":8080", handler)
}
Pros of Dependency Injectionβ
β
Explicit dependencies β Clearer about what a function needs.
β
Easier to test β Can inject mock implementations in unit tests. *See Note
β
More flexible β Supports multiple instances (e.g., different DB connections).
Cons of Dependency Injectionβ
β Function signatures become long β Need to pass dependencies explicitly.
β More boilerplate β Requires more struct initialization and passing.
When to Use Each Approach**β
| Scenario | Global State β | Dependency Injection β |
|---|---|---|
| Small apps, scripts | β | π« |
| Large applications | π« | β |
| Testing with mock databases | π« | β |
| Keeping function signatures clean | β | π« |
| Managing multiple DB instances | π« | β |
Conclusion**β
- Thereβs no universal βrightβ choiceβit depends on your project.
- Use global state for simplicity when it makes sense.
- Use dependency injection when you need testability and flexibility.
- Hybrid approaches can work, but be careful with global modifications.
- In your case, switching to global state helped clean up function signatures while keeping things manageable.
*_ A note on Testability _ - I personally use E2E API call tests, since E2E tests can validate the entire flow. Therefore for typical CRUD backends i dont have unit tests for handlers. This makes dependency injection less critical for testability for this specific use case, but of course there may be other situations where I choose DI specifically for testability reasons.
This captures the trade-offs youβve been working through in real-time. Want to add anything else before we refine it into a full article?
Comments
No comments yet. Be the first!