Structure your codebase so AI agents don’t wreck it

⚡ Quick Start: You’ll learn a practical framework for organizing functions and data models so both humans and AI coding agents can safely modify your codebase. No special tools needed, just disciplined naming, typing, and structure.

A post gaining traction on Hacker News lays out a clear architectural philosophy: if you want AI to help write your code, your codebase needs to be self-documenting first. The core argument is straightforward: how you split logic into functions and shape data determines whether AI agents help or hurt your project over time.

What stands out here is that this isn’t about prompting techniques or which AI tool to use. It’s about code structure as a defense mechanism.

Step 1: Build With Semantic Functions

Start by identifying your building blocks. A semantic function should take all required inputs, return all necessary outputs, and do nothing else.

Why this matters: AI agents navigating your codebase will read function signatures to understand what code does. If your functions have hidden side effects or unclear boundaries, agents will misuse them, and so will new team members.

Rules for semantic functions:

  • Keep them as minimal as possible to prioritize correctness
  • No side effects unless that’s the explicit goal
  • They should never need comments: the name and signature tell the full story
  • They should be extremely unit testable

Good examples range from quadratic_formula() to retry_with_exponential_backoff_and_run_y_in_between<Y: func, X: Func>(x: X, y: Y). Even if these functions are never reused, “future humans and agents going over the code will appreciate the indexing of information.”

💡 Tip: If logic is complicated and unclear inside a large flow, break it into a series of self-describing semantic functions that each take what they need, return data for the next step, and don’t do anything else.

Step 2: Wrap Complexity in Pragmatic Functions

Pragmatic functions are wrappers around semantic functions plus unique business logic. Think provision_new_workspace_for_github_repo(repo, user) or handle_user_signup_webhook().

Why this matters: production systems get messy. Pragmatic functions give that mess a home without contaminating your reusable building blocks.

Rules for pragmatic functions:

  • Use them in only a few places. If they spread everywhere, break the logic down into semantic functions instead
  • Expect them to change completely over time
  • Add doc comments, but don’t restate the function name. Note unexpected behavior like “fails early on balance less than 10”
  • Testing falls into integration testing, not unit testing

⚠️ Warning: Take doc comments on pragmatic functions with a grain of salt. Developers working inside the function may have forgotten to update them. Fact-check when something feels off.

Step 3: Make Wrong States Impossible With Models

Your data shapes should enforce correctness at the point of construction, not deep inside some unrelated flow.

Why this matters: every optional field is a question the rest of the codebase has to answer every time it touches that data. AI agents don’t have the tribal knowledge to answer those questions correctly.

Rules for models:

  • If a model allows field combinations that should never exist together, the model isn’t doing its job
  • Names should be precise enough that you can look at any field and know if it belongs. UnverifiedEmail, PendingInvite, BillingAddress — each tells you exactly what fields are valid
  • When two concepts are often needed together but independent, compose them: UserAndWorkspace { user: User, workspace: Workspace } instead of flattening

Step 4: Use Brand Types to Prevent Silent Bugs

Values with identical shapes can represent completely different domain concepts. { id: "123" } might be a DocumentReference in one place and a MessagePointer in another.

Why this matters: if your functions just accept { id: String }, the code accepts either one without complaint. AI agents generating new code will happily pass the wrong ID type, and you won’t find out until three layers deep.

Fix: Wrap primitives in distinct types. DocumentId(UUID) instead of a bare UUID. Accidentally swapping two IDs becomes a syntax error instead of a silent bug.

Step 5: Watch for the Two Common Breaks

Functions break when a semantic function morphs into a pragmatic function for convenience. Other parts of the codebase that relied on its tight behavior start doing things they didn’t intend. The fix: name functions by where they’re used, not just what they do. Make it clear in the name that behavior isn’t tightly defined.

Models break slower. They start focused, then someone adds “just one more” optional field because it’s easier than creating a new model. Then someone else does the same. Eventually, the model is a loose bag of half-related data. “When a model’s fields no longer cohere around its name, that’s the signal to split it.”

🔜 Next Steps

  • Audit your most-touched files for functions that mix semantic and pragmatic responsibilities
  • Identify models with more than two optional fields and evaluate whether they should be split
  • Add brand types for any ID or reference value that gets passed between modules
  • Run an AI coding agent against a well-structured module and a messy one, compare the output quality

This is significant because AI coding agents are only as good as the codebase they operate in. Clean structure isn’t just good engineering practice anymore: it’s a prerequisite for getting reliable output from your AI tools. You can find the full discussion and original post on Hacker News.

Scroll to Top