Stage 1 · Code
Working with Databases
Connection Pools and Schema Migrations
A real service does not talk to the database through one lonely pipe forever. This lesson explains the pool behind `sql.DB`, the knobs that shape it, and the migration files that keep schema changes orderly.
One Handle, Many Connections
It is easy to look at a *sql.DB and imagine one fixed socket to the database. That is not how it works. A better picture is a taxi stand. Your service asks for rides all day long. Some taxis are already waiting, some are driving passengers, and some may leave if they have sat around too long. The stand coordinates all of that so callers do not manually open and close a brand-new taxi for every trip.
That coordination is called connection pooling. Reusing live connections is faster than rebuilding them for every request. But a pool also needs limits. Too few connections and requests wait in line. Too many connections and the database itself becomes overloaded because every application instance is trying to be helpful at once.
Treat *sql.DB as a long-lived shared object for the whole process. You usually create it once during startup and reuse it everywhere. Opening one per request defeats pooling and adds avoidable connection churn.
Tuning the Pool for Real Traffic
Three settings appear again and again because they answer three different failure modes. SetMaxOpenConns says how many live database connections your process may have in total. SetMaxIdleConns says how many finished connections it is worth keeping around for the next burst. SetConnMaxLifetime says how long a connection may live before being recycled, which helps when load balancers, proxies, or the database server itself dislike ancient connections lingering forever.
| Setting | What it controls | Too low can cause | Too high can cause |
|---|---|---|---|
SetMaxOpenConns | Total simultaneous connections | Requests waiting on the pool | Database exhaustion or noisy-neighbor pressure |
SetMaxIdleConns | How many ready-to-reuse connections stay warm | More reconnect churn during bursts | Unused connections occupying server resources |
SetConnMaxLifetime | How long a connection may live before refresh | More frequent reconnects if overly short | Stale, long-lived connections surviving too long |
Imagine a checkout service running on four application instances. If each instance allows 100 open connections, the database may suddenly face 400 clients before you even count admin tools, background workers, or migrations. That is why pool tuning is not just a code detail. It is capacity planning in miniature.
Why Migrations Exist
Schema changes deserve the same discipline as code changes. You would not open a production source file in a text editor on the server, type a few random lines, and hope the rest of the team somehow guesses what changed. Hand-editing a production schema is the database version of that mistake.
A migration is a versioned, ordered change to the schema. Usually it lives in a file with a clear sequence number or timestamp. That file becomes the permanent record of how the schema evolved: create table, add column, rename index, backfill data, and so on. Because the changes are ordered, a fresh environment can replay them from the beginning and arrive at the same final shape as production.
If you change the production schema by hand, you create a secret history. Staging, local development, future teammates, and disaster recovery scripts all lose the authoritative path that explains how the schema got here.
Tracing a Schema Change
Suppose your support_tickets table originally knows the ticket title and whether the ticket is closed, but not how urgent the work is. Product asks for a priority field so the team can separate a spelling fix from a payment outage. A migration is how you teach the schema that new idea.
| Version | Columns in `support_tickets` |
|---|---|
| Before migration | id, title, closed |
| After migration | id, title, closed, priority |
-- 20260720_add_priority_to_support_tickets.up.sql
ALTER TABLE support_tickets
ADD COLUMN priority TEXT NOT NULL DEFAULT 'normal';
The DEFAULT 'normal' part matters. Existing rows need some value the moment the column appears, or the database cannot satisfy the NOT NULL rule for old data.
- Before the migration, ticket
#42might be stored as('Printer room badge issue', false). - The migration adds a
prioritycolumn with a default of'normal'. - After the migration, the same row effectively becomes
('Printer room badge issue', false, 'normal'). - New code can now update urgent incidents to
'high'without breaking older rows.
Quiz and Practice
What is the most accurate mental model for `*sql.DB`?
Hands-On Project
Implement `TunePool` so it applies the team's agreed settings: 16 max open connections, 4 max idle connections, and a 30 minute connection lifetime.
Summary and Key Takeaways
- A
*sql.DBis a shared connection pool manager, not a single fragile wire. - Pool tuning is about balancing application throughput against total database capacity.
SetMaxOpenConns,SetMaxIdleConns, andSetConnMaxLifetimesolve different operational problems.- Migrations make schema changes ordered, reviewable, and repeatable across environments.
- Never let production become the only place that knows how your schema evolved.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.