Your service doubles its application replicas to handle a traffic spike. Errors increase, the database refuses connections, and someone proposes doubling every connection pool. In an interview, this is a useful moment to slow down: which resource is exhausted, who owns its budget, and what happens to requests that cannot acquire it?
A connection-pooling discussion lets senior and staff engineers connect application code to database behavior. This guide builds one answer around a PostgreSQL-backed service, a Go application pool, and an optional proxy. The scenario, numbers, and practice rubric below are illustrative preparation tools, not an employer's hiring criteria or production tuning recommendations.
1. Draw the connection ownership boundaries
Start by distinguishing three objects: a request, an application connection, and a database backend connection. A request may acquire a connection briefly or retain it across an entire transaction. A proxy may keep a client connection open while lending different backend connections to successive transactions. Count each boundary separately before choosing limits.
In Go, sql.DB is a concurrent database handle that manages a pool; it is not one database connection. SetMaxOpenConns caps open connections, and operations wait when the pool cannot supply one. The Go connection-management guide explicitly warns that introducing this limit can produce semaphore-like deadlocks. A pool therefore creates a coordination policy as well as connection reuse.
Ask which workloads share the database: interactive APIs, background imports, scheduled reports, migrations, and operational tools. Then identify how many independently configured pool instances each workload creates. A service with multiple processes or database handles may own several pools per replica. An attractive per-pool number means little until you multiply it by every possible owner.
Use one request as the walkthrough
Take an order-update endpoint: validate input, acquire a connection, begin a transaction, update two records, commit, and return. Mark the acquire and release points. Now add a slow external notification call. If that call occurs before commit, ask whether the database transaction needs to remain open during network waiting. Moving it requires a delivery design, but the first useful observation is that connection occupancy includes more than SQL execution.
2. Allocate a database-wide budget
PostgreSQL's max_connections is a server limit, not an application entitlement. PostgreSQL also has privileged connection reserves, and increasing the maximum increases allocation of certain resources, including shared memory. The PostgreSQL 18 connection settings explain these constraints. Preserve administrative access explicitly instead of treating every available slot as application capacity.
Illustrative budget: suppose a hypothetical database permits 240 connections. Allocate 20 to privileged and operational headroom and 40 to other workloads. That leaves 180 for this service. With a maximum of 12 simultaneously connected replicas, including deployment overlap, a direct-to-database ceiling of 15 connections per replica fits the arithmetic: 12 × 15 = 180. This is a connection-count envelope, not evidence that 180 concurrent queries perform well.
Change the interviewer’s assumption: there can now be 18 replicas during a rollout. The same pool setting permits 270 connections, exceeding the service allocation. You must change the replica envelope, pool allocation, or architecture. Describe who owns that change and how configuration prevents an autoscaler and a deployment controller from independently spending the same budget.
Use measured connection hold times to build load-test hypotheses. If completed work uses 30 connection-seconds during one second of steady operation, its average occupancy is 30 connections. That accounting identity does not tell you the safe maximum: bursts, lock waits, uneven replicas, and query mix still matter. Test several concurrency limits and compare completed useful work, response latency, and database saturation. Choose a setting from that evidence rather than equating more connections with more throughput.
When introducing PgBouncer, repeat the calculation for its backend pools. Its configuration documentation defines default_pool_size per user/database pair and distinguishes client limits from server limits. Enumerate pool pairs and proxy instances, including reserve pools. A proxy does not turn one local configuration value into a global database ceiling.
3. Make waiting a deliberate product decision
A full pool forces a choice about waiting, rejection, or reduced work. State the endpoint deadline first, then decide how much may be consumed before a connection is acquired. A request that has spent nearly its whole budget in a queue should not start expensive SQL that cannot finish in time. A large waiting room can conceal overload while users experience timeouts.
Use cancellation-aware database calls and propagate the caller’s deadline. The Go cancellation guide shows deriving a timeout context, passing it to QueryContext, and releasing its resources with the returned cancel function. Verify the actual driver’s cancellation behavior in testing; an application returning a timeout is not sufficient proof that server-side work has stopped.
Keep server protection separate from client waiting. PostgreSQL exposes statement_timeout, lock_timeout, and idle_in_transaction_session_timeout, each covering a different condition. Its client defaults documentation describes their scope and cautions against indiscriminate global timeout settings. None should be presented as a substitute for the application’s connection-acquisition deadline.
Decide which work yields first
For this practice design, pause an optional analytics refresh before rejecting an order update. Give background imports their own bounded concurrency allocation. Separate pools can isolate waiting, but their combined limits still spend the same database budget. Explain how a background worker learns to slow down and how an interactive client receives an explicit retryable error when appropriate.
Google's SRE chapter on handling overload describes graceful degradation and rejecting work when resources cannot sustain it. Apply that principle to the connection boundary: bound admitted work and avoid retries that immediately recreate the rejected load. In a mock interview, name the specific optional response fields or jobs you would defer and the user-visible behavior that remains correct.
4. Choose pooling mode from transaction semantics
PgBouncer offers session, transaction, and statement pooling. Session pooling retains a server connection for the client session; transaction pooling returns it after a transaction ends; statement pooling disallows multi-statement transactions. The PgBouncer feature matrix also lists session-oriented features that do not work in transaction pooling, including LISTEN and session-level advisory locks.
Ask what the application expects to survive across transaction boundaries. A listener, a session setting, and a temporary table may require different treatment from an ordinary short transaction. Inventory those behaviors before proposing transaction pooling. Do not copy a categorical claim that prepared statements never work: current PgBouncer documents protocol-level prepared-plan support when max_prepared_statements is nonzero, while SQL PREPARE/DEALLOCATE remain incompatible in its transaction-pooling matrix.
Managed proxies need their own compatibility review. AWS documents that RDS Proxy session pinning binds a client to a backend until the session ends, reducing multiplexing. Its rules depend on the database engine and session behavior. A claim about MySQL session handling is not evidence for PostgreSQL. Watch the documented DatabaseConnectionsCurrentlySessionPinned metric when testing representative application traffic.
The interview decision should sound conditional: use transaction pooling for the compatible short-transaction path, retain a deliberately budgeted session-oriented path where required, and test both. State which behaviors would make you revise that choice. This is more defensible than describing a proxy as a universal fix for database connection pressure.
5. Trace leaks and self-inflicted deadlocks
Consider a pool with one available slot. A handler begins a transaction, then calls a helper that uses the parent sql.DB to run another query. The transaction holds the slot; the helper waits for a connection; the handler waits for the helper before committing. Enlarging the pool may hide this ownership mistake until concurrent traffic reproduces it.
The Go transaction guide says to use sql.Tx operations within the transaction and warns that mixing in nontransactional sql.DB calls can produce inconsistent views or deadlocks. Make the intended transaction scope explicit in helper interfaces. End every transaction through commit or rollback, including error paths, and test the path that returns early.
Result lifetimes deserve the same attention. The database/sql reference documents Rows.Close, connection release, and DBStats fields such as in-use connections, idle connections, wait count, and wait duration. Review whether rows are consumed and closed on every path. A connection held while an application slowly serializes a large response can look different from a connection executing a slow query.
Use counter changes over an interval when interpreting cumulative waits. Correlate application acquisition time with transaction duration and database evidence. PostgreSQL's activity statistics documentation describes pg_stat_activity, including session states and wait events. Those observations help distinguish lock contention, idle transactions, and active work. Avoid treating low database CPU as proof that acquiring a connection should be fast.
6. Rehearse failures and defend a change
Run a mock interview in three passes. First, draw the ownership path and allocate the direct-connection budget. Second, introduce a proxy and identify which session behaviors affect reuse. Third, inject a failure: a background import starts during a rolling deployment while one transaction waits on a lock. Follow the waiting request until it succeeds, times out, or is rejected.
Illustrative practice rubric: give yourself one point for each explanation you can make concretely: the maximum aggregate connection count; acquire and release ownership; a bounded waiting policy; the pooling mode’s compatibility constraints; a cancellation experiment; and a recovery signal. This six-point checklist is a self-review aid, not a validated hiring score or a prediction of interview outcomes.
Finish with a reversible experiment. Propose changing one pool setting on a limited application cohort while keeping the database-wide envelope fixed. Compare useful completions, acquisition waits, connection churn, and transaction failures against the existing configuration. Include deployment overlap and recovery from a database restart in testing. Define a rollback trigger before the experiment rather than interpreting every improvement in one metric as success.
A strong answer makes connection ownership visible from request arrival to resource release. Bring a small budget calculation and one failing transaction trace into your next practice session. They give you concrete evidence to discuss when the interviewer changes the replica count, introduces session state, or asks why adding capacity made the outage worse.
Practice explaining the resource boundary
Use Interview Copilot to prepare technical answers and rehearse how you defend assumptions, transaction boundaries, and failure handling before an interview.
Start practicingSources & References
- Go: Managing connections
- PostgreSQL 18: Connections and authentication
- PgBouncer: Configuration
- Go: Canceling in-progress operations
- PostgreSQL 18: Client connection defaults
- Google SRE: Handling overload
- PgBouncer: Pooling modes and SQL feature matrix
- AWS: Avoiding RDS Proxy pinning
- Go: Executing transactions
- Go: database/sql reference
- PostgreSQL 18: Activity statistics