Skip to content

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:

const user = data as User;  // What if data is not actually a User?

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