Junior developers can write good code—and that is where this story begins
Imagine a pull request from a junior developer. The code is easy to read, the function is well named, the TypeScript is sound, the query is parameterised, a unit test is present, and the author can explain every line. If we judge only what is inside the file, this is good work.
Then the reviewer asks one short question:
How many times is this
Poolcreated?
The conversation changes immediately because the answer is not in the syntax, the types, or perhaps even the test. It depends on where the function is called, how often it runs, and how long the object it creates remains alive.
That is the use case this article is about. The junior developer is not bad at writing code. The opposite may be true: with AI assistance, they may write good code remarkably quickly. What can still be missing is the layer of experience that connects one line of code to the behaviour of the whole application.
This is not a claim that every junior has the same gap, nor is it about age. A developer early in their career has simply had fewer opportunities to encounter production failures that make certain questions automatic, even when the code in front of them is already strong.
Use case: what this function gets right
This example uses pg, the PostgreSQL driver for Node.js. Assume the function is called from a request handler.
// users.ts
import { Pool } from 'pg';
export async function getUser(id: string) {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
return rows[0];
}
This code gets several things right.
- The function has a clear input and responsibility
- The query uses a
$1parameter instead of concatenatingidinto SQL - The asynchronous operation is awaited before the function returns
pool.query()automatically checks out and returns a client for one query- The code is short enough to read and unit-test easily
AI can produce code like this, and a junior developer can genuinely read, verify, and extend it. Saying only that “AI-generated code cannot be trusted” does not explain the problem in this use case.
A unit test can reasonably mock pg and verify the query and parameters. That test can pass while leaving another question untouched: how many times does the function run in the real application, and what does each invocation create?
What is missing is not a broken line
The prompt may ask only for “a function that fetches a user from the database.” AI answers within the function’s scope: construct what it needs, run the query, and return the result. That answer is complete inside the question’s boundary, while the problem appears at a larger boundary.
- FunctionWhere the prompt is usually asked
What you check hereRight return value, types pass
- FileWhere the prompt is usually asked
What you check hereImports resolve, this file's tests pass
- Module graph
What you check hereWho else constructs this client
- ProcessWhere this article's problem appears
What you check hereHow long an instance lives, who closes it
- FleetWhere this article's problem appears
What you check hereHow many processes, each with its own copy
Every call to getUser() executes new Pool() once. If the function sits in a request handler, the number of Pools grows with the number of requests—even though a Pool exists to keep database clients available for reuse.
There is no syntax error, no type error, and no obviously alarming line. The missing questions concern scope and lifetime.
- Should the Pool belong to the function, request, module, or process?
- Can the next request reach and reuse the same Pool?
- When requests overlap, what determines the number of Pools and connections?
- Who owns configuring and shutting it down?
A developer who has already seen connection exhaustion or a process that would not shut down tends to ask these questions automatically. Someone who has not encountered those failures is not less capable; that event simply does not exist in their mental model yet.
A shared instance makes the gap visible
According to the pg.Pool documentation, a Pool starts empty and creates clients when queries need them. pool.query() returns a client to the Pool after use. In the example, however, the next request cannot reach the previous Pool because the Pool was created inside the function.
When requests overlap, each request may therefore open a connection through its own Pool instead of sharing a bounded set of connections. This does not happen because the junior developer forgot the API. It happens because they have not yet seen where this object belongs in the running system.
- 100overlapping requests
- ≈100separate Pools
- ≈100connection attempts in the burst
- Reuse
- The client returns to a Pool no later request can reach
- After the query
- The idle client waits for that Pool’s idle timeout
- Owner
- No central place owns configuration and shutdown
- 100overlapping requests
- 1shared Pool
- ≤10active clients; the rest wait
- Reuse
- Every request can reach the same Pool
- After the query
- The idle client is reusable until its idle timeout
- Owner
- The database module owns configuration and shutdown
This example assumes one overlapping query per request and a shared Pool configured with max: 10. The number accepted by the database still depends on the system’s connection budget and timeouts.
For a long-lived Node.js application, the answer is much shorter than the explanation: create the Pool at module scope and import the same instance wherever it is needed.
// db.ts
import { Pool } from 'pg';
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
});
// users.ts
import { pool } from './db';
export async function getUser(id: string) {
const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
return rows[0];
}
Node.js reuses a module that resolves to the same identity—CommonJS uses the resolved filename, while ESM uses the resolved URL—so callers inside one runtime receive the same exports. The useful starting model is one Pool per runtime rather than one per request. Multiple processes, hot reload, and serverless environments have different boundaries and must be evaluated against the actual runtime.
The lesson is not the word “singleton” or the illustrative max: 10. It is the question: how many of this object should exist, and how long should it live? The same question applies to HTTP clients, SDKs, caches, queue consumers, and many other resources.
AI can help produce code before experience reveals some consequences
In the past, developers often took longer to reach a working API. They read documentation, corrected syntax errors, and struggled with unfamiliar libraries along the way. Not all of that friction was useful, and there is no reason to make a new generation suffer through it. Some of it, however, forced a developer to ask how a library behaved.
AI removes much of that friction. A junior can reach production-shaped code sooner, which is a real advantage. But visible output can now run ahead of the invisible mental model. If a team treats tidy code as proof of complete understanding, the missing context may first appear under load or during failure instead of during review.
A January 2026 Anthropic experiment asked 52 participants, mostly junior software engineers, to learn an unfamiliar Python library. The AI group averaged 50% on a post-task quiz, compared with 67% for the hand-coding group, while its roughly two-minute speed advantage was not statistically significant. The sample was small, the test was immediate, and the setup used a sidebar assistant, so the result cannot stand in for every tool or long-term outcome.
More useful than the headline number is what the higher-scoring AI users did: they asked follow-up questions, requested explanations, or used AI for conceptual inquiry rather than generation alone. The lesson is not to stop using AI. It is that using AI to finish and using AI to understand are different working modes.
Senior engineers need to teach questions, not just return a correct patch
If a review comment says only “move the Pool to db.ts,” the junior receives correct code without necessarily receiving the reason. When the next object is an HTTP client instead of a Pool, the same gap can return under a different name.
A review that transfers the mental model should reach questions like these:
- Does this function run once per request or once per process?
- Which resource does the object hold behind its API?
- Do we need one per user, request, module, process, or system?
- If 100 calls overlap, which number becomes 100?
- Who owns shutdown, retry, and recovery when the dependency fails?
- Which test would reveal this issue rather than merely prove that each line works?
A comment that supplies the answer: “Move the Pool to
db.ts.”A comment that builds understanding: “This function runs on every request. With 100 requests, how many Pools exist—and what problem is a Pool meant to solve?”
When the junior answers that question, they are not memorising a patch. They are adding scope and lifetime to their set of reasoning tools. AI can help here too: compare both designs, explain the hidden resource, or propose failure scenarios for the team to inspect together.
Shared instances are only the first case
The gap between “writes good code” and “sees the whole system” is not limited to Pools. Other examples have the same shape.
- Module-level state that unintentionally shares one request’s data with another
- Retries that are locally reasonable at each layer but multiply one operation when combined
- Queries that are individually correct but do not share the same transaction connection
- Background jobs that succeed once but are not idempotent when delivered again
- Logs that help debugging while also capturing secrets or user data
Each example can look good inside its own file. The problem lives in the relationship between files, time, users, and infrastructure—something syntax alone cannot teach.
Junior developers in the AI era do not have to write worse code. They may write good code earlier than previous generations did. What teams must design alongside that speed is review, pairing, and a set of questions that lets system understanding grow just as quickly.
If the work involves reviewing architecture and development practices, see the scope of Nixxel’s software development service to assess what the project needs.
Sources and further reading
- Anthropic: How AI assistance impacts the formation of coding skills
- node-postgres: Pooling
- node-postgres:
pg.PoolAPI - node-postgres: Suggested project structure
- Node.js: CommonJS module caching
- Node.js: ECMAScript modules and URL-based caching
- Type-safe forms in Astro: Validate FormData without casts
- Rust vs. Go in the Agentic AI Era: Which Should We Choose?

