Middle · Architecture and systems
Who closes the injected database client?
A request-scoped service receives a shared *sql.DB and defers db.Close in every handler. Is dependency injection correct here?
Try explaining this aloud before opening the answer. State your assumptions and trace a boundary case.
package main
import (
"fmt"
"io"
)
type Client struct{ name string }
func (c *Client) Close() error { fmt.Println("close", c.name); return nil }
type Service struct{ client *Client }
func main() {
client := &Client{name: "shared-store"}
var closer io.Closer = client
defer closer.Close()
service := Service{client: client}
fmt.Println(service.client.name)
}Reveal a strong answer
Dependency injection in Go is usually ordinary constructors and parameters. The important question is not which container library to use but who creates a shared client, who is allowed to close it and how partial startup is cleaned up. Keep wiring near main so a handler cannot silently allocate a new database pool or depend on a hidden global singleton.
Accept concrete values until a boundary genuinely needs an interface. Validate configuration before serving traffic. Constructors return errors for required setup and document whether they take ownership. Separate startup, serving and shutdown contexts. Stop accepting work, drain owners and only then close shared dependencies; closing the pool first makes graceful shutdown generate avoidable failures. main owns the client and schedules its cleanup exactly once. Service borrows the client and does not close it after each request. The example omits a real database so its lifetime is visible without a network. In production, unwind resources created before any later constructor fails and report shutdown errors according to their significance.
A dependency-injection framework cannot infer the correct business lifetime. A request-scoped service may borrow a process-scoped client, but a process-scoped service must not retain a request context. Test partial startup failure, duplicate Close, expired request contexts and shutdown ordering. Avoid packages that read environment variables or open connections as import-time side effects.
Common trap
Passing an object as a constructor argument does not automatically transfer ownership of Close.
Try it
Draw resource ownership for startup, requests, partial construction failure and shutdown; distinguish a borrowed pool from request-scoped Rows.
Worked solution
The shared pool belongs to the composition root and must not be closed by each borrower. Requests close their rows and bodies; process shutdown drains request and worker owners before closing shared clients. Constructors state ownership and validate required configuration. A dependency-injection framework does not determine these lifetimes or fix cleanup when a later constructor fails.
Change the constraint
How do you test that a failed second constructor closes the first resource exactly once?
How did you do?
Self-assessment, not a compiler pass or interview score. Explain the reasoning unaided before marking understood.
Not marked. Progress stays on this browser, separate from course completion.
Primary references
Checked 2026-10-28. Use documentation for your pinned runtime, driver and tool version.