Pools
Understand connection pools, identifiers, shared pools, Fork, and Close behavior across DB drivers.
Each NetLifeGuru database driver creates and manages its own connection pool.
The shared db package defines the common connection interface, while the concrete driver package creates the real pool for MySQL, PostgreSQL, or Scylla.
A typical flow is:
- create a driver connection
- call
CreatePool - return a forked
db.Conn - use the shared
dbhelpers in application code - close the original driver connection when the application shuts down
Basic Flow
conn := mysql.New()
err := conn.CreatePool(db.Config{
Identifier: "default",
Host: "127.0.0.1",
Port: 3306,
Database: "app",
Username: "root",
Password: "secret",
})
if err != nil {
return nil, err
}
return conn.Fork(), nilThe returned value implements db.Conn.
func connectDB() (db.Conn, error) {
// create pool
// return conn.Fork()
}CreatePool
CreatePool creates or reuses a connection pool for the selected driver.
if err := conn.CreatePool(cfg); err != nil {
return nil, err
}The config contains connection settings such as:
- identifier
- host
- port
- database or keyspace
- username
- password
- pool limits
- timeouts
- driver-specific options
Each driver validates and normalizes the config before opening the connection.
Identifier
Every pool has an identifier.
Identifier: "default"The identifier is a logical name for the pool inside the driver.
Common examples:
Identifier: "default"
Identifier: "analytics"
Identifier: "tenant-a"
Identifier: "tenant-b"Use identifiers when your application needs multiple pools.
Identifier Conflicts
The same identifier cannot be reused with different connection settings.
For example, this is valid:
cfg := db.Config{
Identifier: "default",
Host: "127.0.0.1",
Database: "app",
}Calling CreatePool again with the same identifier and the same effective configuration reuses the pool.
But this is a conflict:
cfg := db.Config{
Identifier: "default",
Host: "127.0.0.1",
Database: "analytics",
}If the identifier already exists with different configuration, the driver returns an error.
This protects applications from accidentally reusing the same logical pool name for different databases.
Shared Pool Reuse
Drivers can reuse an existing physical pool when the effective connection configuration is the same.
For example, two connection objects with the same host, port, credentials, database, and relevant driver settings can share the same underlying pool.
This is useful when different parts of an application create equivalent connections.
The driver tracks references internally and only closes the underlying pool when the last reference is closed.
Fork
Fork returns a connection handle that implements db.Conn.
appConn := conn.Fork()Use the forked connection in application code:
users, err := db.List[User](ctx, appConn, `
SELECT * FROM users
`)A forked connection is useful because application code only needs the shared db.Conn interface.
It does not need to know whether the connection came from MySQL, PostgreSQL, or Scylla.
Why Return Fork
Connection setup usually happens in infrastructure code.
func connectDB() (db.Conn, error) {
conn := mysql.New()
if err := conn.CreatePool(cfg); err != nil {
return nil, err
}
return conn.Fork(), nil
}Application and repository code can then depend only on db.Conn.
func ListUsers(ctx context.Context, conn db.Conn) ([]User, error) {
return db.List[User](ctx, conn, `
SELECT * FROM users
`)
}This keeps query code independent from the selected driver.
Close
Close the connection when the application shuts down.
conn.Close()For MySQL and PostgreSQL, Close closes the underlying SQL or pool connection when the last reference is released.
For Scylla, Close closes the underlying Scylla session when the last reference is released.
A common application pattern is:
conn := mysql.New()
if err := conn.CreatePool(cfg); err != nil {
return err
}
defer conn.Close()If your setup function returns only conn.Fork(), keep a reference to the original driver connection when you also need to close it explicitly at shutdown.
Multiple Pools
You can create multiple pools by using different identifiers.
primary := mysql.New()
err := primary.CreatePool(db.Config{
Identifier: "primary",
Host: "127.0.0.1",
Database: "app",
})
if err != nil {
return err
}
analytics := mysql.New()
err = analytics.CreatePool(db.Config{
Identifier: "analytics",
Host: "127.0.0.1",
Database: "analytics",
})
if err != nil {
return err
}Each pool can then be used through Fork.
primaryConn := primary.Fork()
analyticsConn := analytics.Fork()Multi-Tenant Pools
Identifiers are useful for tenant-based applications.
func connectTenant(identifier string, database string) (db.Conn, error) {
conn := mysql.New()
err := conn.CreatePool(db.Config{
Identifier: identifier,
Host: "127.0.0.1",
Database: database,
Username: "app",
Password: "secret",
})
if err != nil {
return nil, err
}
return conn.Fork(), nil
}Pool Identifier
Every pool has an identifier.
The identifier is not a database name and it is not necessarily the database host. It is an application-level name used to register, reuse, and protect connection pools.
Identifier: "default"Use different identifiers when your application has multiple logical connections:
Identifier: "primary"
Identifier: "analytics"
Identifier: "tenant-a"For multi-tenant applications, the identifier can be based on your tenant routing model.
Examples:
Identifier: "example.com"
Identifier: "customer-a"
Identifier: "tenant-42"A domain name can be a good identifier when each tenant is selected by request host.
A tenant slug or customer ID is usually better when tenants are selected from application data, authentication claims, or internal routing.
Avoid using only the database host as the identifier unless the host is truly the unique logical connection name in your application.
The same identifier can be reused only with the same effective connection configuration.
If the same identifier is used with different host, database, credentials, or driver settings, the driver returns a pool identifier conflict error.
Example identifiers:
tenant-a
tenant-b
tenant-cEach tenant can point to a separate database, keyspace, or cluster depending on the driver and architecture.
Same API Across Drivers
Pool creation is driver-specific, but the returned application connection uses the shared API.
conn, err := connectDB()
if err != nil {
return err
}
users, err := db.List[User](ctx, conn, `
SELECT * FROM users
`)The query helpers do not need to know which driver created the pool.
Driver Differences
The pool implementation depends on the selected driver.
| Driver | Pool implementation |
|---|---|
| MySQL | database/sql pool |
| Postgres | pgxpool |
| Scylla | gocql.Session |
The shared db.Conn interface hides these details from application code.
However, connection behavior, transaction support, insert behavior, and SQL/CQL syntax still depend on the driver.
Recommended Pattern
Use a small connection function per driver.
func connectDB() (db.Conn, func() error, error) {
conn := mysql.New()
if err := conn.CreatePool(cfg); err != nil {
return nil, nil, err
}
cleanup := func() error {
conn.Close()
return nil
}
return conn.Fork(), cleanup, nil
}Then use it from main.
conn, cleanup, err := connectDB()
if err != nil {
log.Fatal(err)
}
defer cleanup()This keeps both values available:
db.Connfor application code- cleanup function for shutdown
Notes
Use CreatePool once during application startup when possible.
Use Fork to pass a shared db.Conn into repositories, services, handlers, jobs, or other application layers.
Use clear identifiers when managing more than one connection pool.
Avoid reusing the same identifier for different databases or tenants.
Close the driver connection during application shutdown.