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 Pool created?

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 $1 parameter instead of concatenating id into 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.

The scope a prompt is asked at and the scope where the problem appears are rarely the same layer.
  1. FunctionWhere the prompt is usually asked

    What you check hereRight return value, types pass

  2. FileWhere the prompt is usually asked

    What you check hereImports resolve, this file's tests pass

  3. Module graph

    What you check hereWho else constructs this client

  4. ProcessWhere this article's problem appears

    What you check hereHow long an instance lives, who closes it

  5. 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.

A Pool is not a connection: where it is constructed determines how traffic fans out into connection attempts.
Construct a Pool in the request handler
  1. 100overlapping requests
  2. ≈100separate Pools
  3. ≈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
Share a Pool inside one runtime
  1. 100overlapping requests
  2. 1shared Pool
  3. ≤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