DOMAIN:BACKEND:PITFALLS¶
OWNER: urszula (Team Alfa), maxim (Team Bravo)
UPDATED: 2026-03-24
SCOPE: backend anti-patterns, with special focus on LLM-generated code pitfalls
SEVERITY: each pitfall rated CRITICAL, HIGH, or MODERATE
AGENTS: all backend agents + antje (TDD), koen (code quality)
PITFALLS:LLM_SPECIFIC¶
LLM_01:SCAFFOLDING_WITHOUT_IMPLEMENTATION¶
SEVERITY: CRITICAL
DESCRIPTION: LLM generates function signatures, class structures, and file scaffolding, but bodies contain TODO comments, placeholder returns, or throw new Error('Not implemented').
DETECTION: search for TODO, FIXME, not implemented, throw new Error in generated code
WHY_IT_HAPPENS: LLMs optimize for structural completeness, not behavioral completeness. They fill the shape but skip the substance.
ANTI_PATTERN:
async function processPayment(orderId: string, amount: number): Promise<PaymentResult> {
// TODO: Implement payment processing
throw new Error('Not implemented');
}
FIX: every function must have a real implementation or be explicitly marked as a future stub with a linked work item
RULE: GE agents never leave TODO/FIXME in code — if you cannot implement it, do not create the function
CHECK: before marking task complete, search for TODO|FIXME|not implemented|NotImplementedError — if found, task is NOT complete
LLM_02:SILENT_FAILURES_AND_FAKE_OUTPUT¶
SEVERITY: CRITICAL
DESCRIPTION: LLM generates code that runs without errors but produces incorrect results. It may remove safety checks, return hardcoded values, or create fake output matching expected format.
WHY_IT_HAPPENS: LLMs optimize for code that compiles and runs, not code that is correct. Recent models (2025-2026) fail silently more than older models that crashed loudly.
ANTI_PATTERN:
// Looks correct — runs without error — but always returns empty array
async function searchUsers(query: string): Promise<User[]> {
if (!query) return [];
const results = await db.select().from(users)
.where(ilike(users.name, query)); // Missing % wildcards
return results;
}
FIX: write tests with realistic data that verify actual behavior, not just shape
RULE: every function with business logic gets a test that verifies the CORRECT output, not just that it returns the right type
LLM_03:PACKAGE_HALLUCINATION¶
SEVERITY: HIGH
DESCRIPTION: LLM invents npm packages that do not exist, or references deprecated/renamed packages. Installing hallucinated packages may install malicious "slopsquatted" packages.
WHY_IT_HAPPENS: LLMs generate plausible-sounding package names based on patterns in training data. Attackers register these names with malicious code.
ANTI_PATTERN:
import { validateSchema } from 'hono-zod-helpers'; // this package does not exist
import { drizzleUtils } from 'drizzle-orm-utils'; // this package does not exist
FIX: verify every import against npm registry before installing
CHECK: npm info <package-name> — if it returns 404, the package does not exist
RULE: only use packages from GE's approved dependency list or well-known publishers (honojs, drizzle-team, sindresorhus, etc.)
LLM_04:OUTDATED_API_USAGE¶
SEVERITY: HIGH
DESCRIPTION: LLM generates code using deprecated APIs, removed methods, or patterns from older versions. Common with Zod (v3 vs v4), Drizzle (frequent API changes), Hono (v3 vs v4).
WHY_IT_HAPPENS: LLM training data includes old tutorials, Stack Overflow answers from 2021-2023, and deprecated documentation.
EXAMPLES:
- Zod v4: use .issues not .errors on ZodError
- Drizzle: pgTable index callback now returns an array, not an object
- Hono: c.req.json() is deprecated — use c.req.valid('json') with zValidator
- Node.js: require() in ESM context — use import
RULE: always check the CURRENT version of the library being used
TOOL: npm ls <package> to verify installed version
LLM_05:SECURITY_BLIND_SPOTS¶
SEVERITY: CRITICAL
DESCRIPTION: 62% of LLM-generated code contains security vulnerabilities (2024 benchmark, 9 models). Common: missing input validation, hardcoded secrets, SQL injection via string concatenation, missing auth checks.
WHY_IT_HAPPENS: LLMs learn from public code — and most public code has security issues. Models optimize for functionality, not security.
ANTI_PATTERN:
// Missing auth check — any user can delete any user
app.delete('/users/:id', async (c) => {
await db.delete(users).where(eq(users.id, c.req.param('id')));
return c.json({ success: true });
});
FIX: every mutation endpoint must have auth middleware + authorization check
CHECK: does every DELETE/PUT/POST endpoint have authMiddleware and optionally requireRole()?
LLM_06:OVER_ENGINEERING¶
SEVERITY: MODERATE
DESCRIPTION: LLM generates overly complex abstractions for simple problems. Factory patterns for one implementation, generic base classes used once, event systems for synchronous code.
WHY_IT_HAPPENS: LLMs have seen many enterprise Java patterns and apply them where simplicity would suffice.
ANTI_PATTERN:
// Abstract factory for creating a single type of thing
abstract class BaseRepositoryFactory<T> {
abstract createRepository(): IRepository<T>;
}
class UserRepositoryFactory extends BaseRepositoryFactory<User> {
createRepository() { return new UserRepository(); }
}
// Just... use the UserRepository directly
FIX: if there is only one implementation, do not abstract. YAGNI (You Ain't Gonna Need It).
RULE: abstractions are justified only when there are 2+ concrete implementations or clear test isolation needs
PITFALLS:DATABASE¶
DB_01:N_PLUS_1_QUERIES¶
SEVERITY: CRITICAL
DESCRIPTION: fetching a list of items, then querying for related data inside a loop. If there are 100 items, this generates 101 queries instead of 1-2.
ANTI_PATTERN:
const projects = await db.select().from(projects);
for (const project of projects) {
// Runs 1 query PER project — devastating at scale
const tasks = await db.select().from(tasks).where(eq(tasks.projectId, project.id));
project.tasks = tasks;
}
FIX:
// Single query with relational API
const projects = await db.query.projects.findMany({
with: { tasks: true },
});
DETECTION: search for await inside for/forEach/map loops that query the database
TOOL: enable Drizzle query logging and look for repeated queries
DB_02:CONNECTION_POOL_EXHAUSTION¶
SEVERITY: CRITICAL
DESCRIPTION: every request opens a new database connection without pooling, or pool is too small for traffic, causing requests to queue and timeout.
ANTI_PATTERN:
// Creating new connection PER request
app.get('/users', async (c) => {
const sql = postgres(process.env.DATABASE_URL); // new connection
const db = drizzle(sql);
const users = await db.select().from(users);
// Connection never closed
return c.json(users);
});
FIX: create connection pool ONCE at startup, reuse via shared db instance
CHECK: is postgres() called once at module level, or inside request handlers?
DB_03:MISSING_INDEXES¶
SEVERITY: HIGH
DESCRIPTION: queries on large tables without indexes cause full table scans, degrading performance exponentially as data grows.
CHECK: every column used in WHERE, JOIN ON, and ORDER BY clauses should have an index
CHECK: foreign key columns MUST be indexed (PostgreSQL does not auto-index foreign keys)
TOOL: EXPLAIN ANALYZE <query> — look for "Seq Scan" on tables with >1000 rows
DB_04:TRANSACTION_MISUSE¶
SEVERITY: HIGH
ANTI_PATTERN: wrapping reads in transactions unnecessarily
FIX: transactions are for multi-step writes that must be atomic
ANTI_PATTERN: calling external APIs inside transactions (holding locks while waiting for network)
FIX: read data, call external API, then open transaction for final write
ANTI_PATTERN: transactions spanning user interaction (keeping transaction open for seconds/minutes)
FIX: transactions should complete in milliseconds
DB_05:MIGRATION_WITHOUT_REVIEW¶
SEVERITY: HIGH
DESCRIPTION: running drizzle-kit push or auto-generated migrations without reviewing the SQL. Drizzle may generate destructive DDL (DROP COLUMN, ALTER TYPE).
RULE: always read the generated .sql file before applying to any non-local environment
CHECK: does the migration contain DROP, ALTER TYPE, or RENAME? If yes, use expand-contract pattern.
PITFALLS:API¶
API_01:UNBOUNDED_RESPONSES¶
SEVERITY: HIGH
DESCRIPTION: returning all rows from a table without pagination. Fine with 10 rows, breaks with 100,000.
ANTI_PATTERN:
app.get('/users', async (c) => {
const users = await db.select().from(users); // ALL users
return c.json(users);
});
FIX: always paginate. Default pageSize=20, max pageSize=100. See api-design.md.
API_02:MISSING_INPUT_VALIDATION¶
SEVERITY: CRITICAL
DESCRIPTION: trusting client input without validation. Leads to data corruption, injection, and crashes.
ANTI_PATTERN:
app.post('/users', async (c) => {
const body = await c.req.json(); // unvalidated — could be anything
await db.insert(users).values(body); // might crash, might inject
});
FIX: use zValidator with Zod schema on EVERY endpoint that accepts input
API_03:SECRETS_IN_CODE¶
SEVERITY: CRITICAL
DESCRIPTION: hardcoded API keys, database URLs, JWT secrets in source code. Even in private repos, this is a security risk (leaked in logs, CI output, container images).
ANTI_PATTERN:
const JWT_SECRET = 'super-secret-key-123';
const DB_URL = 'postgresql://admin:password@localhost:5432/db';
FIX: all secrets from environment variables, validated with Zod env schema at startup
CHECK: search codebase for common secret patterns: password, secret, api_key, token as string literals
API_04:ERROR_LEAKING¶
SEVERITY: HIGH
DESCRIPTION: returning raw error messages, stack traces, or database errors to clients. Reveals internal implementation details.
ANTI_PATTERN:
try {
await db.insert(users).values(data);
} catch (err) {
return c.json({ error: err.message }, 500);
// Client sees: "duplicate key value violates unique constraint \"users_email_key\""
}
FIX: map database/internal errors to generic client-facing messages. Log full error server-side.
PITFALLS:NODEJS¶
NODE_01:UNHANDLED_PROMISE_REJECTIONS¶
SEVERITY: CRITICAL
DESCRIPTION: unhandled promise rejections crash Node.js (default since Node.js 15). Missing await, missing .catch(), or fire-and-forget promises.
ANTI_PATTERN:
app.post('/send-email', async (c) => {
// Fire and forget — if this fails, it crashes the process
sendEmail(c.req.valid('json').email);
return c.json({ success: true });
});
FIX:
// Option 1: await it
await sendEmail(email);
// Option 2: explicit error handling for fire-and-forget
sendEmail(email).catch((err) => logger.error({ err }, 'Failed to send email'));
RULE: always handle unhandledRejection at process level as safety net
NODE_02:SYNC_IO_IN_REQUEST_PATH¶
SEVERITY: HIGH
DESCRIPTION: using synchronous I/O (fs.readFileSync, crypto.pbkdf2Sync) in request handlers blocks the event loop for ALL requests.
DETECTION: search for Sync in request handler files
FIX: use async versions (fs.readFile, crypto.pbkdf2) or move to worker threads
NODE_03:MEMORY_LEAKS¶
SEVERITY: HIGH
SOURCES:
- Unbounded Map/Set used as cache (grows forever)
- Event listeners added but never removed (especially on long-lived connections)
- Closures capturing large objects
- Unclosed database connections / Redis subscriptions
DETECTION:
- Monitor process.memoryUsage().heapUsed over time — steady growth = leak
- Take heap snapshots 5 minutes apart, compare retained sizes
- Watch for increasing GC pause times
FIX: use LRU caches with max size, remove event listeners on cleanup, close connections on shutdown
NODE_04:MISSING_GRACEFUL_SHUTDOWN¶
SEVERITY: HIGH
DESCRIPTION: process exits immediately on SIGTERM without closing connections. Causes connection pool exhaustion, orphaned database locks, partial writes.
CHECK: does the application handle SIGTERM?
IF: no THEN: see node-production.md graceful shutdown pattern
KUBERNETES_IMPACT: k8s sends SIGTERM, waits 30s (terminationGracePeriodSeconds), then SIGKILL. Without handler, all in-flight requests are killed.
PITFALLS:TYPESCRIPT¶
TS_01:ANY_EVERYWHERE¶
SEVERITY: HIGH
DESCRIPTION: using any to silence TypeScript errors. Disables all type checking for that value, spreading unsafety through the codebase.
DETECTION: grep -r "any" --include="*.ts" | grep -v "node_modules"
FIX: use unknown and narrow with type guards, or fix the actual type
EXCEPTION: third-party library definitions that genuinely require any (annotate with comment)
TS_02:TYPE_ASSERTION_ABUSE¶
SEVERITY: MODERATE
DESCRIPTION: using as to force types instead of fixing the actual type error. Hides bugs that TypeScript would otherwise catch.
ANTI_PATTERN:
FIX:
const parsed = userSchema.safeParse(data);
if (!parsed.success) throw new Error('Invalid user data');
const user = parsed.data; // Validated at runtime, typed at compile time
TS_03:SEPARATE_TYPE_AND_SCHEMA¶
SEVERITY: MODERATE
DESCRIPTION: defining a TypeScript interface AND a Zod schema for the same shape separately. They inevitably drift apart.
ANTI_PATTERN:
interface User { id: string; name: string; email: string; }
const userSchema = z.object({ id: z.string(), name: z.string() });
// email is missing from schema — no compile-time error
FIX: define Zod schema first, infer type: type User = z.infer<typeof userSchema>
PITFALLS:ARCHITECTURE¶
ARCH_01:GOD_SERVICE¶
SEVERITY: HIGH
DESCRIPTION: single service file handling all business logic for an entire domain. Becomes untestable and unmaintainable.
DETECTION: service file > 500 lines
FIX: split into focused functions, one per use case (vertical slices)
ARCH_02:CIRCULAR_DEPENDENCIES¶
SEVERITY: HIGH
DESCRIPTION: module A imports from module B, which imports from module A. Causes undefined imports at runtime.
DETECTION: use madge --circular src/ to detect cycles
FIX: extract shared types/interfaces into a separate module that both depend on
ARCH_03:BUSINESS_LOGIC_IN_ROUTES¶
SEVERITY: HIGH
DESCRIPTION: putting business rules, validation, and data transformation directly in Hono route handlers. Makes it untestable and framework-coupled.
ANTI_PATTERN:
app.post('/projects', async (c) => {
const body = await c.req.json();
// 50 lines of business logic directly in handler
if (body.budget > 100000 && user.role !== 'admin') { ... }
// ...
});
FIX: route handler should ONLY do: validate input, call service, map Result to HTTP response (max 10-15 lines)
ARCH_04:DUAL_WRITE_WITHOUT_OUTBOX¶
SEVERITY: HIGH
DESCRIPTION: writing to database AND publishing to Redis/message queue in the same request without transactional guarantee. If one succeeds and the other fails, data is inconsistent.
FIX: use transactional outbox pattern — write event to DB in same transaction, relay to Redis separately. See architecture-patterns.md.
PITFALLS:GE_SPECIFIC¶
GE_01:REDIS_PORT¶
SEVERITY: HIGH
DESCRIPTION: GE Redis runs on port 6381, NOT the default 6379. Hardcoding 6379 causes connection failures.
FIX: read from config/ports.yaml, not from memory
GE_02:ZOD_V4_ISSUES¶
SEVERITY: MODERATE
DESCRIPTION: Zod v4 renames .errors to .issues on ZodError. Code using .errors silently gets undefined.
FIX: always use .issues when accessing ZodError fields
GE_03:XADD_WITHOUT_MAXLEN¶
SEVERITY: CRITICAL
DESCRIPTION: Redis XADD without MAXLEN creates unbounded streams that consume unbounded memory. Caused $100/hr token burn in GE history.
RULE: EVERY XADD call MUST include MAXLEN — 100 for per-agent streams, 1000 for system streams
GE_04:HOT_PATCHING_PODS¶
SEVERITY: CRITICAL
DESCRIPTION: using kubectl cp to patch running pods. Python/Node.js cache modules at startup — copied files are not picked up.
FIX: always rebuild container image and restart deployment
PITFALLS:AGENTIC_CHECKLIST¶
BEFORE_MARKING_TASK_COMPLETE:
1. SEARCH: TODO|FIXME|not implemented|NotImplementedError — must be zero results
2. SEARCH: any in TypeScript files — must be zero or justified with comment
3. SEARCH: Sync in request handler files — must be zero
4. SEARCH: password|secret|api_key|token as string literals — must be zero
5. CHECK: every endpoint has input validation (zValidator)
6. CHECK: every list endpoint has pagination
7. CHECK: every mutation has auth middleware
8. CHECK: N+1 queries — no await inside loops that query DB
9. CHECK: graceful shutdown handler present
10. CHECK: MAXLEN on every XADD call
11. RUN: vitest — zero failures
12. RUN: npx tsc --noEmit — zero errors