Skip to main content

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**​

ScenarioGlobal 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!