A junior developer recently asked me if he was falling behind because he used AI less than his peers.
I said - "Using AI for everything early in your career buys speed at the cost of mental models".
The cache bugs I wrote by hand years ago taught me why write ordering and TTL expiry interact badly. That lesson only stuck because I watched my own wrong intuition fail in production and had to trace the logs myself. A generated fix skips the failure, and the failure was the teacher.
If you accept AI fixes without understanding the underlying failure, you trade a quick fix today for a system you cannot debug when something unusual breaks.
Here is how I approach this now -
- Form a hypothesis first,
- use the model to find blind spots, and
- ration where AI is allowed to write code.
- The Hypothesis-First AI Loop The most common trap with coding agents is pasting an error log into the prompt and asking the model to fix it.
When you do that, you skip the step where you build a mental model of the system. The model returns code that compiles and passes the immediate test, but you do not learn why the failure happened or what secondary effects the change introduces.
A more reliable loop keeps the engineer responsible for the hypothesis while using the model to stress-test assumptions.

Figure 1: The four-step loop for pairing with coding agents.
The Four Steps:
- Form a hypothesis: Before opening a prompt, write down what you think is failing and why. Even if your guess is wrong, stating it forces you to model the system in your head.
- Ask the model for alternatives: Instead of asking for code, ask the model what other failure modes could produce the same symptom. For example: "I suspect this timeout is caused by database connection pool saturation. What other downstream bottlenecks could produce this error pattern?"
- Verify against empirical evidence: Check distributed traces, server logs, and queue metrics to see which explanation fits the data.
- Update your understanding: Compare the actual root cause with your original hypothesis. The gap between what you thought happened and what the telemetry showed is where your understanding improves.
2. A Common Failure Mode: Naive Retries and Pool Exhaustion
Here is an example I have seen in production code generated by AI assistants.
A service calling an external API starts seeing intermittent HTTP 504 gateway timeouts. When given the error log, the model suggests adding a retry loop with an increased timeout.
# Anti-pattern: Naive retry loop masking systemic failure
import time
import requests
def call_upstream_service(url: str, payload: dict, max_retries: int = 5):
# Model suggested increasing timeout from 3s to 10s and retrying 5 times
timeout = 10.0
for attempt in range(1, max_retries + 1):
try:
response = requests.post(url, json=payload, timeout=timeout)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as exc:
if attempt == max_retries:
raise RuntimeError(f"Service failed after {max_retries} attempts: {exc}")
# Predictable exponential backoff without jitter
sleep_duration = 2 ** attempt
time.sleep(sleep_duration)
In isolated unit tests, this snippet passes. Under production load, it causes several problems:
- Synchronized retry storms: Because the backoff uses fixed powers of two without jitter, hundreds of concurrent callers retry at the exact same intervals, overwhelming the upstream service as it tries to recover.
- Socket and thread exhaustion: Increasing the timeout from 3 seconds to 10 seconds means threads hold outbound connections three times longer. Under steady traffic, the web server exhausts its worker pool, and incoming requests queue up until the health check fails.
- Latency amplification: Instead of failing fast within 3 seconds so the caller can degrade gracefully, requests hang for up to a minute before failing anyway.

Figure 2: How an unjittered retry loop exhausts connection pools compared to a bounded circuit-breaker pattern.
The model suggested this implementation because retrying with exponential backoff is standard boilerplate in its training data. It has no visibility into your thread pool limits, traffic distribution, or upstream capacity.
3. Resilient Implementation: Bounded Retries and Circuit Breaking
When you know the failure dynamics, you do not let the model pick the strategy. You define the bounds first: maximum latency, randomized jitter, and a circuit breaker that sheds load when the downstream service is down.
Here is a Python implementation that enforces those boundaries:
import random
import time
from dataclasses import dataclass
from enum import Enum
from typing import Callable, Any
class CircuitState(Enum):
CLOSED = "CLOSED"
OPEN = "OPEN"
HALF_OPEN = "HALF_OPEN"
@dataclass(frozen=True)
class ResilienceConfig:
max_attempts: int = 3
base_backoff_sec: float = 0.2
max_backoff_sec: float = 2.0
failure_threshold: int = 5
recovery_timeout_sec: float = 15.0
class CircuitBreakerOpenException(Exception):
"""Raised when requests are shed immediately because the circuit is open."""
pass
class BoundedResilientClient:
"""
Client that limits retry duration, adds full jitter,
and protects thread pools through circuit breaking.
"""
def __init__(self, name: str, config: ResilienceConfig):
self.name = name
self.config = config
self.state = CircuitState.CLOSED
self.failure_count = 0
self.last_state_change = time.monotonic()
def _on_failure(self):
self.failure_count += 1
if self.failure_count >= self.config.failure_threshold:
self.state = CircuitState.OPEN
self.last_state_change = time.monotonic()
def _on_success(self):
self.failure_count = 0
self.state = CircuitState.CLOSED
def _compute_backoff_with_jitter(self, attempt: int) -> float:
ceiling = min(
self.config.max_backoff_sec,
self.config.base_backoff_sec * (2 ** (attempt - 1))
)
return random.uniform(0, ceiling)
def execute(self, action: Callable[[], Any]) -> Any:
now = time.monotonic()
if self.state == CircuitState.OPEN:
if now - self.last_state_change > self.config.recovery_timeout_sec:
self.state = CircuitState.HALF_OPEN
else:
# Fail immediately to protect local thread and connection pools
raise CircuitBreakerOpenException(f"{self.name} circuit is OPEN. Request shed.")
for attempt in range(1, self.config.max_attempts + 1):
try:
result = action()
self._on_success()
return result
except Exception:
if attempt == self.config.max_attempts or self.state == CircuitState.HALF_OPEN:
self._on_failure()
raise
backoff = self._compute_backoff_with_jitter(attempt)
time.sleep(backoff)
This pattern prevents cascading issues in three ways:
- Immediate rejection: When the circuit is open, requests fail in fractions of a millisecond, leaving thread pools free for healthy routes.
- De-synchronized traffic: Full jitter spreads retries evenly across time, avoiding thundering herd spikes on recovering services.
- Strictly bounded wait time: Retries cap out quickly rather than holding resources for long timeouts.
4. The AI Task Rationing Matrix
To build and preserve engineering judgment, categorize engineering tasks by the risk of outsourcing understanding:

Figure 3: Framework for delegating engineering tasks to AI based on cognitive value and risk.
| Category | Typical Tasks | Role for AI | Review Rule |
|---|---|---|---|
| Low Risk (High Leverage) | Syntax lookups, boilerplate, regular expressions, test setup, CLI flags | Full delegation | Verify syntax and run existing tests. Relearning these syntax details adds little value. |
| Medium Risk (Collaborative) | Edge cases, schema validation, test coverage gaps, local refactoring | Sounding board | Write the core logic yourself first. Ask the model what inputs or boundaries you missed. |
| High Risk (Critical Core) | Concurrency controls, database transactions, auth/permissions, cache invalidation | Adversarial reviewer | Design and write alone. Use the model only to generate adversarial test cases against your finished contracts. |
5. Practical Guidelines for Teams
Before merging code authored or suggested by an agent, review against these checks:
- [ ] Can you explain the execution path and edge cases without consulting the prompt?
- [ ] Did you state a hypothesis about the bug before asking the model for a fix?
- [ ] Are failure modes (timeouts, 429 rate limits, partial payloads) verified with actual test assertions rather than happy-path mocks?
- [ ] Are retries, timeouts, and resource pools strictly bounded?
- [ ] Does the change solve the root cause rather than increasing a buffer or timeout to mask a bottleneck?
6. Summary
Coding tools are fast, but they do not build mental models for you.
Point AI at the parts of your work where being wrong has low consequence: typing boilerplate, scaffolding tests, and looking up syntax. Keep your hands on the core logic: state management, failure modes, and system boundaries.
Top comments (0)