Constraints and Validation
Agents satisfy the requirements you state and silently drop the ones you assume. A senior engineer "just knows" that an external HTTP call needs a timeout; an agent knows that too, but under pressure to complete the visible task, unstated constraints are the first thing cut. Non-functional requirements must therefore exist as text the agent reads every session — in CLAUDE.md or the spec — not as review comments you repeat weekly.
Constraint blocks that actually bind
A constraint binds when it is checkable ("every external call has a timeout") and floats when it is aspirational ("the system should be resilient"). Write each one so that a reviewer — human or agent — could grep for violations. Below, each area gets a copyable block; put project-wide ones in CLAUDE.md, feature-specific ones in the spec. See CLAUDE.md and AGENTS.md for placement mechanics.
Scalability
Agents write code that works at demo scale: unbounded queries, N+1 loops, in-memory accumulation of "all the rows". State your real numbers — an agent told "500k invoices" will paginate; an agent told nothing will findAll().
## Scalability constraints
- Assume: 500k invoices, 40k users, largest tenant = 15% of all data.
- Every list endpoint: paginated (cursor-based), max page size 100.
- No unbounded queries: every SELECT on invoices/events tables has a
LIMIT or a WHERE on an indexed column. Flag any new full-table scan.
- No per-row queries inside loops (N+1). Batch or join.
- Any operation touching >1k rows runs as a background job, not in the
request cycle.Security
The dangerous agent failure isn't the exotic vulnerability — it's the mundane one replicated 30 times: string-built SQL, secrets in code, missing authz checks on "internal" endpoints. Agents also cheerfully log entire request objects, tokens included.
## Security constraints
- All SQL through the query builder / parameterized statements. String
concatenation into SQL is a blocking review failure.
- Every endpoint declares its authz rule explicitly; there are no
"internal, no auth needed" routes. Middleware default is deny.
- Secrets only via env/secret manager. Never in code, config files,
test fixtures, or example docs — including placeholder-looking real keys.
- Never log: tokens, passwords, full request bodies, PII fields
(email is PII here). Log user IDs, not user objects.
- New dependency = justification in the PR body (see quality gates).Observability
Agent-written failure paths tend to be catch (e) { console.log(e) } — technically handled, operationally useless. Specify the shape of telemetry, because "add logging" produces noise while "structured log with request id" produces something you can query at 3 a.m.
## Observability constraints
- Every external call (HTTP, DB, queue, cache): explicit timeout,
defined retry policy (or explicit "no retry" with reason), and a
structured log line with request_id, target, duration_ms, outcome.
- Logs are structured JSON via our logger (src/lib/log.ts). No bare
console.log outside scripts/.
- Every background job logs start/end/failure with job id and duration.
- New user-facing failure modes get a metric, not just a log line.
- request_id propagates across service and queue boundaries.Maintainability
This is where architecture erosion starts: each agent session adds one more "pragmatic" bypass of the layering. Encode the load-bearing boundaries.
## Maintainability constraints
- Layering: routes -> services -> repositories. Route handlers never
import the DB client directly (enforced by eslint import rules).
- No new top-level directories or cross-cutting abstractions without
an ADR (see docs/decisions/).
- Copy-paste threshold: third occurrence of a pattern gets extracted;
before that, duplication is fine. Do not build abstractions for
hypothetical future cases.
- Public function signatures in src/services/ are a contract: changing
one requires updating all callers in the same PR.Diagrams from the agent, committed to the repo
Agents produce competent Mermaid, and unlike a whiteboard photo it lives in Git, renders in your docs site, and — critically — gets read back by future agent sessions as architectural context. Two high-value asks:
Read src/ and generate two Mermaid diagrams for docs/architecture.md:
1. flowchart LR of the modules in src/ and their actual import
dependencies (only what exists in code — do not draw the intended
architecture, draw the real one).
2. sequenceDiagram of the invoice-creation flow from POST /invoices
to the PDF landing in S3, including the queue hop and every
external call with its timeout.
Max 15 nodes each. If the real dependency graph violates the layering
described in CLAUDE.md, list the violations below the diagram instead
of hiding them.The as-built-not-as-intended instruction is the whole trick: agents default to drawing the flattering version. Diffs to these diagrams in later PRs are an early-warning system — a new arrow from routes to db is erosion made visible.
Validating a proposed architecture: the challenge prompt
An agent that proposed a design will defend it; the same agent, explicitly re-roled as an adversary and fed concrete failure scenarios, critiques it well. Generic "what are the risks?" yields generic risks. Name the scenarios — load multiples, resource exhaustion, credential compromise, dependency death — and demand mechanism-level answers.
Adversarial review of the proposed design in docs/specs/notifications.md.
Role: staff engineer doing a production-readiness review. You did not
write this design and you do not need to like it.
Walk through each scenario. For each: what EXACTLY happens (component
by component), what the user sees, what the on-call sees, and whether
recovery is automatic or manual.
1. Load is 100x the stated estimate for one hour (viral tenant).
Where does it break first? What backs up behind that?
2. The notifications queue consumer dies Friday 6pm and nobody notices
until Monday. Queue depth? Message TTLs? What is lost vs delayed?
What happens at restart — thundering herd?
3. The SendGrid API key leaks. Blast radius? How do we rotate without
downtime? What tells us it leaked?
4. Postgres fails over mid-send. Which notifications double-send?
Where is the idempotency key, and who checks it?
5. A tenant configures a webhook target that hangs for 30s per call.
Does that tenant degrade everyone else?
Rules: no fixes yet, findings first. Rate each finding: blocking /
should-fix / acceptable-with-monitoring. "It should be fine" is not
an acceptable answer — show the mechanism or flag it as unknown.Run this in a fresh session when possible — a clean context has no authorship pride and no accumulated agreement with you. Findings rated "blocking" go back into the design; the decision and its survived challenges go into an ADR (see Decision Records), so next quarter's session doesn't re-argue what scenario 2 already settled.
Related
- Decision Records — where validated decisions go so they persist across sessions.
- Exploring Options — generating the candidate designs this page stress-tests.
- CLAUDE.md and AGENTS.md — where constraint blocks live and how agents consume them.
- Quality Gates — turning constraints into mechanical CI enforcement instead of prose.
- Quality Prompts — more review and validation prompts in library form.