Clean code: production contracts · 3 MIN READ
A composition root makes lifetime and cleanup visible
Construct dependencies once and make their ownership and shutdown order explicit.
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.
clean-code-production · A MAP OF THE IDEA
- Validate config
- Construct dependencies
- Serve borrowed clients
- Drain then close
Useful when
- Construct dependencies once and make their ownership and shutdown order explicit.
- Makes the owner and the failure boundary explicit.
Watch out
- A dependency-injection framework cannot infer the correct business lifetime.
- Run native service and failure tests in your own development environment.
Implementation & operation cost: Additional state and tests must be justified by the demonstrated failure; this lesson is not a requirement for every Go program.
The contract and the failure
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.
Work through the example
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.
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)
}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.
Production decisions
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.
Explain it in an interview
A service receives a shared database client. Should each request close it? No. The composition root owns the shared client; requests borrow it and close only request-scoped resources such as rows or bodies. Describe the state before the operation, its owner, the failure between steps and the evidence you would test. Distinguish what this example demonstrates from the infrastructure a production service still needs.
TAKE THIS WITH YOU
Construct dependencies once and make their ownership and shutdown order explicit. No. The composition root owns the shared client; requests borrow it and close only request-scoped resources such as rows or bodies.
Check your understanding
A service receives a shared database client. Should each request close it?
Go to the source
Progress stays in this browser. No account needed.