Engineering Principles

Modern Software Engineering in Anger

Practical principles for building software in the age of AI agents

Core Principles

High-quality software should optimize for the following attributes:

  1. Joyful and Intuitive User Experience — Interactions should feel obvious, fast, and pleasant, such that users reach for the application by choice rather than obligation.
  2. Correctness — The software reliably does what it is intended to do.
  3. Clarity — The code's purpose, behavior, and rationale are clear.
  4. Simplicity — The system uses no more complexity than the problem requires.
  5. Concision — Important information is easy to see and unnecessary noise is minimized.
  6. Maintainability — The system can be understood, modified, tested, and operated safely.
  7. Extensibility — Expected areas of future change can evolve without destabilizing unrelated parts of the system.
  8. Cost Efficiency — Architecture should avoid unnecessary standing infrastructure and keep idle cost close to zero whenever practical.
  9. Security - Minimize trust, privilege, exposed surface area, and sensitive data.
  10. Operability - Production software should make its health, behavior, and failures understandable.
  11. Infrastructure as Code - Treat deployment infrastructure as versioned software.
  12. Consistency — Similar problems are solved in similar ways using established conventions.
  13. Documentation and Agent Context - The smallest durable body of knowledge that allows a human or agent to make good decisions about the system.

1Joyful and Intuitive User Experience

Great engineering is only valuable if it produces software people actually want to use.

Software should be designed from the perspective of the person using it, not merely from the perspective of the person or agent implementing it.

Every interaction should aim to feel:

  • Obvious — Users understand what to do.
  • Fast — The system responds promptly and avoids unnecessary delay.
  • Predictable — Actions behave as users expect.
  • Forgiving — Mistakes are easy to recover from.
  • Pleasant — The experience feels polished and considered.

Optimize for the User's Goal

Design around what the user is trying to accomplish, not around the internal mechanics of the system.

Ask:

  • What is the job to be done?
  • What is the shortest reasonable path to that outcome?
  • How can the system make it as easy and pleasant as possible?

Do not expose internal system complexity unless the user benefits from understanding or controlling it.

Make the Common Path Obvious

The most common workflow should be immediately understandable.

Prefer:

  • Clear labels over clever terminology.
  • Familiar interaction patterns.
  • Sensible defaults over unnecessary configuration.
  • Clear next steps.
  • Progressive disclosure of advanced functionality.

Optimize for the common case without preventing more advanced use where it is genuinely needed.

Minimize Friction

Every additional field, click, screen, confirmation, setting, and decision-to-make is a user tax.

Avoid asking users to:

  • Repeat actions unnecessarily.
  • Configure options they do not need.
  • Understand implementation terminology.
  • Perform work that can be automated by the code.

Prefer removing unnecessary steps over making unnecessary steps faster.

Make It Feel Fast

Performance is part of the user experience.

Respond quickly to interactions, keep unnecessary work off the user's critical path, and show useful progress when waiting is unavoidable.

Keep Users Oriented and Recoverable

Users should understand what happened after they take an action.

Make important state changes visible, explain failures in understandable terms, preserve user work where possible, and provide a clear recovery path.

For destructive actions, provide appropriate safeguards. For easily reversible actions, prefer recovery or undo over excessive confirmation.

Hide Incidental Complexity

Implementation complexity should not automatically become product complexity.

The existence of multiple services, models, databases, queues, providers, or processing stages does not mean the user should have to reason about them.

Expose internal distinctions only when they correspond to meaningful user choices.

Design for Everyone

Accessibility is part of usability.

Use established accessibility standards and ensure that core workflows remain usable by people with different abilities, input methods, and assistive technologies.

Product Quality Is an Engineering Responsibility

Implementation decisions affect latency, responsiveness, error handling, recoverability, accessibility, and workflow complexity.

Engineers and coding agents should ask not only:

"Does this work?"

but also:

"What is it like to use?"

2Correctness

Software must reliably do what it is intended to do.

Elegant, maintainable software is still poor software if it produces wrong results, corrupts state, or fails unpredictably.

Make the Contract Explicit

Before implementing or changing behavior, identify:

  • Expected inputs and outputs.
  • Business rules and invariants.
  • Error and recovery behavior.
  • Important edge cases.
  • Security and authorization requirements.

Do not silently invent behavior when requirements are unclear. Make assumptions explicit.

Preserve Invariants and Data Integrity

Important truths about the system should be enforced, not merely documented.

Use types, schemas, constraints, transactional boundaries, and validation where appropriate to prevent invalid states.

Be especially deliberate about:

  • State transitions.
  • Concurrent updates.
  • Duplicate requests.
  • Retries.
  • Partial failures.
  • Operations that must be atomic or idempotent.

Validate at Boundaries

Treat user input, external APIs, files, queues, databases, and network calls as trust boundaries.

Validate data where responsibility changes, and handle plausible failure modes such as timeouts, malformed responses, rate limits, duplicate delivery, and unavailable dependencies.

Prefer explicit failure over silently producing an incorrect result.

Test the Behavior That Matters

Tests should protect:

  • Core user-visible behavior.
  • Business rules.
  • Important invariants.
  • Edge cases.
  • Failure and recovery paths.
  • Security-sensitive behavior.
  • Integration boundaries.

Prefer tests of observable contracts and end-to-end process flows over those tightly coupled to implementation details.

AI Applications Require Evals

Traditional tests are not sufficient for AI applications. Model behavior is probabilistic and can change with prompts, models, tools, context, and data. Important behavior should therefore be evaluated against representative examples of real use.

Evals should:

  • Measure behaviors that matter to the application.
  • Be based on observed or plausible failure modes.
  • Use representative production traces and test cases.
  • Be rerun when prompts, models, tools, or workflows change.
  • Prefer clear, application-specific criteria over generic quality metrics.

Start simple. Review real outputs, identify recurring failures, and turn those failures into repeatable evals.

If an AI behavior matters, there should be an eval for it.

Verify Rather Than Assume

When correctness depends on the behavior of a library, API, framework, or external system, verify it.

Use the strongest practical evidence available:

  • Type checking and static analysis.
  • Compilation.
  • Automated tests.
  • Integration and end-to-end tests.
  • Runtime inspection.
  • Logs and metrics.
  • Reproduction of the actual user workflow.

This is particularly important for coding agents: plausible code is not evidence of correct code.

The amount of verification should be proportional to the consequence of being wrong.

3Clarity

Code should be written for the person or agent who will read it later.

A reader should be able to answer:

  • What is this code doing?
  • Why is it doing it this way?

Make Behavior Apparent

Improve clarity by:

  • Choosing descriptive names.
  • Organizing related logic together.
  • Breaking complex operations into understandable units.
  • Making control flow and data flow apparent.
  • Making important state changes explicit.
  • Using comments or documentation where code alone cannot communicate intent.

Explain Rationale When Necessary

Comments are particularly valuable for explaining:

  • Non-obvious business rules.
  • Performance trade-offs.
  • Compatibility constraints.
  • Security requirements.
  • External system behavior.
  • Counterintuitive algorithms.
  • Important edge cases.
  • Architectural decisions that might otherwise appear accidental.

Prefer comments that explain why rather than restating what.

4Simplicity

Use the simplest design that satisfies the actual requirements.

Simple systems have fewer moving parts, less hidden state, fewer abstractions, and less infrastructure. They are easier to understand, test, operate, and change.

Question the Need

Before adding a feature, service, abstraction, dependency, workflow, or configuration option, ask:

  • What problem does this solve?
  • Who or what depends on it?
  • Is that need real now, or hypothetical?
  • What happens if we do nothing?

Do not create complexity in anticipation of requirements that may never arrive.

Use the Least Mechanism Necessary

If the capability is necessary, solve it with the least powerful mechanism that comfortably satisfies the requirement.

Prefer, in order:

  1. Basic language or platform capabilities.
  2. Standard libraries and native platform facilities.
  3. Established dependencies that materially improve the solution.
  4. New frameworks, services, abstractions, or infrastructure only when simpler options are inadequate.

A familiar or sophisticated tool is not automatically the right tool.

Complexity Must Earn Its Place

Additional complexity may be justified by requirements such as:

  • Scale.
  • Reliability.
  • Security.
  • Concurrency.
  • Performance.
  • Cost
  • Regulatory constraints.
  • Compatibility.

When complexity is necessary, make the reason explicit.

Don't Optimize What Can Be Removed

When something becomes slow, expensive, fragile, or difficult to maintain, do not assume the answer is to optimize it.

First ask whether it still needs to exist.

Before refactoring, scaling, or redesigning it, ask:

  • What value does it provide?
  • Is that value still worth its complexity and cost?
  • What would happen if we removed or simplified it?

Prefer elimination over optimization when the capability is unnecessary or no longer worth its cost.

Gate the Introduction of Third-Party Dependencies

The use of third-party libraries, packages, and frameworks can accelerate product development by way of responsibility delegation. But they can also introduce supply chain risks and unnecessary bloat into software products. Critically evaluate the inclusion of any third-party solution by asking:

  • Does the solution solve my problem?
  • Does the solution fit the scope of my problem?
  • What risks am I taking on by incorporating the solution?
  • Can a coding agent easily reproduce the required functionality?

5Concision

Concise code maximizes signal and minimizes noise.

The goal is not minimum line count. The goal is to make important information easy to see.

Signal can be obscured by:

  • Repetition.
  • Boilerplate.
  • Extraneous syntax.
  • Opaque names.
  • Excessive indirection.
  • Unnecessary abstractions.
  • Excessive commentary.
  • Overly fragmented code.

Do not remove duplication merely to satisfy a rule. A bad abstraction can be worse than a small amount of repetition.

6Maintainability

Maintainable systems allow future engineers to make changes correctly without reconstructing the original author's mental model.

Maintainable software:

  • Makes assumptions explicit.
  • Separates responsibilities appropriately.
  • Minimizes coupling.
  • Avoids unnecessary dependencies.
  • Avoids speculative functionality.
  • Uses abstractions that reflect meaningful concepts.
  • Provides stable and understandable interfaces.
  • Supports safe evolution.
  • Includes tests for important behavior.
  • Produces useful diagnostics.

Optimize for Safe Change

Prefer designs in which:

  • Related changes remain localized.
  • Important invariants are easy to identify.
  • Common modifications do not require editing unrelated components.
  • Failures occur close to their cause.
  • Dependencies are explicit.
  • Interfaces can evolve without widespread breakage.

7Extensibility

Systems should be designed so that areas expected to change can evolve without requiring broad modification of stable parts of the system.

Extensibility is not a requirement to make every component generic or pluggable.

Design Around Likely Axes of Change

Likely areas of variation may include:

  • Data providers.
  • Storage backends.
  • Authentication mechanisms.
  • Model providers.
  • Payment processors.
  • Output formats.
  • Processing strategies.
  • Deployment targets.
  • External integrations.
  • Business rules known to vary.

Where meaningful variation is expected, isolate it behind an appropriate component boundary.

Use Modular Boundaries

Components should have:

  • A coherent responsibility.
  • A clear interface.
  • Explicit inputs and outputs.
  • Minimal knowledge of other components' internals.
  • Limited side effects outside their responsibility.

Prefer Stable Contracts Over Internal Coupling

Avoid dependencies on:

  • Internal data structures.
  • Private implementation details.
  • Incidental side effects.
  • Undocumented behavior.
  • Shared mutable state.

Prefer Composition Over Central Branching

When new variants are expected, prefer adding components rather than continually expanding centralized conditional logic.

Preserve Replaceability

Where practical, components behind stable interfaces should be replaceable without redesigning the rest of the system.

Avoid Speculative Extensibility

Every interface, plugin mechanism, configuration layer, and abstraction adds complexity.

8Cost-Efficient Architecture

For cloud-based systems, keep idle system cost close to zero whenever practical.

Treat standing infrastructure cost as an architectural constraint, not merely an operational concern.

Do Not Provision Infrastructure by Habit

Technology familiarity is not sufficient justification for architectural complexity.

Do not introduce infrastructure such as:

  • Kubernetes clusters.
  • Permanently running virtual machines.
  • Always-on container fleets.
  • Dedicated database clusters.
  • Load balancers.
  • NAT gateways.
  • Search clusters.
  • Message-broker clusters.
  • Large reserved capacity.

unless requirements justify their ongoing cost and operational burden.

Prefer Architectures That Scale Down

When requirements permit, prefer components whose cost tracks actual usage.

Examples include:

  • Serverless compute.
  • Event-driven processing.
  • Consumption-based managed services.
  • Object storage.
  • Databases with low-cost or zero-capacity idle states.
  • On-demand batch execution.
  • Static delivery for static content.

Standing Costs Must Be Sensible

Always-on infrastructure may be justified by:

  • Consistently high utilization.
  • Strict latency requirements.
  • Specialized networking.
  • Stateful workloads.
  • Predictable high throughput.
  • Workloads unsuitable for scale-to-zero platforms.
  • Regulatory or isolation requirements.
  • Economics favoring reserved capacity at sustained utilization.

Consider Total Operational Cost

Infrastructure choices also create costs in:

  • Configuration.
  • Monitoring.
  • Patching.
  • Security.
  • Deployment.
  • Incident response.
  • Capacity planning.
  • Backup and recovery.
  • Developer cognitive load.

Start Small and Scale When Evidence Requires It

Whenever feasible:

  1. Start with the simplest low-idle-cost architecture that satisfies requirements.
  2. Measure actual workload and bottlenecks.
  3. Introduce additional infrastructure when observed requirements justify it.

9Security

Software should be secure by default.

Minimize trust, privilege, exposed surface area, and sensitive data.

Apply Least Privilege

Grant users, services, agents, and infrastructure only the permissions they need.

Avoid broad credentials, unnecessary administrative access, and shared secrets.

Make Trust Boundaries Explicit

Treat input crossing a system boundary as untrusted until validated.

Authenticate and authorize at the appropriate boundary, and do not rely on client-side enforcement for security decisions.

Minimize Sensitive Data

Collect, retain, expose, and log only the sensitive data the system actually needs.

Keep secrets out of source code, logs, and user-visible errors. Use appropriate secret-management mechanisms instead.

Fail Securely

For security-sensitive operations, failure should not accidentally grant access or weaken protection.

Prefer designs where unsafe states are difficult to create and easy to detect.

10Operability

Production software should make its health, behavior, and failures understandable.

Systems should be easy to diagnose, recover, and operate without unnecessary manual intervention.

Make the System Observable

Emit enough information to understand what the system is doing.

Use logs, metrics, traces, and health signals as appropriate to the system.

Prefer signals that help answer:

  • What failed?
  • Where did it fail?
  • Why did it fail?
  • Who or what was affected?

For AI applications, preserve end-to-end traces of model and agent execution so significant decisions, tool calls, errors, and outputs can be reconstructed.

Design for Recovery

Plan for expected failure modes.

Where appropriate, provide:

  • Safe retries.
  • Rollback.
  • Restore procedures.
  • Graceful degradation.
  • Recovery from partial failure.

Avoid designs where routine failures require heroic intervention.

Automate Routine Operations

Automate repetitive operational work when practical, especially deployment, recovery, maintenance, and health checks.

Operational complexity should earn its place just like implementation complexity.

11Infrastructure as Code

Production infrastructure should be defined as code whenever practical.

Infrastructure definitions should be version-controlled, reviewable, repeatable, and sufficient to recreate the environment without relying on undocumented manual configuration.

Prefer infrastructure changes made through code over changes made directly in consoles or production systems.

Infrastructure as Code should make it possible to:

  • Reproduce environments consistently.
  • Review and audit infrastructure changes.
  • Recover or rebuild infrastructure when necessary.
  • Reduce configuration drift between environments.

Manual changes may occasionally be necessary, but they should be captured back into the infrastructure definition rather than becoming permanent hidden state.

12Consistency

Similar problems should generally be solved in similar ways.

Consistency should apply to:

  • Naming.
  • Project structure.
  • Error handling.
  • Testing patterns.
  • API design.
  • Logging.
  • Configuration.
  • Data representation.
  • Common implementation patterns.

Do not preserve a poor pattern merely because it already exists.

13Documentation and Agent Context

The objective is to document the smallest durable body of knowledge that allows a human or agent to make good decisions about the system, exposed progressively as that knowledge becomes relevant.

Documentation is part of the software system. It should live with the code, evolve with the code, and be structured for efficient discovery by both humans and AI coding agents.

Co-location

Project documentation should normally be versioned in the same repository as the code it describes.

Core documentation belongs under:

docs/

Important engineering knowledge should not exist only in external wikis, chat threads, ticket systems, or people's memories.

External systems may supplement repository documentation, but information required to understand, modify, secure, or operate the software should remain discoverable from the repository.

Treat Documentation as an Agent Interface

Repository documentation is the interface through which coding agents acquire project-specific judgment.

Code shows what the system currently does. Documentation should explain what implementation alone cannot reliably reveal:

  • What the overarching principles of development for the system are.
  • Why the system is structured this way.
  • Which constraints are intentional.
  • Which patterns should continue.
  • Which patterns are temporary.
  • Which trade-offs have already been considered.
  • Which parts of the system are expected to change.
  • Which user-experience characteristics must be preserved.
  • Which security and operational boundaries must not be violated.

Good documentation reduces the chance that an agent makes a locally reasonable but globally incorrect change.

Organization

Documentation should be organized by concern rather than accumulated in a single handbook or miscellaneous notes file.

A typical structure may look like:

docs/
├── llms.txt
├── architecture.md
├── user-experience.md
├── security.md
├── data.md
├── operations.md
├── testing.md
└── adr/
    ├── llms.txt
    ├── 0001-use-postgresql.md
    └── 0002-use-event-driven-processing.md

Create only the documents the project actually needs.

For larger systems, documentation may also live closer to a subsystem:

services/
└── payments/
    └── docs/
        ├── llms.txt
        ├── architecture.md
        └── integration.md

Add hierarchy only when it creates a meaningful boundary for discovery or retrieval.

Progressive Disclosure Pattern

Documentation should follow a progressive-disclosure model:

Provide a small, high-signal index first. Link from it to focused documents. Retrieve deeper context only when the task requires it.

Every documentation collection intended for agent use should provide an llms.txt index.

At minimum:

docs/llms.txt

should be the primary entry point to project documentation.

Subcollections with substantial independent content should have their own index. In particular:

docs/adr/llms.txt

should index Architecture Decision Records.

An agent working on a task should:

  1. Read the nearest relevant llms.txt.
  2. Identify the documents related to the task.
  3. Open only those documents.
  4. Follow deeper references only when additional context is necessary.

For example:

docs/llms.txt
    ↓
docs/security.md
    ↓
docs/adr/llms.txt
    ↓
docs/adr/0012-use-oidc.md

An authentication change should not require loading unrelated documentation or every historical architectural decision.

Context should expand in response to need.

Keep Indexes Small and High-Signal

An llms.txt file is a routing layer, not the documentation itself.

It should contain:

  1. An H1 naming the collection.
  2. A short blockquote explaining its purpose.
  3. Minimal orientation needed to interpret the index.
  4. H2 sections grouping related resources.
  5. Markdown links with concise descriptions.

Example:

# Project Documentation

> Engineering and product documentation for this application. Use this index
> to locate the smallest set of documents relevant to the task.

## Architecture

- [System Architecture](architecture.md): Major components, boundaries, data flow, and deployment model.
- [Data Architecture](data.md): Data ownership, storage, schemas, retention, and lifecycle.

## Product

- [User Experience](user-experience.md): Core workflows, interaction principles, and interface conventions.

## Security

- [Security Architecture](security.md): Trust boundaries, authentication, authorization, secrets, and threat assumptions.

## Decisions

- [Architecture Decision Records](adr/llms.txt): Significant architectural decisions and their rationale.

Descriptions should tell the reader why the document may be relevant, not merely repeat its title.

If the index itself becomes expensive to understand, progressive disclosure has failed.

Prefer Focused, Self-Contained Documents

Each document should cover one coherent concern and contain enough local context to be useful on its own.

Prefer:

security.md
architecture.md
data.md

over:

misc-notes.md
everything-about-the-project.md
engineering-brain-dump.md

Split large topics when doing so creates a meaningful retrieval boundary, not merely to make files shorter.

Documentation should preserve information that cannot be reliably reconstructed from implementation alone, including:

  • Design rationale.
  • Important constraints.
  • Trade-offs.
  • Security assumptions.
  • User-experience requirements.
  • Operational requirements.
  • Important invariants.
  • External constraints.

Avoid documentation that simply paraphrases implementation details already obvious from the code.

Document Types

Core Documents

Core documents describe the current system and the constraints engineers and agents should understand before modifying it.

Typical documents include:

  • Architecture — Major components, boundaries, dependencies, data flow, and deployment model.
  • User Experience — Core workflows, interaction principles, usability constraints, and product conventions.
  • Security — Trust boundaries, authentication, authorization, secrets, threat assumptions, and security requirements.
  • Data — Ownership, storage, schemas, lifecycle, retention, and movement of data.
  • Operations — Deployment, observability, recovery, runtime dependencies, and operational expectations.
  • Testing — Testing strategy, critical test boundaries, and important quality requirements.

These describe the current state and enduring expectations of the system.

Architecture Decision Records

Significant architectural decisions should be recorded under:

docs/adr/

ADRs preserve reasoning that would otherwise be visible only through code or institutional memory.

Each ADR should be a self-contained Markdown document capturing:

  • The context that forced the decision.
  • The decision itself.
  • The rationale and trade-offs.
  • The consequences.
  • Relevant implementation references.

Maintain:

docs/adr/llms.txt

as the progressive-disclosure index for those decisions.

The index should group ADRs by architectural concern and include each record's current status and a concise description.

Agents should inspect the ADR index before making changes in areas where previous architectural decisions may constrain the solution.

Keep Documentation Current

Documentation that no longer reflects the system can be worse than missing documentation because humans and agents may treat it as authoritative.

When changing the system, determine whether the change affects:

  • Core documentation.
  • Existing ADRs.
  • Relevant llms.txt indexes.

Update affected documentation in the same change whenever practical.

Architectural decision changes should be recorded as documents that supersede the previous ADR rather than updating.